Compare commits

..

227 Commits

Author SHA1 Message Date
c8e33d43cb 体验地址更改 2025-06-13 13:56:47 +08:00
6c6af870ae jeecgboot vs 敲敲云 2025-06-13 13:53:33 +08:00
79107599a6 增加jeecgboot vs 敲敲云 说明 2025-06-13 13:51:56 +08:00
b6e8b37aee 禁用不好使 2025-06-12 17:00:35 +08:00
195ddd0421 降低shiro版本号,导致不兼容jdk8 2025-06-11 22:18:48 +08:00
9ddad931ff 【issues/8374】分页始终显示在底部 2025-06-10 09:52:27 +08:00
fc05fe1aff 补充.npmrc 2025-06-09 09:22:21 +08:00
7e325f68ca Merge pull request #8389 from 1298191366/master
open api功能优化
2025-06-06 15:22:11 +08:00
f74da23d06 升级积木报表到最新版2.0.0 2025-06-06 11:21:50 +08:00
e279915ba2 --author:wangshuai---date:2025-06-06---for:【QQYUN-12667】open api功能优化--- 2025-06-06 09:54:55 +08:00
bb6f077a95 Revert "//update---author:wangshuai---date:2025-06-05---for:open api功能优化:ak接口返回未明确提示错误---"
This reverts commit 7a031e6135.
2025-06-06 09:35:43 +08:00
7a031e6135 //update---author:wangshuai---date:2025-06-05---for:open api功能优化:ak接口返回未明确提示错误--- 2025-06-06 09:35:27 +08:00
157877f9a6 【issues/8233】resetFields时无法重置 2025-06-05 09:34:54 +08:00
25a71fa66c 【issues/8232】代码设置JSelectDept组件值没翻译 2025-06-05 09:34:11 +08:00
fdc02fa68a [-- author: liusq---date:20250604--for: issue【日志管理】的异常日志列表显示不正常 #8295 --] 2025-06-05 09:33:24 +08:00
ec3c34969a ---author:chenrui---date:2025/6/4-----for:[issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309 --- 2025-06-05 09:31:32 +08:00
5a215525d5 调整aksk测试数据 2025-05-29 16:07:30 +08:00
450b93d916 Merge pull request #8358 from xlh12306/master
使用原生方式对接口进行测试 修复授权管理页面重置AK,SK未写入到数据库中的异常
2025-05-29 15:56:13 +08:00
2d62bad2a9 使用原生方式对接口进行测试 修复授权管理页面重置AK,SK未写入到数据库中的异常
使用原生方式对接口进行测试
修复授权管理页面重置AK,SK未写入到数据库中的异常
2025-05-28 22:22:39 +08:00
0b10096f1c 修复微服务启动失败报错 2025-05-28 15:49:56 +08:00
431ddb8fcb 系统监控的头两个tab不好使,接口404--- 2025-05-27 22:51:25 +08:00
ddf0f61ae5 修复v3.8.0 存在绕过sql黑名单限制sql注入漏洞 #8335
修复minidao `getQueryTableInfo`无法解析带括号的表名、增加数据库名解析能力
2025-05-27 18:40:34 +08:00
4042579167 提供更新的数据库脚本,加入openapi的表和放开AI流程设计菜单 2025-05-27 09:51:52 +08:00
bd5fda5968 访问Swagger接口不带doc.html后缀,会丢失项目前缀/jeecg-boot/导致测试接口,返回下载文件 2025-05-26 19:03:29 +08:00
fdbd9c30ac 解决Swagger3Config 接口文档参数显示问题 #8325 2025-05-25 22:49:07 +08:00
b8b4d3f29d JVxetable升级到最新版 2025-05-25 18:09:19 +08:00
78212aa7c0 通讯录手机号码没有显示出来 #8282 2025-05-25 16:53:33 +08:00
6dc3c6af2a nacos增加aigc配置 2025-05-22 11:01:06 +08:00
b7a6812140 更新pom.xml,添加jeecg-boot-module-airag依赖 2025-05-22 09:25:39 +08:00
7efc51e30e 更新README.md,修改新手指南和在线体验部分的描述 2025-05-19 14:14:18 +08:00
bd83b994bc Merge pull request #8298 from xlh12306/master
openapi相关测试项修改
2025-05-18 17:01:28 +08:00
8fb81f331c 提交openAPiModel 2025-05-16 23:13:11 +08:00
fb188a83a1 提交openapi单元测试代码
调整接口管理的字段位置
授权管理删掉无用配置
授权管理编辑改为重置生成ak sk功能
授权页面应该改成跟角色授权类型的效果
2025-05-16 23:12:28 +08:00
9aea5de668 基于安全认证AK和SK鉴权调用OPEN API功能 2025-05-15 10:23:27 +08:00
8979dd7ae9 Merge branch 'master' into master 2025-05-14 22:17:00 +08:00
a56bd05389 将请求方式修改为大写,修复openapi回调执行调用接口失败的问题 2025-05-14 22:04:06 +08:00
9cf3328ea4 uniapp3代码生成器模板 2025-05-14 18:33:15 +08:00
37c593e1d4 删除jsqlparse代码 2025-05-14 15:04:57 +08:00
49ba40e98a 提交api接口设计前端页面以及后端更改 2025-05-10 23:56:06 +08:00
0e184eaa64 升级shiro到2.0.4 2025-05-09 10:17:14 +08:00
86a3ed9dae 更新OpenApiController,使用创建用户生成token签名,简化用户关联逻辑 2025-05-09 10:11:33 +08:00
94bff11eb1 移除sqlparse代码改调minidao方法、升级fastjson版本号到2.0.57 2025-05-07 13:37:54 +08:00
590d73dfe3 修改版本号 2025-05-06 16:01:31 +08:00
fe9630d15c 【issues/8211】判断逻辑写的有问题--
org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java
2025-05-06 09:24:20 +08:00
77ae25b86a 解决jeecg-boot-V3.8.0使用下拉树组件时高频率报错UT005071: Undertow request failed HttpServerExchange{ GET /sys/dict/loadTreeData} #8217 2025-05-04 17:28:30 +08:00
3b34276cf8 新增一个UI组件,支持通过部门选人,更加便捷 2025-05-04 16:34:12 +08:00
cffba084fc 【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错 2025-05-04 16:27:28 +08:00
d1589acc41 回滚swagger配置 2025-05-03 17:14:18 +08:00
0d18e536f0 解决swagger生成文档,参数乱码问题 2025-04-29 16:14:51 +08:00
21392c44f8 issues/8181】同步部门(将钉钉部门同步到本地) #8181 2025-04-29 09:54:21 +08:00
2730d8e06f 流程编排脚本节点不好使问题 2025-04-28 17:36:57 +08:00
0002606d41 流程编排脚本节点不好使问题 2025-04-28 12:54:38 +08:00
8bd19484ee 升级jeecg-aiflow依赖版本至1.0.3 2025-04-28 11:51:55 +08:00
5d2db92613 默认切jdk17 2025-04-27 10:21:56 +08:00
c1b39d21dd 升级AI引擎,底层接口开放 2025-04-26 16:16:50 +08:00
e83c9b8190 【jvxeTable】expandConfig属性无用 2025-04-26 16:09:49 +08:00
9bd03f467d 【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效。 2025-04-25 16:59:03 +08:00
e032591366 • 【AI】兼容jdk21,让程序可以启动,但是提示AI流程编排无法使用
• 【AI】流程,入参中的必填没有校验
• 【AI】开启多租户,导致流程接口调用提示流程不存在
• 【AI】流程调用流程接口改成无需登录
2025-04-24 14:02:32 +08:00
de767e07b4 【issues/8093】删除后会先变成编码再显示label文字 2025-04-24 09:27:58 +08:00
b77d3e36ab 【issues/8075】可编辑行无法获取最新的值 2025-04-24 09:27:16 +08:00
7885aaed3b 【issues/8137】vxetable表格禁用后分页隐藏了 2025-04-24 09:26:27 +08:00
6f4c2eb77c 【pull/8013】修复 BasicTable position 属性类型配置 --- 2025-04-24 09:25:22 +08:00
3f0597a0f6 【issues/7986】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效 2025-04-24 09:24:48 +08:00
04a3764f00 弹窗中勾选,再点取消,值被选中了
弹窗勾选了值,点击取消再次打开弹窗遗留了上次的勾选的值
2025-04-24 09:24:00 +08:00
68464109de Merge remote-tracking branch 'origin/master' 2025-04-23 19:10:59 +08:00
79866c5823 AI大模型使用本地部署的deepseek报错 #8164 2025-04-23 19:10:44 +08:00
d64b8ecaef Merge pull request #8160 from EightMonth/master
修复 #8134
2025-04-23 18:42:45 +08:00
ec9f2b146a 敲敲云支持在线免费搭建AI知识库 2025-04-23 15:32:37 +08:00
69fd2888a1 Update application.yml 2025-04-23 11:12:59 +08:00
cb1d8e3527 修复 #8134 2025-04-22 10:05:04 +08:00
a89b299a4b 邮件发送失败,修改yml配置方式 2025-04-21 10:19:23 +08:00
7a15bfc161 错别字修复 2025-04-20 16:51:56 +08:00
65f7eb9542 支持全信创环境 2025-04-18 14:54:53 +08:00
92808f9164 JeecgBoot 里程碑 v3.8.0,AI 首版本发布 2025-04-17 14:13:36 +08:00
d3ed3f49d3 JeecgBoot 里程碑 v3.8.0,AI 首版本发布 2025-04-17 14:12:34 +08:00
da3d39c59c 里程碑 v3.8.0,AI 首版本发布 2025-04-17 14:11:51 +08:00
f332758179 【issues/3656】popupdict回显 2025-04-16 18:45:59 +08:00
61a8904e52 【issues/8097】icon组件svg图标显示空白问题 2025-04-16 18:42:22 +08:00
b032a415aa AI介绍 2025-04-15 16:57:24 +08:00
81821eeddc AI大模型介绍 2025-04-15 16:14:07 +08:00
5288b1fe73 AI介绍 2025-04-15 16:12:52 +08:00
408f192b37 一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。 2025-04-15 16:11:40 +08:00
6a9f188282 AI大模型的集成导致暂时不支持jdk21 2025-04-15 16:07:24 +08:00
168f15e1c2 AI大模型说明 2025-04-15 16:04:11 +08:00
ae814a7e8b 删除废弃无用代码 2025-04-15 15:03:25 +08:00
7160ea32cb 默认主题配置不生效 2025-04-15 11:24:22 +08:00
19fc610ef5 【issues/8098】修复示例中的Tab标签页关闭操作问题 2025-04-15 11:21:08 +08:00
44146f073e 【issues/8078】角色选择组件点击文字部分会一直选中 2025-04-15 11:19:36 +08:00
8abce5ad9c 升级新版online 2025-04-15 10:52:35 +08:00
447e439612 还原提交 2025-04-15 10:32:13 +08:00
088c79238e 解决翻译Popup字典错误 #8114 2025-04-15 09:52:22 +08:00
ad72c807f6 JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用 2025-04-14 20:16:17 +08:00
db3d95e1a7 AI模型发布 2025-04-14 20:06:19 +08:00
1a16b5550f AI项目介绍更新 2025-04-14 20:01:04 +08:00
c6fe809013 AIGC大模型增加向量库的yml配置 2025-04-14 19:05:27 +08:00
677b57ae09 【知识库】知识库上传之后没有命中,下载文件下载不下来。但是磁盘里面有--- 2025-04-14 18:36:00 +08:00
02f21de8d5 AI大模型,key调用错误处理 2025-04-14 18:32:02 +08:00
f7ca26fff0 AI大模型调用错误处理 2025-04-14 18:15:11 +08:00
6c6aa964e8 补充AI大模型对接向量库配置说明 2025-04-14 17:45:04 +08:00
b0bab050dd AI大模型功能 2025-04-14 17:40:44 +08:00
87197be8f7 aiflow采用jdk8编译,解决项目不支持jdk8问题 2025-04-14 16:18:58 +08:00
75aa1fe5a0 解决bug:ai大模型-ai应用管理-配置菜单后404 #8111 2025-04-14 15:05:01 +08:00
3e434ce6b4 v3.8.0代码上传,AI大模型功能发布 2025-04-14 13:32:29 +08:00
e07508d29f v3.8.0代码上传,AI大模型功能发布 2025-04-14 11:42:46 +08:00
e18e980892 Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-04-14 11:38:44 +08:00
a2f18fd0d9 代码生成器模板更新,popup字典下拉翻译问题 2025-04-10 15:36:17 +08:00
9ad7ef5ab4 Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-04-09 16:25:32 +08:00
27a7046465 Popup字典列表渲染支持翻译 2025-04-09 16:04:08 +08:00
47a2a6fbac Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-04-09 13:45:29 +08:00
83b1c8692e ApiSelect组件支持分页下拉方案 #7883 2025-04-09 09:40:58 +08:00
73a5f64d7e 【issues/8034】hash模式下退出重登录默认跳转地址异常 2025-04-09 09:39:34 +08:00
1d18a54b8a 解决升级undertow后,性能监控tomcat信息tab项展示的问题 2025-04-09 09:36:49 +08:00
e877929a42 Merge branch 'HEAD' of https://github.com/jeecgboot/JeecgBoot.git 2025-04-08 22:05:42 +08:00
5f5970d7d5 解决钉钉登录报错 class com.alibaba.fastjson2.JSONObject cannot be cast to class com.alibaba.fastjson.JSONObject 2025-04-08 22:05:20 +08:00
e05c9ddc08 Merge pull request #8073 from EightMonth/master
新增controller测试用例
2025-04-08 16:27:43 +08:00
b77e9e25a3 AI功能介绍 2025-04-08 15:51:11 +08:00
a8bf090352 Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-04-08 15:29:22 +08:00
b6c9f9db68 集成aiflow AI流程设计器 2025-04-08 14:53:58 +08:00
8c406dcdea 项目介绍 2025-04-08 11:44:45 +08:00
215b263d1a 项目介绍 2025-04-08 11:41:21 +08:00
4247ffd082 AIGC专题介绍页 2025-04-08 11:04:32 +08:00
ec87621858 AI流程大模型功能 2025-04-08 10:48:24 +08:00
bc6cf0e7b4 Merge branch 'HEAD' of https://github.com/jeecgboot/JeecgBoot.git 2025-04-08 09:56:48 +08:00
353ad058d1 AIGC 功能视频介绍 2025-04-08 09:56:32 +08:00
cf63b056d5 新增controller测试用例 2025-04-08 09:20:00 +08:00
60eeef14c7 更新pom.xml,替换aiflow依赖为jeecg-aiflow并调整版本 2025-04-08 00:02:35 +08:00
25b30153a0 集成jeecg的aiflow AI流程设计器 2025-04-07 23:23:52 +08:00
beff2a271e Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-04-07 16:10:29 +08:00
f743622655 Merge pull request #8066 from EightMonth/master
编写单个controller的测试用例
2025-04-07 16:08:30 +08:00
70a1f0b7db 编写单个controller的测试用例 2025-04-07 15:59:46 +08:00
e166d916da 重要参数补充 2025-04-07 15:42:21 +08:00
b79ba97614 v3.8.0 完整数据库脚本 2025-04-07 15:29:58 +08:00
4f371672d0 升级版本号v3.8.0,归档历史增量SQL 2025-04-07 15:18:02 +08:00
80ae183b58 升级版本号v3.8.0 发布AI大模型功能 2025-04-07 14:33:00 +08:00
b4cac11368 AI大模型管理功能后台代码 2025-04-07 14:10:37 +08:00
4936e140e9 归档老的增量SQL 2025-04-07 14:09:27 +08:00
9fd4c3b3d2 将demo迁移到module模块中 2025-04-07 14:09:17 +08:00
4f2f1d6265 将AI对话聊天界面换成新地址 2025-04-07 14:08:44 +08:00
b878f6b6be 删除老的AI聊天界面 2025-04-07 14:08:36 +08:00
507289ff6c AIGC应用平台+知识库模块 2025-04-07 14:08:27 +08:00
bb4cdf93b1 Merge pull request #8059 from EightMonth/master
优化swagger文档架构改造
2025-04-04 23:05:37 +08:00
7d7fde63ec 【issues/7996】表格列组件取消所有或者只勾选中间,显示非预期 2025-04-04 22:46:30 +08:00
c22fd21233 【issues/7812】linkageConfig改变了,vxetable没更新 2025-04-04 22:45:00 +08:00
d4d0c884f0 归集spring-doc默认配置 2025-04-03 22:31:26 +08:00
e6fe3459e4 配置调整 2025-04-03 18:17:43 +08:00
9ebc3c49ec 优化swagger文档架构改造 2025-04-03 17:54:51 +08:00
45e9b03e2d app地址修改 2025-04-03 12:05:47 +08:00
696b65240f AI大模型支持 2025-04-03 12:03:34 +08:00
f4339677e7 版本发布日期调整 2025-04-03 11:50:05 +08:00
0aaf0ad3ad AIGC应用平台介绍 2025-04-03 11:33:45 +08:00
0e5cd5bb3e AIGC介绍 2025-04-03 11:29:26 +08:00
24406a4cac AI介绍 2025-04-03 11:27:53 +08:00
1a1c840a3b AI功能展示 2025-04-03 11:25:29 +08:00
2e257343a4 aigc独立专题介绍页面 2025-04-03 11:20:05 +08:00
7871c465c8 ERP风格内嵌子表生成源代码,展开子表的会根据展开的次数进行创建销毁子表的组件 2025-04-03 10:09:36 +08:00
714d9dc244 解决启动警告 WARN io.undertow.websockets.jsr:68 - UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used 2025-04-03 00:00:18 +08:00
f23b176cf4 fix: update link text for GitHub URL in header 2025-04-02 22:45:42 +08:00
494f5e21f5 undertow配置、增加springdoc配置、简化flyway配置 2025-04-01 22:02:39 +08:00
dea260cae3 注释掉日志打印 2025-04-01 21:35:45 +08:00
0fcc08c204 fix: update import path for AiChatProperties in ChatServiceImpl 2025-04-01 21:33:08 +08:00
608d2e1318 Merge pull request #8038 from EightMonth/knife_biz
Knife4j升级4.4.0 业务代码改动
2025-04-01 21:28:35 +08:00
7e436f2efd Merge pull request #8039 from EightMonth/knife_arch
Knife4j升级4.4.0 架构代码改动
2025-04-01 21:28:23 +08:00
c18fb68e81 接口文档调整,为免登录接口排除token校验请求头 2025-04-01 17:01:58 +08:00
299f63c6e9 修改token验证请求头信息及排除不需要的接口信息 2025-04-01 15:05:08 +08:00
20037ae02b 添加误移除内容 2025-03-31 16:36:43 +08:00
dcdbb5ac7a 3.7.4 版本数据库脚本 2025-03-31 16:11:25 +08:00
cd9ba3bac9 新增改动 2025-03-31 15:58:26 +08:00
060bf2282c 更新readme 2025-03-31 15:15:17 +08:00
d88f2f81e9 knife4j升级4.4.0注解改造 2025-03-31 14:54:06 +08:00
18cc746fcf 升级java-jwt到4.5.0 2025-03-31 14:35:47 +08:00
fcc7842e89 修改自动生成接口文档范围
(cherry picked from commit ff58a1dd26)
2025-03-31 14:32:33 +08:00
e91af8cfd3 knife4j升级4.4.0架构改造 2025-03-31 14:25:56 +08:00
a57cc1fa3d 3.7.4 版本升级online在线开发模块 2025-03-31 14:23:05 +08:00
80d37f9a03 3.7.4 版本升级online在线开发模块 2025-03-31 11:58:30 +08:00
502ef2f65d 【同步3.7.4版本代码】新增全局布局隐藏配置,优化多个组件的属性和逻辑 2025-03-30 19:09:07 +08:00
62daec9c16 3.7.4 版本增量升级SQL 2025-03-30 18:40:49 +08:00
9191a8b620 【3.7.4 开源代码同步】新增断言异常类和断言工具类,优化JWT工具类,更新文件类型白名单,切换undertow配置,修改多个YAML配置文件 2025-03-30 17:51:55 +08:00
4fb53637aa 支持数据库说明 2025-03-28 18:32:58 +08:00
178a2368cb 3.7.4(预计发布日期:2025-04-10,代码逐步提交) 2025-03-28 09:35:57 +08:00
0813dee0d4 优化oConvertUtils中childArray转换逻辑 2025-03-27 19:47:09 +08:00
9579a60abd jdk支持说明:支持jdk8、17、21 2025-03-27 19:38:46 +08:00
5cd5bf7a0b 移除冗余Flyway配置,优化application.yml文件 2025-03-27 18:48:03 +08:00
c0265981d1 视频介绍 2025-03-27 18:43:18 +08:00
2ec13165d3 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	jeecg-boot/pom.xml
2025-03-27 18:26:20 +08:00
ac5ee60364 Merge pull request #8027 from EightMonth/master
移除junit4,使用默认junit5
2025-03-27 18:09:34 +08:00
5ff1b56fe2 移除junit4,使用默认junit5 2025-03-27 16:58:35 +08:00
ea914be3a6 升级至3.7.4,更新Dockerfile及jdk依赖 2025-03-27 15:19:41 +08:00
cb65e1796c 【3.7.4版本底层依赖大升级】
默认改成JDK17;fastjson升级至2.0.56;升级spring-cloud小版本;升级jimureport到最新版1.9.5;升级weixin4j到2.0.1;
spring-cloud >2021.0.8
spring-cloud-alibaba >2021.0.6.2
2025-03-27 15:19:00 +08:00
ff815f1ae6 更新版本至3.7.4并调整发布时间 2025-03-27 15:17:48 +08:00
c8ebdc162f 更新版本至3.7.4并调整发布时间 2025-03-27 15:17:08 +08:00
76df4f8600 Merge pull request #8006 from EightMonth/master
回退 "fastjson升级至2.0.43"
2025-03-25 15:04:30 +08:00
a27b94c4c6 Revert "fastjson升级至2.0.43"
This reverts commit d87ffc11e7.
2025-03-25 14:55:36 +08:00
5ad06274cb 升级shiro至1.13.0及shiro-redis至3.2.3 2025-03-25 14:34:31 +08:00
d38fa1901e Merge pull request #8003 from EightMonth/master
fastjson升级至2.0.43
2025-03-25 14:29:10 +08:00
b4aab76057 Merge branch 'master' of https://github.com/EightMonth/jeecg-boot 2025-03-25 14:00:58 +08:00
d87ffc11e7 fastjson升级至2.0.43 2025-03-25 14:00:43 +08:00
a67b3409b1 Merge pull request #7981 from EightMonth/master
修复 #7951 CVE-2023-6378
2025-03-21 23:38:08 +08:00
e11d831114 修复 #7951 CVE-2023-6378 2025-03-20 16:50:37 +08:00
ec410456b2 Merge pull request #7879 from yangyang122/feature/tenant_edit_role
in tenant mode, edit role constraints change
2025-03-20 10:46:46 +08:00
e9326cab8c Merge pull request #7651 from testnet0/master
修复flyway自动升级失败
2025-03-20 10:39:08 +08:00
61d2857ca7 【issues/7940】componentProps写成函数形式时,updateSchema写成对象时,参数没合并 --- 2025-03-20 10:09:48 +08:00
cba4847413 【issues/7948】修复JselectRole组件不支持双向绑定 --- 2025-03-20 10:09:06 +08:00
bc56384325 【issues/7954】BasicUpload组件上传文件,限制上传格式校验出错 --- 2025-03-20 10:08:20 +08:00
d7dc1b8dc7 【issues/7956】修复showSummary: false时且有内嵌子表时合计栏错位 2025-03-20 10:06:57 +08:00
11d9f2b4e4 视频介绍 2025-03-18 10:10:33 +08:00
64fcf7fcfc 提供完整的初始化sql 含积木报表最新增量sql 2025-03-16 16:04:43 +08:00
e6ddeef18c 默认关闭AI助手悬浮按钮 2025-03-13 09:53:05 +08:00
47a074d9cb 解决严重bug,War包方式部署,服务启动报错 2025-03-11 09:43:39 +08:00
c32f0407e8 更新README.md,添加AI大模型文档链接 2025-03-09 16:38:00 +08:00
22bbc23069 更新README.md,优化AI功能列表及支持模型 2025-03-07 18:25:33 +08:00
b2078d502a 更新README.md,添加AI平台支持及功能介绍 2025-03-07 18:20:10 +08:00
ed165464c8 升级druid版本到1.2.24 #7869
升级jeewx-api版本到weixin4j 2.0.0
2025-03-07 15:55:09 +08:00
9cf66e7026 Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-03-06 22:04:19 +08:00
11f8038196 AIGC功能模块即将出炉 2025-03-06 22:03:29 +08:00
d2105dc095 in tenant mode, edit role constraints change 2025-03-04 14:58:38 +08:00
32287d98ac Merge pull request #7833 from hoperunChen/QQYUN/QQYUN-10958
[QQYUN-10958]删除jeecgboot中的MongoAutoConfiguration,因为jm-nosql中已经有了
2025-03-03 17:19:27 +08:00
31cf94ac7b Merge pull request #7850 from EightMonth/master
修复 #7702
2025-03-03 16:38:39 +08:00
0d28c92c83 master分支默认jdk8 2025-03-02 17:57:26 +08:00
1cc5cee92d 升级项目介绍 2025-03-02 17:53:04 +08:00
26246eda7a 升级druid版本到1.2.24 #7869
升级jeewx-api版本到weixin4j 2.0.0
2025-03-02 17:49:29 +08:00
10dd2a2dd0 反馈bug 2025-02-28 16:05:08 +08:00
5abc745496 jeecg官方文档更新 2025-02-26 10:30:57 +08:00
353e8e2fb6 [issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288--- 2025-02-24 22:49:58 +08:00
56661815ec 【issues/7830】合计小数计算精度 --- 2025-02-24 22:48:45 +08:00
03b6d20fcb 修复 #7702 2025-02-24 17:33:52 +08:00
2388c8c46d [QQYUN-10958]删除jeecgboot中的MongoAutoConfiguration,因为jm-nosql中已经有了 2025-02-20 10:43:20 +08:00
ac30ed67bc 修复flyway自动升级失败 2024-12-25 15:38:39 +08:00
810 changed files with 28926 additions and 10940 deletions

164
README-AI.md Normal file
View File

@ -0,0 +1,164 @@
AIGC应用平台介绍
===============
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
JeecgBoot平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
### AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
#### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
> - Markdown文档库导入
> JEECG AI允许用户直接导入整个Markdown文档库这不仅保留markdown格式还支持图片的导入确保文档内容的完整性和可视化效果。
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染。
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
| 功能 | Dify | Jeecg AI |
|------------|------------------|-----------------------------------------|
| AI工作流 | 有 | 有 |
| RAG 管道向量搜索 | 有 | 有 |
| AI模型管理 | 有 | 有 |
| AI应用管理 | 有 | 有 |
| AI知识库 | 有 | 有 |
| 产品方向 | 一款独立的 LLM 应用开发平台 | 低代码与AIGC应用二者结合的平台 |
| 业务集成 | 业务集成能力弱 | 更方便与业务系统集成,调用系统接口和逻辑更加方便 |
| AI业务流 | 侧重AI逻辑流程 | AI流程编排作为低代码的业务引擎用户可以通过AI流程配置各种业务流和AI流程 |
| 实现语言 | python + react | JAVA + vue3 |
| 上传markdown文档库(支持图片) | 不支持 | 支持 |
| AI对话支持发图和展示图片 | 支持 | 支持 |
### 安装向量库 pgvector
- https://help.jeecg.com/aigc/config
## 功能特点
- AI流程: 提供强大的AI流程设计器引擎支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
- AI流程即服务: 通过AI流程编排你需要的智能体结合AI+自定义开发节点 实现功能性 API让你瞬间拥有各种智能体API。
- AI助手对话功能: 集成 ChatGPT、Deepseek、智普、私有大模型 等 AI 模型,提供智能对话和生成式 AI 功能,深度与知识库结合提供更精准的知识。
- RAG 功能: 涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本支持检索增强生成RAG将未训练数据与 AI 模型集成,提升智能交互能力。
- AI 知识库: 通过导入文档或已有问答对进行训练,让 AI 模型能根据文档以交互式对话方式回答问题。
- 模型管理支持对接各种大模型包括本地私有大模型Deepseek/ Llama 3 / Qwen 2 等)、国内公共大模型(通义千问 / 腾讯混元 / 字节豆包 / 百度千帆 / 智谱 AI / Kimi 等和国外公共大模型OpenAI / Claude / Gemini 等);
- 无缝嵌入Iframe一键嵌入,支持将AI聊天助手快速嵌入到第三方系统让系统快速拥有智能问答能力提高用户满意度。
#### 在线体验
- JeecgBoot演示 https://boot3.jeecg.com
- 敲敲云在线搭建AI知识库https://app.qiaoqiaoyun.com
## 技术交流
- 开发文档https://help.jeecg.com/aigc
- QQ群716488839
## 功能列表
- AI应用管理(普通应用、高级流程应用)
- AI模型管理
- AI知识库
- AI应用平台(普通、对接AI流程)
- AI流程编排
- AI聊天支持嵌入第三方
- AI向量库对接
## 支持AI模型
| AI大模型 | 支持 |
|---------------| --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |
## AIGC能做什么
AIGC模块是一个基于AI的自动化流程编排工具和聊天应用搭建平台它可以帮助用户快速生成AI流程接口和聊天应用提高效率。
以下是一些具体的应用场景和示例:
- 你可能需要一个翻译接口可以通过AI流程编排搭建出来。
- 你可能需要一个接口转换工具可以通过AI流程编排搭建出来。比如jimureport所需要接口返回格式与你的系统不同你通过AI接口实现自动转换
- 你可能需要一个聊天机器人可以通过AI流程编排搭建出来。
- 你可能需要一个自动化流程可以通过AI流程编排搭建出来。
- 你可能需要一个自动化处理文件的流程可以通过AI流程结合python脚本实现操作电脑文件等。
## AI应用平台功能展示
AI模型列表
![](https://oscimg.oschina.net/oscnet//a5fb3e0d69ca1706b0de221535c7acaa.png)
选择AI模型配置你的参数
![](https://oscimg.oschina.net/oscnet//1f941472758a5fc227f54f2683953b8e.png)
AI知识库支持手工录入文本导入pdf\\word\\excel等文档支持问答对训练
![](https://oscimg.oschina.net/oscnet//150bb33f48d6c8e2ae059e2a58f4200b.png)
![](https://oscimg.oschina.net/oscnet//032d16c915b0f79318935484c81df260.png)
AI流程提供强大的AI流程设计器引擎支持编排 AI 工作过程,满足复杂业务场景,支持画布上构建和实时运行查看 AI流程运行情况。
![](https://oscimg.oschina.net/oscnet//f40f9aa275cd4aea94e1c209513151e2.png)
目前支持的节点有开始、结束、AI知识库节点、AI节点、分类节点、分支节点、JAVA节点、脚本节点、子流程节点、http请求节点、直接回复节点等节点
![](https://oscimg.oschina.net/oscnet//6d86480ab1bbfab5b2e6992b416b2152.png)
节点项配置
![](https://oscimg.oschina.net/oscnet//90a5f76b6b4fc406e2e2b87245b35459.png)
在线运行看结果
![](https://oscimg.oschina.net/oscnet//bc9817a7bbd94936a5a3e885abe3cb38.png)
AI应用配置支持AI流程配置和简单的AI配置
![](https://oscimg.oschina.net/oscnet//a853d9be4d3756806799ad025e722df8.png)![](https://oscimg.oschina.net/oscnet//d3bcbf5977c6fb75a8f996e1e40590be.png)
可以关联多个知识库右侧是AI智能回复你可以搭建自己的智能体比如搭建一个 “诗词达人” “翻译助手”
![](https://oscimg.oschina.net/oscnet//c26a848136be3e22ec1e0651e78976c2.png)
可以将创建的聊天应用,集成到第三方系统中
![](https://oscimg.oschina.net/oscnet//39c6f589ef46f0454b229915ffa263f4.png)

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.7.3 (Release date: 2025-02-10)
Current version: 3.8.0 (Release date: 2025-04-18)
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-guojusoft-orange.svg)](http://www.jeecg.com)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -21,7 +21,7 @@ Current version: 3.7.3 (Release date: 2025-02-10)
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!
@ -37,7 +37,7 @@ AI Empowering Low-Code: Currently, JeecgBoot supports AI large models such as Ch
Technical support
-----------------------------------
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
Problems or bugs in use can be found in [Making on the Issues](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
##### Project description
@ -49,6 +49,11 @@ Problems or bugs in use can be found in [Making on the Issues](https://github.co
| `jeecg-uniapp` | [APP development framework, a code multi terminal adaptation, and support APP, small program, H5](https://github.com/jeecgboot/jeecg-uniapp) |
### Video Introduction
[![](https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/jeecgvideo02.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
Download other source code
-----------------------------------
@ -64,8 +69,8 @@ Jeecg-Boot AI low code platform can be applied in the development of any J2EE pr
Starts the project
-----------------------------------
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker Quick start](https://help.jeecg.com/java/docker/quick.html)
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start](https://help.jeecg.com/java/docker/quick)
@ -74,9 +79,9 @@ Technical documentation
- Website [http://www.jeecg.com](http://www.jeecg.com)
- Demo [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat.html)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
- Newbie guide [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
- QQ group ⑩716488839、⑨808791225、⑧825232878、⑦791696430、⑥730954414(full)、683903138(full)、⑤860162132(full)、④774126647(full)、③816531124(full)、②769925425(full)、①284271917(full)
- QQ group ⑩716488839、⑨808791225
@ -176,7 +181,7 @@ Technical Architecture:
#### Development Environment
- Language: Java 8+ (17)
- Language: Java Default Jdk17(support jdk8、jdk21)
- IDE(JAVA) : IDEA (lombok plug-in must be installed)
@ -193,17 +198,17 @@ Technical Architecture:
- Basic framework: Spring Boot 2.7.18
- Microservice framework: Spring Cloud Alibaba 2021.0.1.0
- Microservice framework: Spring Cloud Alibaba 2021.0.6.2
- Persistence layer framework: MybatisPlus 3.5.3.2
- Report tool: JimuReport 1.9.3
- Report tool: JimuReport 1.9.5
- Security framework: Apache Shiro 1.12.0, Jwt 3.11.0
- Security framework: Apache Shiro 1.13.0, Jwt 4.5.0
- Microservice technology stack: Spring Cloud Alibaba, Nacos, Gateway, Sentinel, Skywalking
- Database connection pool: Alibaba Druid 1.1.22
- Database connection pool: Alibaba Druid 1.1.24
- Log printing: logback
@ -242,8 +247,16 @@ Technical Architecture:
| --- | --- |
| DeepSeek | √ |
| ChatGPT | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |
AI Config https://help.jeecg.com/java/ai/aichat
AI APP: https://help.jeecg.com/aigc
AI Config https://help.jeecg.com/java/ai/aichat.html
## Microservice solutions
@ -255,7 +268,7 @@ AI Config https://help.jeecg.com/java/ai/aichat.html
- 6. Distributed files Minio and Alioss √
- 7. Unified permission control
- 8. Service monitoring SpringBootAdmin√
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 9. link tracking Skywalking [reference document](https://help.jeecg.com/java/springcloud/super/skywarking)
- 10. Messaging middleware RabbitMQ √
- 11. Distributed task xxl-job √
- 12. Distributed Transaction Seata
@ -272,8 +285,8 @@ AI Config https://help.jeecg.com/java/ai/aichat.html
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")
### quick start
- Microservice Development [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud.html)
- Microservice Development [Monomer upgrade to microservice](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker starts the micro-service background](https://help.jeecg.com/java/docker/springcloud)
### Effect of system

185
README.md
View File

@ -2,12 +2,12 @@
JeecgBoot AI低代码平台
===============
当前最新版本: 3.7.3发布日期2025-02-10
当前最新版本: 3.8.0发布日期2025-04-18
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/jeecgboot/JeecgBoot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://guojusoft.com)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/jeecgboot/JeecgBoot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/jeecgboot/JeecgBoot)
@ -16,78 +16,146 @@ 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.xSpringCloudAnt Design Vue3Mybatis-plusShiroJWT支持微服务、多租户;支持 AI 大模型 DeepSeek 和 ChatGPT、Ollama本地模型; 强大的代码生成器让前后端代码一键生成,无需写任何代码! JeecgBoot 引领 AI 低代码开发模式(AI生成-> OnlineCoding-> 代码生成器-> 手工MERGE) 帮助解决Java项目80%的重复工作让开发更多关注业务。既能快速提高效率节省成本同时又不失灵活性AIGC能力AI对话助手、AI建表、AI写文章、AI流程编排、AI知识库问答等等.
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台,旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro强大的代码生成器让前后端代码一键生成无需写任何代码
成套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业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
`AI赋能低代码:` 目前JeecgBoot支持AI大模型`ChatGPT``DeepSeek`,现在最新版默认使用`DeepSeek`速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表、AI写文章、AI流程编排、AI知识库问答等功能。
适用项目
-----------------------------------
JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中支持信创国产化默认适配达梦和人大金仓。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM其半智能手工Merge的开发方式可以显著提高开发效率70%以上,极大降低开发成本。
### 视频介绍
[![](https://upload.jeecg.com/jeecg/qiaoqiaoyunsite/jeecgvideo02.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
#### 项目说明
适用项目
-----------------------------------
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 为使用国产软件和硬件的用户提供了高效的开发解决方案。
项目说明
-----------------------------------
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `jeecg-uniapp` | [配套APP框架](https://github.com/jeecgboot/jeecg-uniapp) 适配多个终端支持APP、小程序、H5 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
- 快速体验: [一分钟体验低代码](https://jeecg.blog.csdn.net/article/details/106079007?spm=1001.2014.3001.5502 "一分钟体验零代码") | [在线体验零代码](https://app.qiaoqiaoyun.com/myapps/index "在线体验零代码")
- 开发文档: [文档中心](https://help.jeecg.com) | [AI集成配置(支持DeepSeek)](https://help.jeecg.com/java/ai/aichat.html)
- 反馈问题: [在Github上提Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
AIGC功能清单
在线体验
-----------------------------------
- AI对聊天助手
> JeecgBoot vs 敲敲云
> - JeecgBoot是低代码产品拥有很多低代码能力比如流程设计、表单设计、大屏设计代码生成器适合半开发模式开发+低代码结合),也可以集成零代码的应用管理模块;
> - 敲敲云是零代码产品完全不写代码通过配置搭建业务系统其在jeecgboot基础上研发而成删除了online、代码生成、OA等很多需要编码的功能只保留了应用管理和聊天、流程、日程、文件四个标准OA功能
- JeecgBoot低代码 https://boot3.jeecg.com
- 敲敲云零代码https://app.qiaoqiaoyun.com
- APP演示: http://jeecg.com/appIndex
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc) | [低代码初体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
AIGC应用平台介绍
-----------------------------------
JeecgBoot 平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
- [AIGC专题介绍页](README-AI.md)
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
##### AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
> - Markdown文档库导入
> JEECG AI允许用户直接导入整个Markdown文档库这不仅保留markdown格式还支持图片的导入确保文档内容的完整性和可视化效果。
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染。
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单建议表单设计器
- AI流程编排研发中
- AI知识库问答系统研发中
- AI应用开发平台研发中
- AI聊天窗口支持嵌入第三方研发中
- AI表单字段建议(表单设计器)
##### AI大模型支持
| AI大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地模型 | √ |
| 等等。。 | √ |
<b>关注公众号了解官方动态</b>
![公众号](https://jeecg.com/images/jeecg/qrcode_jeecgboot.jpg "在这里输入图片标题")
技术架构:
@ -96,15 +164,15 @@ AIGC功能清单
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 8+ (支持17)
- 语言Java 默认jdk17(支持jdk8、jdk21)
- 依赖管理Maven
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.9.3
- 安全框架Apache Shiro 1.12.0Jwt 3.11.0
- 报表工具: JimuReport 1.9.5
- 安全框架Apache Shiro 1.13.0Jwt 4.5.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.1.22
- 数据库连接池阿里巴巴Druid 1.1.24
- AI大模型支持 `ChatGPT` `DeepSeek`切换
- 日志打印logback
- 缓存Redis
@ -130,7 +198,9 @@ AIGC功能清单
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
#### 支持
#### 平台支持数据
> jeecgboot平台支持以下数据库默认我们只提供mysql脚本其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
| 数据库 | 支持 |
| --- | --- |
@ -139,20 +209,11 @@ AIGC功能清单
| Sqlserver2017 | √ |
| PostgreSQL | √ |
| MariaDB | √ |
| MariaDB | √ |
| 达梦 | √ |
| 人大金仓 | √ |
| TiDB | √ |
#### 支持AI大模型
| AI大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Ollama本地搭建大模型 | √ |
AI集成文档 https://help.jeecg.com/java/ai/aichat.html
| kingbase8 | √ |
## 微服务解决方案
@ -166,7 +227,7 @@ AI集成文档 https://help.jeecg.com/java/ai/aichat.html
- 6、分布式文件 Minio、阿里OSS √
- 7、统一权限控制 JWT + Shiro √
- 8、服务监控 SpringBootAdmin√
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
- 10、消息中间件 RabbitMQ √
- 11、分布式任务 xxl-job √
- 12、分布式事务 Seata
@ -178,8 +239,8 @@ AI集成文档 https://help.jeecg.com/java/ai/aichat.html
#### 微服务方式启动
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud.html)
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
#### 微服务架构图
@ -260,10 +321,10 @@ AI集成文档 https://help.jeecg.com/java/ai/aichat.html
│ ├─AI对话助手
│ ├─AI建表
│ ├─AI写文章
│ ├─AI流程编排(研发中)
│ ├─AI知识库问答系统(研发中)
│ ├─AI应用开发平台(研发中)
│ ├─AI聊天窗口支持嵌入第三方(研发中)
│ ├─AI流程编排
│ ├─AI知识库问答系统
│ ├─AI应用开发平台
│ ├─AI聊天窗口支持嵌入第三方
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.7.3发布日期2025-02-10
当前最新版本: 3.8.0发布日期2025-04-21
[![AUR](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg)](https://github.com/zhangdaiscott/jeecg-boot/blob/master/LICENSE)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](http://jeecg.com/aboutusIndex)
[![](https://img.shields.io/badge/version-3.7.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub stars](https://img.shields.io/github/stars/zhangdaiscott/jeecg-boot.svg?style=social&label=Stars)](https://github.com/zhangdaiscott/jeecg-boot)
[![GitHub forks](https://img.shields.io/github/forks/zhangdaiscott/jeecg-boot.svg?style=social&label=Fork)](https://github.com/zhangdaiscott/jeecg-boot)
@ -44,14 +44,14 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup.html)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick.html)
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
微服务启动
-----------------------------------
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer.html)
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud.html)
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
@ -66,10 +66,10 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.8.1
- 报表工具: JimuReport 1.9.4
- 安全框架Apache Shiro 1.12.0Jwt 3.11.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.1.22
- 数据库连接池阿里巴巴Druid 1.1.24
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
@ -113,7 +113,7 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
- 6、分布式文件 Minio、阿里OSS √
- 7、统一权限控制 JWT + Shiro √
- 8、服务监控 SpringBootAdmin√
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking.html)
- 9、链路跟踪 Skywalking [参考文档](https://help.jeecg.com/java/springcloud/super/skywarking)
- 10、消息中间件 RabbitMQ √
- 11、分布式任务 xxl-job √
- 12、分布式事务 Seata

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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');

View File

@ -3,7 +3,6 @@
> JeecgBoot属于平台级产品每次升级改动较大目前做不到平滑升级。
### 增量升级方案
#### 1.代码合并
本地通过svn或git做好主干在分支上做业务开发jeecg每次版本发布可以手工覆盖主干的代码对比合并代码
@ -12,12 +11,5 @@
- 其他库请手工执行SQL, 目录: `jeecg-module-system\jeecg-system-start\src\main\resources\flyway\sql\mysql`
> 注意: 升级sql只提供mysql版本如果有权限升级, 还需要手工角色授权,退出重新登录才好使。
#### 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.兼容问题
#### 3.兼容问题
每次发版,会针对不兼容地方重点说明。

View File

@ -4,15 +4,11 @@
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.7.3</version>
<version>3.8.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
<properties>
<spring-boot.version>3.1.5</spring-boot.version>
</properties>
<repositories>
<repository>
<id>aliyun</id>
@ -47,7 +43,7 @@
<!--jeecg-tools-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-common3</artifactId>
<artifactId>jeecg-boot-common</artifactId>
</dependency>
<!--集成springmvc框架并实现自动配置 -->
<dependency>
@ -119,14 +115,14 @@
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${dynamic-datasource-spring-boot-starter.version}</version>
</dependency>
@ -177,7 +173,6 @@
<artifactId>DmDialect-for-hibernate5.0</artifactId>
<version>${dm8.version}</version>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
@ -196,14 +191,7 @@
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- shiro-redis -->
<dependency>
@ -219,61 +207,13 @@
<artifactId>checkstyle</artifactId>
<groupId>com.puppycrawl.tools</groupId>
</exclusion>
<!-- TODO shiro 无法使用 spring boot 3.X 自带的jedis降版本处理 -->
<exclusion>
<artifactId>jedis</artifactId>
<groupId>redis.clients</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- TODO shiro 无法使用 spring boot 3.X 自带的jedis降版本处理 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</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>
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>
@ -283,11 +223,21 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>codegenerate</artifactId>
<version>${codegenerate.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-io</artifactId>
<groupId>commons-io</groupId>
</exclusion>
<exclusion>
<artifactId>mysql-connector-java</artifactId>
<groupId>mysql</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- AutoPoi Excel工具类-->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<groupId>org.jeecgframework</groupId>
<artifactId>autopoi-web</artifactId>
<version>${autopoi-web.version}</version>
<exclusions>
@ -312,6 +262,12 @@
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<exclusions>
<exclusion>
<artifactId>checker-qual</artifactId>
<groupId>org.checkerframework</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 阿里云短信 -->
@ -330,16 +286,6 @@
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<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>
<groupId>com.squareup.okhttp3</groupId>
@ -363,11 +309,15 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -2,7 +2,7 @@ package org.jeecg.common.api.dto;
import lombok.Data;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
/**

View File

@ -21,14 +21,13 @@ import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
@ -173,7 +172,7 @@ public class AutoLogAspect {
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) {

View File

@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;

View File

@ -0,0 +1,21 @@
package org.jeecg.common.exception;
/**
* jeecgboot断言异常
* for [QQYUN-10990]AIRAG
* @author chenrui
* @date 2025/2/14 14:31
*/
public class JeecgBootAssertException extends JeecgBootException {
private static final long serialVersionUID = 1L;
public JeecgBootAssertException(String message) {
super(message);
}
public JeecgBootAssertException(String message, int errCode) {
super(message, errCode);
}
}

View File

@ -1,8 +1,6 @@
package org.jeecg.common.exception;
import cn.hutool.core.util.ObjectUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.shiro.SecurityUtils;
@ -25,14 +23,19 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.connection.PoolException;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 异常处理器
@ -47,6 +50,13 @@ public class JeecgBootExceptionHandler {
@Resource
BaseCommonService baseCommonService;
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationExceptions(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e);
addSysLog(e);
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
}
/**
* 处理自定义异常
*/

View File

@ -23,9 +23,9 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

View File

@ -2,6 +2,7 @@ package org.jeecg.common.system.base.entity;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
@ -12,7 +13,6 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: Entity基类

View File

@ -5,7 +5,7 @@ import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.SpringContextUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;

View File

@ -6,18 +6,16 @@ import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Objects;
import java.util.stream.Collectors;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
@ -202,11 +200,13 @@ public class JwtUtil {
}
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 是否存在字符串标志
boolean multiStr = false;
boolean multiStr;
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
key = key.substring(1,key.length()-1);
multiStr = true;
}
} else {
multiStr = false;
}
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
@ -289,7 +289,15 @@ public class JwtUtil {
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull)
.map(orgCode -> "'" + orgCode + "'")
//update-begin---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.map(orgCode -> {
if (multiStr) {
return "'" + orgCode + "'";
} else {
return orgCode;
}
})
//update-end---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.collect(Collectors.joining(", "));
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}

View File

@ -0,0 +1,239 @@
package org.jeecg.common.util;
import org.jeecg.common.exception.JeecgBootAssertException;
/**
* 断言检查工具
* for for [QQYUN-10990]AIRAG
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public class AssertUtils {
/**
* 确保对象为空,如果不为空抛出异常
*
* @param msg
* @param obj
* @throws JeecgBootAssertException
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public static void assertEmpty(String msg, Object obj) {
if (oConvertUtils.isObjectNotEmpty(obj)) {
throw new JeecgBootAssertException(msg);
}
}
/**
* 确保对象不为空,如果为空抛出异常
*
* @param msg
* @param obj
* @throws JeecgBootAssertException
* @author chenrui
* @date 2017-06-22 10:05:56
*/
public static void assertNotEmpty(String msg, Object obj) {
if (oConvertUtils.isObjectEmpty(obj)) {
throw new JeecgBootAssertException(msg);
}
}
/**
* 验证对象是否相同
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertEquals(String message, Object expected,
Object actual) {
if (oConvertUtils.isEqual(expected, actual)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 验证不相同
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertNotEquals(String message, Object expected,
Object actual) {
if (oConvertUtils.isEqual(expected, actual)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否相等
*
* @param message
* @param expected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertSame(String message, Object expected,
Object actual) {
if (expected == actual) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 验证不相等
*
* @param message
* @param unexpected
* @param actual
* @author chenrui
* @date 2018/9/12 15:45
*/
public static void assertNotSame(String message, Object unexpected,
Object actual) {
if (unexpected == actual) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否为真
*
* @param message
* @param condition
*/
public static void assertTrue(String message, boolean condition) {
if (!condition) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证 condition是否为false
*
* @param message
* @param condition
*/
public static void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}
/**
* 验证是否存在
*
* @param message
* @param obj
* @param objs
* @param <T>
* @throws JeecgBootAssertException
* @author chenrui
* @date 2018/1/31 22:14
*/
public static <T> void assertIn(String message, T obj, T... objs) {
assertNotEmpty(message, obj);
assertNotEmpty(message, objs);
if (!oConvertUtils.isIn(obj, objs)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 验证是否不存在
*
* @param message
* @param obj
* @param objs
* @param <T>
* @throws JeecgBootAssertException
* @author chenrui
* @date 2018/1/31 22:14
*/
public static <T> void assertNotIn(String message, T obj, T... objs) {
assertNotEmpty(message, obj);
assertNotEmpty(message, objs);
if (oConvertUtils.isIn(obj, objs)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 确保src大于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertGt(String message, Number src, Number des) {
if (oConvertUtils.isGt(src, des)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 确保src大于等于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertGe(String message, Number src, Number des) {
if (oConvertUtils.isGe(src, des)) {
return;
}
throw new JeecgBootAssertException(message);
}
/**
* 确保src小于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertLt(String message, Number src, Number des) {
if (oConvertUtils.isGe(src, des)) {
throw new JeecgBootAssertException(message);
}
}
/**
* 确保src小于等于des
*
* @param message
* @param src
* @param des
* @author chenrui
* @date 2018/9/19 15:30
*/
public static void assertLe(String message, Number src, Number des) {
if (oConvertUtils.isGt(src, des)) {
throw new JeecgBootAssertException(message);
}
}
}

View File

@ -5,7 +5,7 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
/**
*

View File

@ -19,7 +19,7 @@ import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.io.ByteArrayInputStream;
import java.io.File;

View File

@ -1,10 +1,10 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jeecg.common.exception.JeecgBootException;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;

View File

@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.handler.IFillRuleHandler;
import org.jeecg.common.system.query.QueryGenerator;
import javax.servlet.http.HttpServletRequest;
/**

View File

@ -1,6 +1,6 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;

View File

@ -36,7 +36,6 @@ public class RestUtil {
}
return domain;
}
private static String getPath() {
if (path == null) {
path = SpringContextUtils.getApplicationContext().getEnvironment().getProperty("server.servlet.context-path");

View File

@ -1,7 +1,7 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.ServiceNameConstants;

View File

@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
@ -16,7 +17,13 @@ import java.util.regex.Pattern;
* @author zhoujf
*/
@Slf4j
public class SqlInjectionUtil {
public class SqlInjectionUtil {
/**
* sql注入黑名单数据库名
*/
public final static String XSS_STR_TABLE = "peformance_schema|information_schema";
/**
* 默认—sql注入关键词
*/
@ -167,7 +174,28 @@ public class SqlInjectionUtil {
}
return false;
}
/**
* 判断是否存在SQL注入关键词字符串
*
* @param keyword
* @return
*/
@SuppressWarnings("AlibabaUndefineMagicConstant")
private static boolean isExistSqlInjectTableKeyword(String sql, String keyword) {
// 需要匹配的sql注入关键词
String[] matchingTexts = new String[]{"`" + keyword, "(" + keyword, "(`" + keyword};
for (String matchingText : matchingTexts) {
String[] checkTexts = new String[]{" " + matchingText, "from" + matchingText};
for (String checkText : checkTexts) {
if (sql.contains(checkText)) {
return true;
}
}
}
return false;
}
/**
* sql注入过滤处理遇到注入关键字抛异常
*
@ -208,6 +236,14 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {
@ -244,6 +280,14 @@ public class SqlInjectionUtil {
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
String[] xssTableArr = XSS_STR_TABLE.split("\\|");
for (String xssTableStr : xssTableArr) {
if (isExistSqlInjectTableKeyword(value, xssTableStr)) {
log.error(SqlInjectionUtil.SQL_INJECTION_KEYWORD_TIP, xssTableStr);
log.error(SqlInjectionUtil.SQL_INJECTION_TIP_VARIABLE, value);
throw new JeecgSqlInjectionException(SqlInjectionUtil.SQL_INJECTION_TIP + value);
}
}
// 三、SQL注入检测存在绕过风险 (正则校验)
for (String regularOriginal : XSS_REGULAR_STR_ARRAY) {

View File

@ -11,7 +11,7 @@ import org.jeecg.common.exception.JeecgBoot401Exception;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
/**
* @Author scott

View File

@ -1,6 +1,6 @@
package org.jeecg.common.util.encryption;
import org.apache.shiro.lang.codec.Base64;
import org.apache.shiro.codec.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;

View File

@ -42,6 +42,7 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("pdf");
FILE_TYPE_WHITE_LIST.add("csv");
// FILE_TYPE_WHITE_LIST.add("xml");
FILE_TYPE_WHITE_LIST.add("md");
//音视频文件
FILE_TYPE_WHITE_LIST.add("mp4");
@ -65,6 +66,10 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("apk");
FILE_TYPE_WHITE_LIST.add("wgt");
//幻灯片文件后缀
FILE_TYPE_WHITE_LIST.add("ppt");
FILE_TYPE_WHITE_LIST.add("pptx");
//设置禁止文件的头部标记
FILE_TYPE_MAP.put("3c25402070616765206c", "jsp");
FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048", "php");

View File

@ -9,10 +9,11 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -463,7 +464,7 @@ public class oConvertUtils {
return false;
}
String[] childs = (String[]) childArray.toArray();
List<String> childs = childArray.toJavaList(String.class);
for (String v : childs) {
if (!isIn(v, all)) {
return false;
@ -1028,5 +1029,109 @@ public class oConvertUtils {
}
return result;
}
/**
* 判断对象是否为空 <br/>
* 支持各种类型的对象
* for for [QQYUN-10990]AIRAG
* @param obj
* @return
* @author chenrui
* @date 2025/2/13 18:34
*/
public static boolean isObjectEmpty(Object obj) {
if (null == obj) {
return true;
}
if (obj instanceof CharSequence) {
return isEmpty(obj);
} else if (obj instanceof Map) {
return ((Map<?, ?>) obj).isEmpty();
} else if (obj instanceof Iterable) {
return isObjectEmpty(((Iterable<?>) obj).iterator());
} else if (obj instanceof Iterator) {
return !((Iterator<?>) obj).hasNext();
} else if (isArray(obj)) {
return 0 == Array.getLength(obj);
}
return false;
}
/**
* iterator 是否为空
* for for [QQYUN-10990]AIRAG
* @param iterator Iterator对象
* @return 是否为空
*/
public static boolean isEmptyIterator(Iterator<?> iterator) {
return null == iterator || false == iterator.hasNext();
}
/**
* 判断对象是否不为空
* for for [QQYUN-10990]AIRAG
* @param object
* @return
* @author chenrui
* @date 2025/2/13 18:35
*/
public static boolean isObjectNotEmpty(Object object) {
return !isObjectEmpty(object);
}
/**
* 如果src大于des返回true
* for [QQYUN-10990]AIRAG
* @param src
* @param des
* @return
* @author: chenrui
* @date: 2018/9/19 15:30
*/
public static boolean isGt(Number src, Number des) {
if (null == src || null == des) {
throw new IllegalArgumentException("参数不能为空");
}
if (src.doubleValue() > des.doubleValue()) {
return true;
}
return false;
}
/**
* 如果src大于等于des返回true
* for [QQYUN-10990]AIRAG
* @param src
* @param des
* @return
* @author: chenrui
* @date: 2018/9/19 15:30
*/
public static boolean isGe(Number src, Number des) {
if (null == src || null == des) {
throw new IllegalArgumentException("参数不能为空");
}
if (src.doubleValue() < des.doubleValue()) {
return false;
}
return true;
}
/**
* 判断是否存在
* for [QQYUN-10990]AIRAG
* @param obj
* @param objs
* @param <T>
* @return
* @author chenrui
* @date 2020/9/12 15:50
*/
public static <T> boolean isIn(T obj, T... objs) {
return isIn(obj, objs);
}
}

View File

@ -3,6 +3,8 @@ package org.jeecg.common.util.security;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.jeecg.common.util.oConvertUtils;
import java.util.*;
import java.util.regex.Matcher;
@ -66,6 +68,8 @@ public abstract class AbstractQueryBlackListHandler {
if(flag == false){
return false;
}
Set<String> xssTableSet = new HashSet<>(Arrays.asList(SqlInjectionUtil.XSS_STR_TABLE.split("\\|")));
for (QueryTable table : list) {
String name = table.getName();
String fieldRule = ruleMap.get(name);
@ -81,6 +85,16 @@ public abstract class AbstractQueryBlackListHandler {
}
}
// 判断是否调用了黑名单数据库
String dbName = table.getDbName();
if (oConvertUtils.isNotEmpty(dbName)) {
dbName = dbName.toLowerCase().trim();
if (xssTableSet.contains(dbName)) {
flag = false;
log.warn("sql黑名单校验数据库【" + dbName + "】禁止查询");
break;
}
}
}
// 返回黑名单校验结果(不合法直接抛出异常)
@ -135,6 +149,8 @@ public abstract class AbstractQueryBlackListHandler {
* 查询的表的信息
*/
protected class QueryTable {
//数据库名
private String dbName;
//表名
private String name;
//表的别名
@ -158,6 +174,14 @@ public abstract class AbstractQueryBlackListHandler {
this.fields.add(field);
}
public String getDbName() {
return dbName;
}
public void setDbName(String dbName) {
this.dbName = dbName;
}
public String getName() {
return name;
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -1,255 +1,255 @@
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 解析所有表名和字段的类
*/
@Slf4j
public class JSqlParserAllTableManager {
private final String sql;
private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
/**
* 别名对应实际表名
*/
private final Map<String, String> tableAliasMap = new HashMap<>();
/**
* 解析后的sql
*/
private String parsedSql = null;
JSqlParserAllTableManager(String selectSql) {
this.sql = selectSql;
}
/**
* 开始解析
*
* @return
* @throws JSQLParserException
*/
public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
// 1. 创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2. 使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(this.sql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
SelectBody selectBody = selectStatement.getSelectBody();
this.parsedSql = selectBody.toString();
// 3. 解析select查询sql的信息
if (selectBody instanceof PlainSelect) {
PlainSelect plainSelect = (PlainSelect) selectBody;
// 4. 合并 fromItems
List<FromItem> fromItems = new ArrayList<>();
fromItems.add(plainSelect.getFromItem());
// 4.1 处理join的表
List<Join> joins = plainSelect.getJoins();
if (joins != null) {
joins.forEach(join -> fromItems.add(join.getRightItem()));
}
// 5. 处理 fromItems
for (FromItem fromItem : fromItems) {
// 5.1 通过表名的方式from
if (fromItem instanceof Table) {
this.addSqlInfoByTable((Table) fromItem);
}
// 5.2 通过子查询的方式from
else if (fromItem instanceof SubSelect) {
this.handleSubSelect((SubSelect) fromItem);
}
}
// 6. 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
// 6.1 查询的是全部字段
if (selectItem instanceof AllColumns) {
// 当 selectItem 为 AllColumns 时fromItem 必定为 Table
String tableName = plainSelect.getFromItem(Table.class).getName();
// 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
assert sqlInfo != null;
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
// 6.2 查询的是带表别名( u.* )的全部字段
else if (selectItem instanceof AllTableColumns) {
AllTableColumns allTableColumns = (AllTableColumns) selectItem;
String aliasName = allTableColumns.getTable().getName();
// 通过别名获取表名
String tableName = this.tableAliasMap.get(aliasName);
if (tableName == null) {
tableName = aliasName;
}
SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
if (sqlInfo != null) {
// 设置为查询全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
}
}
// 6.3 各种字段表达式处理
else if (selectItem instanceof SelectExpressionItem) {
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
this.handleExpression(expression, alias, plainSelect.getFromItem());
}
}
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
} else {
// 非 select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
return this.allTableMap;
}
/**
* 处理子查询
*
* @param subSelect
*/
private void handleSubSelect(SubSelect subSelect) {
try {
String subSelectSql = subSelect.getSelectBody().toString();
// 递归调用解析
Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
if (map != null) {
this.assignMap(map);
}
} catch (Exception e) {
log.error("解析子查询出错", e);
}
}
/**
* 处理查询字段表达式
*
* @param expression
*/
private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
Function functionExp = (Function) expression;
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expItem : expressions) {
this.handleExpression(expItem, null, fromItem);
}
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
this.handleSubSelect((SubSelect) expression);
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 处理字段
if (expression instanceof Column) {
Column column = (Column) expression;
// 查询字段名
String fieldName = column.getColumnName();
String aliasName = fieldName;
if (alias != null) {
aliasName = alias.getName();
}
String tableName;
if (column.getTable() != null) {
// 通过列的表名获取 sqlInfo
// 例如 user.name这里的 tableName 就是 user
tableName = column.getTable().getName();
// 有可能是别名,需要转换为真实表名
if (this.tableAliasMap.get(tableName) != null) {
tableName = this.tableAliasMap.get(tableName);
}
} else {
// 当column的table为空时说明是 fromItem 中的字段
tableName = ((Table) fromItem).getName();
}
SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
if ($sqlInfo != null) {
$sqlInfo.addSelectField(aliasName, fieldName);
} else {
log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
}
}
}
/**
* 根据表名添加sqlInfo
*
* @param table
*/
private void addSqlInfoByTable(Table table) {
String tableName = table.getName();
// 解析 aliasName
if (table.getAlias() != null) {
this.tableAliasMap.put(table.getAlias().getName(), tableName);
}
SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
sqlInfo.setFromTableName(table.getName());
this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
}
/**
* 合并map
*
* @param source
*/
private void assignMap(Map<String, SelectSqlInfo> source) {
for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
if (sqlInfo == null) {
this.allTableMap.put(entry.getKey(), entry.getValue());
} else {
// 合并
if (sqlInfo.getSelectFields() == null) {
sqlInfo.setSelectFields(entry.getValue().getSelectFields());
} else {
sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
}
if (sqlInfo.getRealSelectFields() == null) {
sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
} else {
sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
}
}
}
}
}
//package org.jeecg.common.util.sqlparse;
//
//import lombok.extern.slf4j.Slf4j;
//import net.sf.jsqlparser.JSQLParserException;
//import net.sf.jsqlparser.expression.*;
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
//import net.sf.jsqlparser.schema.Column;
//import net.sf.jsqlparser.schema.Table;
//import net.sf.jsqlparser.statement.Statement;
//import net.sf.jsqlparser.statement.select.*;
//import org.jeecg.common.exception.JeecgBootException;
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
//
//import java.io.StringReader;
//import java.util.ArrayList;
//import java.util.HashMap;
//import java.util.List;
//import java.util.Map;
//
///**
// * 解析所有表名和字段的类
// */
//@Slf4j
//public class JSqlParserAllTableManager {
//
// private final String sql;
// private final Map<String, SelectSqlInfo> allTableMap = new HashMap<>();
// /**
// * 别名对应实际表名
// */
// private final Map<String, String> tableAliasMap = new HashMap<>();
//
// /**
// * 解析后的sql
// */
// private String parsedSql = null;
//
// JSqlParserAllTableManager(String selectSql) {
// this.sql = selectSql;
// }
//
// /**
// * 开始解析
// *
// * @return
// * @throws JSQLParserException
// */
// public Map<String, SelectSqlInfo> parse() throws JSQLParserException {
// // 1. 创建解析器
// CCJSqlParserManager mgr = new CCJSqlParserManager();
// // 2. 使用解析器解析sql生成具有层次结构的java类
// Statement stmt = mgr.parse(new StringReader(this.sql));
// if (stmt instanceof Select) {
// Select selectStatement = (Select) stmt;
// SelectBody selectBody = selectStatement.getSelectBody();
// this.parsedSql = selectBody.toString();
// // 3. 解析select查询sql的信息
// if (selectBody instanceof PlainSelect) {
// PlainSelect plainSelect = (PlainSelect) selectBody;
// // 4. 合并 fromItems
// List<FromItem> fromItems = new ArrayList<>();
// fromItems.add(plainSelect.getFromItem());
// // 4.1 处理join的表
// List<Join> joins = plainSelect.getJoins();
// if (joins != null) {
// joins.forEach(join -> fromItems.add(join.getRightItem()));
// }
// // 5. 处理 fromItems
// for (FromItem fromItem : fromItems) {
// // 5.1 通过表名的方式from
// if (fromItem instanceof Table) {
// this.addSqlInfoByTable((Table) fromItem);
// }
// // 5.2 通过子查询的方式from
// else if (fromItem instanceof SubSelect) {
// this.handleSubSelect((SubSelect) fromItem);
// }
// }
// // 6. 解析 selectFields
// List<SelectItem> selectItems = plainSelect.getSelectItems();
// for (SelectItem selectItem : selectItems) {
// // 6.1 查询的是全部字段
// if (selectItem instanceof AllColumns) {
// // 当 selectItem 为 AllColumns 时fromItem 必定为 Table
// String tableName = plainSelect.getFromItem(Table.class).getName();
// // 此处必定不为空,因为在解析 fromItem 时,已经将表名添加到 allTableMap 中
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// assert sqlInfo != null;
// // 设置为查询全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// }
// // 6.2 查询的是带表别名( u.* )的全部字段
// else if (selectItem instanceof AllTableColumns) {
// AllTableColumns allTableColumns = (AllTableColumns) selectItem;
// String aliasName = allTableColumns.getTable().getName();
// // 通过别名获取表名
// String tableName = this.tableAliasMap.get(aliasName);
// if (tableName == null) {
// tableName = aliasName;
// }
// SelectSqlInfo sqlInfo = this.allTableMap.get(tableName);
// // 如果此处为空,则说明该字段是通过子查询获取的,所以可以不处理,只有实际表才需要处理
// if (sqlInfo != null) {
// // 设置为查询全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// }
// }
// // 6.3 各种字段表达式处理
// else if (selectItem instanceof SelectExpressionItem) {
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
// Expression expression = selectExpressionItem.getExpression();
// Alias alias = selectExpressionItem.getAlias();
// this.handleExpression(expression, alias, plainSelect.getFromItem());
// }
// }
// } else {
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
// }
// } else {
// // 非 select 查询sql不做处理
// throw new JeecgBootException("非 select 查询sql不做处理");
// }
// return this.allTableMap;
// }
//
// /**
// * 处理子查询
// *
// * @param subSelect
// */
// private void handleSubSelect(SubSelect subSelect) {
// try {
// String subSelectSql = subSelect.getSelectBody().toString();
// // 递归调用解析
// Map<String, SelectSqlInfo> map = JSqlParserUtils.parseAllSelectTable(subSelectSql);
// if (map != null) {
// this.assignMap(map);
// }
// } catch (Exception e) {
// log.error("解析子查询出错", e);
// }
// }
//
// /**
// * 处理查询字段表达式
// *
// * @param expression
// */
// private void handleExpression(Expression expression, Alias alias, FromItem fromItem) {
// // 处理函数式字段 CONCAT(name,'(',age,')')
// if (expression instanceof Function) {
// Function functionExp = (Function) expression;
// List<Expression> expressions = functionExp.getParameters().getExpressions();
// for (Expression expItem : expressions) {
// this.handleExpression(expItem, null, fromItem);
// }
// return;
// }
// // 处理字段上的子查询
// if (expression instanceof SubSelect) {
// this.handleSubSelect((SubSelect) expression);
// return;
// }
// // 不处理字面量
// if (expression instanceof StringValue ||
// expression instanceof NullValue ||
// expression instanceof LongValue ||
// expression instanceof DoubleValue ||
// expression instanceof HexValue ||
// expression instanceof DateValue ||
// expression instanceof TimestampValue ||
// expression instanceof TimeValue
// ) {
// return;
// }
//
// // 处理字段
// if (expression instanceof Column) {
// Column column = (Column) expression;
// // 查询字段名
// String fieldName = column.getColumnName();
// String aliasName = fieldName;
// if (alias != null) {
// aliasName = alias.getName();
// }
// String tableName;
// if (column.getTable() != null) {
// // 通过列的表名获取 sqlInfo
// // 例如 user.name这里的 tableName 就是 user
// tableName = column.getTable().getName();
// // 有可能是别名,需要转换为真实表名
// if (this.tableAliasMap.get(tableName) != null) {
// tableName = this.tableAliasMap.get(tableName);
// }
// } else {
// // 当column的table为空时说明是 fromItem 中的字段
// tableName = ((Table) fromItem).getName();
// }
// SelectSqlInfo $sqlInfo = this.allTableMap.get(tableName);
// if ($sqlInfo != null) {
// $sqlInfo.addSelectField(aliasName, fieldName);
// } else {
// log.warn("发生意外情况,未找到表名为 {} 的 SelectSqlInfo", tableName);
// }
// }
// }
//
// /**
// * 根据表名添加sqlInfo
// *
// * @param table
// */
// private void addSqlInfoByTable(Table table) {
// String tableName = table.getName();
// // 解析 aliasName
// if (table.getAlias() != null) {
// this.tableAliasMap.put(table.getAlias().getName(), tableName);
// }
// SelectSqlInfo sqlInfo = new SelectSqlInfo(this.parsedSql);
// sqlInfo.setFromTableName(table.getName());
// this.allTableMap.put(sqlInfo.getFromTableName(), sqlInfo);
// }
//
// /**
// * 合并map
// *
// * @param source
// */
// private void assignMap(Map<String, SelectSqlInfo> source) {
// for (Map.Entry<String, SelectSqlInfo> entry : source.entrySet()) {
// SelectSqlInfo sqlInfo = this.allTableMap.get(entry.getKey());
// if (sqlInfo == null) {
// this.allTableMap.put(entry.getKey(), entry.getValue());
// } else {
// // 合并
// if (sqlInfo.getSelectFields() == null) {
// sqlInfo.setSelectFields(entry.getValue().getSelectFields());
// } else {
// sqlInfo.getSelectFields().addAll(entry.getValue().getSelectFields());
// }
// if (sqlInfo.getRealSelectFields() == null) {
// sqlInfo.setRealSelectFields(entry.getValue().getRealSelectFields());
// } else {
// sqlInfo.getRealSelectFields().addAll(entry.getValue().getRealSelectFields());
// }
// }
// }
// }
//
//}

View File

@ -1,190 +1,190 @@
package org.jeecg.common.util.sqlparse;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
@Slf4j
public class JSqlParserUtils {
/**
* 解析 查询selectsql的信息
* 此方法会展开所有子查询到一个map里
* key只存真实的表名如果查询的没有真实的表名则会被忽略。
* value只存真实的字段名如果查询的没有真实的字段名则会被忽略。
* <p>
* 例如SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
* 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
*
* @param selectSql
* @return
*/
public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
return allTableManager.parse();
}
/**
* 解析 查询selectsql的信息子查询嵌套
*
* @param selectSql
* @return
*/
public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
if (oConvertUtils.isEmpty(selectSql)) {
return null;
}
// log.info("解析查询Sql{}", selectSql);
// 使用 JSqlParer 解析sql
// 1、创建解析器
CCJSqlParserManager mgr = new CCJSqlParserManager();
// 2、使用解析器解析sql生成具有层次结构的java类
Statement stmt = mgr.parse(new StringReader(selectSql));
if (stmt instanceof Select) {
Select selectStatement = (Select) stmt;
// 3、解析select查询sql的信息
return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
} else {
// 非 select 查询sql不做处理
throw new JeecgBootException("非 select 查询sql不做处理");
}
}
/**
* 解析 select 查询sql的信息
*
* @param selectBody
* @return
*/
private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
// 判断是否使用了union等操作
if (selectBody instanceof SetOperationList) {
// 如果使用了union等操作则只解析第一个查询
List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
}
// 简单的select查询
if (selectBody instanceof PlainSelect) {
SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
PlainSelect plainSelect = (PlainSelect) selectBody;
FromItem fromItem = plainSelect.getFromItem();
// 解析 aliasName
if (fromItem.getAlias() != null) {
sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
}
// 解析 表名
if (fromItem instanceof Table) {
// 通过表名的方式from
Table fromTable = (Table) fromItem;
sqlInfo.setFromTableName(fromTable.getName());
} else if (fromItem instanceof SubSelect) {
// 通过子查询的方式from
SubSelect fromSubSelect = (SubSelect) fromItem;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
sqlInfo.setFromSubSelect(subSqlInfo);
}
// 解析 selectFields
List<SelectItem> selectItems = plainSelect.getSelectItems();
for (SelectItem selectItem : selectItems) {
if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
// 全部字段
sqlInfo.setSelectAll(true);
sqlInfo.setSelectFields(null);
sqlInfo.setRealSelectFields(null);
break;
} else if (selectItem instanceof SelectExpressionItem) {
// 获取单个查询字段名
SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
Expression expression = selectExpressionItem.getExpression();
Alias alias = selectExpressionItem.getAlias();
JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
}
}
return sqlInfo;
} else {
log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
}
}
/**
* 处理查询字段表达式
*
* @param sqlInfo
* @param expression
* @param alias 是否有别名无传null
*/
private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
// 处理函数式字段 CONCAT(name,'(',age,')')
if (expression instanceof Function) {
JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
return;
}
// 处理字段上的子查询
if (expression instanceof SubSelect) {
SubSelect subSelect = (SubSelect) expression;
SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
// 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
return;
}
// 不处理字面量
if (expression instanceof StringValue ||
expression instanceof NullValue ||
expression instanceof LongValue ||
expression instanceof DoubleValue ||
expression instanceof HexValue ||
expression instanceof DateValue ||
expression instanceof TimestampValue ||
expression instanceof TimeValue
) {
return;
}
// 查询字段名
String selectField = expression.toString();
// 实际查询字段名
String realSelectField = selectField;
// 判断是否有别名
if (alias != null) {
selectField = alias.getName();
}
// 获取真实字段名
if (expression instanceof Column) {
Column column = (Column) expression;
realSelectField = column.getColumnName();
}
sqlInfo.addSelectField(selectField, realSelectField);
}
/**
* 处理函数式字段
*
* @param functionExp
* @param sqlInfo
*/
private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
List<Expression> expressions = functionExp.getParameters().getExpressions();
for (Expression expression : expressions) {
JSqlParserUtils.handleExpression(sqlInfo, expression, null);
}
}
}
//package org.jeecg.common.util.sqlparse;
//
//import lombok.extern.slf4j.Slf4j;
//import net.sf.jsqlparser.JSQLParserException;
//import net.sf.jsqlparser.expression.*;
//import net.sf.jsqlparser.parser.CCJSqlParserManager;
//import net.sf.jsqlparser.schema.Column;
//import net.sf.jsqlparser.schema.Table;
//import net.sf.jsqlparser.statement.Statement;
//import net.sf.jsqlparser.statement.select.*;
//import org.jeecg.common.exception.JeecgBootException;
//import org.jeecg.common.util.oConvertUtils;
//import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
//
//import java.io.StringReader;
//import java.util.List;
//import java.util.Map;
//
//@Slf4j
//public class JSqlParserUtils {
//
// /**
// * 解析 查询selectsql的信息
// * 此方法会展开所有子查询到一个map里
// * key只存真实的表名如果查询的没有真实的表名则会被忽略。
// * value只存真实的字段名如果查询的没有真实的字段名则会被忽略。
// * <p>
// * 例如SELECT a.*,d.age,(SELECT count(1) FROM sys_depart) AS count FROM (SELECT username AS foo, realname FROM sys_user) a, demo d
// * 解析后的结果为:{sys_user=[username, realname], demo=[age], sys_depart=[]}
// *
// * @param selectSql
// * @return
// */
// public static Map<String, SelectSqlInfo> parseAllSelectTable(String selectSql) throws JSQLParserException {
// if (oConvertUtils.isEmpty(selectSql)) {
// return null;
// }
// // log.info("解析查询Sql{}", selectSql);
// JSqlParserAllTableManager allTableManager = new JSqlParserAllTableManager(selectSql);
// return allTableManager.parse();
// }
//
// /**
// * 解析 查询selectsql的信息子查询嵌套
// *
// * @param selectSql
// * @return
// */
// public static SelectSqlInfo parseSelectSqlInfo(String selectSql) throws JSQLParserException {
// if (oConvertUtils.isEmpty(selectSql)) {
// return null;
// }
// // log.info("解析查询Sql{}", selectSql);
// // 使用 JSqlParer 解析sql
// // 1、创建解析器
// CCJSqlParserManager mgr = new CCJSqlParserManager();
// // 2、使用解析器解析sql生成具有层次结构的java类
// Statement stmt = mgr.parse(new StringReader(selectSql));
// if (stmt instanceof Select) {
// Select selectStatement = (Select) stmt;
// // 3、解析select查询sql的信息
// return JSqlParserUtils.parseBySelectBody(selectStatement.getSelectBody());
// } else {
// // 非 select 查询sql不做处理
// throw new JeecgBootException("非 select 查询sql不做处理");
// }
// }
//
// /**
// * 解析 select 查询sql的信息
// *
// * @param selectBody
// * @return
// */
// private static SelectSqlInfo parseBySelectBody(SelectBody selectBody) {
// // 判断是否使用了union等操作
// if (selectBody instanceof SetOperationList) {
// // 如果使用了union等操作则只解析第一个查询
// List<SelectBody> selectBodyList = ((SetOperationList) selectBody).getSelects();
// return JSqlParserUtils.parseBySelectBody(selectBodyList.get(0));
// }
// // 简单的select查询
// if (selectBody instanceof PlainSelect) {
// SelectSqlInfo sqlInfo = new SelectSqlInfo(selectBody);
// PlainSelect plainSelect = (PlainSelect) selectBody;
// FromItem fromItem = plainSelect.getFromItem();
// // 解析 aliasName
// if (fromItem.getAlias() != null) {
// sqlInfo.setFromTableAliasName(fromItem.getAlias().getName());
// }
// // 解析 表名
// if (fromItem instanceof Table) {
// // 通过表名的方式from
// Table fromTable = (Table) fromItem;
// sqlInfo.setFromTableName(fromTable.getName());
// } else if (fromItem instanceof SubSelect) {
// // 通过子查询的方式from
// SubSelect fromSubSelect = (SubSelect) fromItem;
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(fromSubSelect.getSelectBody());
// sqlInfo.setFromSubSelect(subSqlInfo);
// }
// // 解析 selectFields
// List<SelectItem> selectItems = plainSelect.getSelectItems();
// for (SelectItem selectItem : selectItems) {
// if (selectItem instanceof AllColumns || selectItem instanceof AllTableColumns) {
// // 全部字段
// sqlInfo.setSelectAll(true);
// sqlInfo.setSelectFields(null);
// sqlInfo.setRealSelectFields(null);
// break;
// } else if (selectItem instanceof SelectExpressionItem) {
// // 获取单个查询字段名
// SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem;
// Expression expression = selectExpressionItem.getExpression();
// Alias alias = selectExpressionItem.getAlias();
// JSqlParserUtils.handleExpression(sqlInfo, expression, alias);
// }
// }
// return sqlInfo;
// } else {
// log.warn("暂时尚未处理该类型的 SelectBody: {}", selectBody.getClass().getName());
// throw new JeecgBootException("暂时尚未处理该类型的 SelectBody");
// }
// }
//
// /**
// * 处理查询字段表达式
// *
// * @param sqlInfo
// * @param expression
// * @param alias 是否有别名无传null
// */
// private static void handleExpression(SelectSqlInfo sqlInfo, Expression expression, Alias alias) {
// // 处理函数式字段 CONCAT(name,'(',age,')')
// if (expression instanceof Function) {
// JSqlParserUtils.handleFunctionExpression((Function) expression, sqlInfo);
// return;
// }
// // 处理字段上的子查询
// if (expression instanceof SubSelect) {
// SubSelect subSelect = (SubSelect) expression;
// SelectSqlInfo subSqlInfo = JSqlParserUtils.parseBySelectBody(subSelect.getSelectBody());
// // 注:字段上的子查询,必须只查询一个字段,否则会报错,所以可以放心合并
// sqlInfo.getSelectFields().addAll(subSqlInfo.getSelectFields());
// sqlInfo.getRealSelectFields().addAll(subSqlInfo.getAllRealSelectFields());
// return;
// }
// // 不处理字面量
// if (expression instanceof StringValue ||
// expression instanceof NullValue ||
// expression instanceof LongValue ||
// expression instanceof DoubleValue ||
// expression instanceof HexValue ||
// expression instanceof DateValue ||
// expression instanceof TimestampValue ||
// expression instanceof TimeValue
// ) {
// return;
// }
//
// // 查询字段名
// String selectField = expression.toString();
// // 实际查询字段名
// String realSelectField = selectField;
// // 判断是否有别名
// if (alias != null) {
// selectField = alias.getName();
// }
// // 获取真实字段名
// if (expression instanceof Column) {
// Column column = (Column) expression;
// realSelectField = column.getColumnName();
// }
// sqlInfo.addSelectField(selectField, realSelectField);
// }
//
// /**
// * 处理函数式字段
// *
// * @param functionExp
// * @param sqlInfo
// */
// private static void handleFunctionExpression(Function functionExp, SelectSqlInfo sqlInfo) {
// List<Expression> expressions = functionExp.getParameters().getExpressions();
// for (Expression expression : expressions) {
// JSqlParserUtils.handleExpression(sqlInfo, expression, null);
// }
// }
//
//}

View File

@ -1,101 +1,101 @@
package org.jeecg.common.util.sqlparse.vo;
import lombok.Data;
import net.sf.jsqlparser.statement.select.SelectBody;
import java.util.HashSet;
import java.util.Set;
/**
* select 查询 sql 的信息
*/
@Data
public class SelectSqlInfo {
/**
* 查询的表名如果是子查询则此处为null
*/
private String fromTableName;
/**
* 表别名
*/
private String fromTableAliasName;
/**
* 通过子查询获取的表信息例如select name from (select * from user) u
* 如果不是子查询则为null
*/
private SelectSqlInfo fromSubSelect;
/**
* 查询的字段集合,如果是 * 则为null如果设了别名则为别名
*/
private Set<String> selectFields;
/**
* 真实的查询字段集合,如果是 * 则为null如果设了别名则为原始字段名
*/
private Set<String> realSelectFields;
/**
* 是否是查询所有字段
*/
private boolean selectAll;
/**
* 解析之后的 SQL (关键字都是大写)
*/
private final String parsedSql;
public SelectSqlInfo(String parsedSql) {
this.parsedSql = parsedSql;
}
public SelectSqlInfo(SelectBody selectBody) {
this.parsedSql = selectBody.toString();
}
public void addSelectField(String selectField, String realSelectField) {
if (this.selectFields == null) {
this.selectFields = new HashSet<>();
}
if (this.realSelectFields == null) {
this.realSelectFields = new HashSet<>();
}
this.selectFields.add(selectField);
this.realSelectFields.add(realSelectField);
}
/**
* 获取所有字段,包括子查询里的。
*
* @return
*/
public Set<String> getAllRealSelectFields() {
Set<String> fields = new HashSet<>();
// 递归获取所有字段,起个直观的方法名为:
this.recursiveGetAllFields(this, fields);
return fields;
}
/**
* 递归获取所有字段
*/
private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
fields.addAll(sqlInfo.getRealSelectFields());
}
if (sqlInfo.getFromSubSelect() != null) {
recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
}
}
@Override
public String toString() {
return "SelectSqlInfo{" +
"fromTableName='" + fromTableName + '\'' +
", fromSubSelect=" + fromSubSelect +
", aliasName='" + fromTableAliasName + '\'' +
", selectFields=" + selectFields +
", realSelectFields=" + realSelectFields +
", selectAll=" + selectAll +
"}";
}
}
//package org.jeecg.common.util.sqlparse.vo;
//
//import lombok.Data;
//import net.sf.jsqlparser.statement.select.SelectBody;
//
//import java.util.HashSet;
//import java.util.Set;
//
///**
// * select 查询 sql 的信息
// */
//@Data
//public class SelectSqlInfo {
//
// /**
// * 查询的表名如果是子查询则此处为null
// */
// private String fromTableName;
// /**
// * 表别名
// */
// private String fromTableAliasName;
// /**
// * 通过子查询获取的表信息例如select name from (select * from user) u
// * 如果不是子查询则为null
// */
// private SelectSqlInfo fromSubSelect;
// /**
// * 查询的字段集合,如果是 * 则为null如果设了别名则为别名
// */
// private Set<String> selectFields;
// /**
// * 真实的查询字段集合,如果是 * 则为null如果设了别名则为原始字段名
// */
// private Set<String> realSelectFields;
// /**
// * 是否是查询所有字段
// */
// private boolean selectAll;
//
// /**
// * 解析之后的 SQL (关键字都是大写)
// */
// private final String parsedSql;
//
// public SelectSqlInfo(String parsedSql) {
// this.parsedSql = parsedSql;
// }
//
// public SelectSqlInfo(SelectBody selectBody) {
// this.parsedSql = selectBody.toString();
// }
//
// public void addSelectField(String selectField, String realSelectField) {
// if (this.selectFields == null) {
// this.selectFields = new HashSet<>();
// }
// if (this.realSelectFields == null) {
// this.realSelectFields = new HashSet<>();
// }
// this.selectFields.add(selectField);
// this.realSelectFields.add(realSelectField);
// }
//
// /**
// * 获取所有字段,包括子查询里的。
// *
// * @return
// */
// public Set<String> getAllRealSelectFields() {
// Set<String> fields = new HashSet<>();
// // 递归获取所有字段,起个直观的方法名为:
// this.recursiveGetAllFields(this, fields);
// return fields;
// }
//
// /**
// * 递归获取所有字段
// */
// private void recursiveGetAllFields(SelectSqlInfo sqlInfo, Set<String> fields) {
// if (!sqlInfo.isSelectAll() && sqlInfo.getRealSelectFields() != null) {
// fields.addAll(sqlInfo.getRealSelectFields());
// }
// if (sqlInfo.getFromSubSelect() != null) {
// recursiveGetAllFields(sqlInfo.getFromSubSelect(), fields);
// }
// }
//
// @Override
// public String toString() {
// return "SelectSqlInfo{" +
// "fromTableName='" + fromTableName + '\'' +
// ", fromSubSelect=" + fromSubSelect +
// ", aliasName='" + fromTableAliasName + '\'' +
// ", selectFields=" + selectFields +
// ", realSelectFields=" + realSelectFields +
// ", selectAll=" + selectAll +
// "}";
// }
//
//}

View File

@ -3,7 +3,7 @@ package org.jeecg.config;
import java.util.ArrayList;
import java.util.List;
import jakarta.annotation.Resource;
import javax.annotation.Resource;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.system.vo.DictModel;

View File

@ -2,9 +2,7 @@ package org.jeecg.config;
import java.io.IOException;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
import jakarta.servlet.*;
import javax.servlet.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -13,6 +11,8 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
/**

View File

@ -12,7 +12,6 @@ import java.util.HashMap;
import java.util.Map;
/**
* @author eightmonth@qq.com
* 启动程序修改DruidWallConfig配置
* 允许SELECT语句的WHERE子句是一个永真条件
* @author eightmonth

View File

@ -17,6 +17,10 @@ public class JeecgBaseConfig {
* @TODO 降低使用成本加的默认值,实际以 yml配置 为准
*/
private String signatureSecret = "dd05f1c54d63749eda95f9fa6d49v442a";
/**
* 自定义后台资源前缀解决表单设计器无法通过前端nginx转发访问
*/
private String customResourcePrefixPath;
/**
* 需要加强校验的接口清单
*/
@ -68,6 +72,14 @@ public class JeecgBaseConfig {
*/
private BaiduApi baiduApi;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;
}
public void setCustomResourcePrefixPath(String customResourcePrefixPath) {
this.customResourcePrefixPath = customResourcePrefixPath;
}
public Elasticsearch getElasticsearch() {
return elasticsearch;
}

View File

@ -1,9 +1,8 @@
//package org.jeecg.config;
//
//
//import io.swagger.annotations.ApiOperation;
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
//import org.jeecg.common.constant.CommonConstant;
//import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
//import org.springframework.beans.BeansException;
//import org.springframework.beans.factory.config.BeanPostProcessor;
//import org.springframework.context.annotation.Bean;
@ -19,13 +18,15 @@
//import springfox.documentation.builders.ParameterBuilder;
//import springfox.documentation.builders.PathSelectors;
//import springfox.documentation.builders.RequestHandlerSelectors;
//import springfox.documentation.oas.annotations.EnableOpenApi;
//import springfox.documentation.schema.ModelRef;
//import springfox.documentation.service.*;
//import springfox.documentation.spi.DocumentationType;
//import springfox.documentation.spi.service.contexts.SecurityContext;
//import springfox.documentation.spring.web.plugins.Docket;
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
//import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
//
//import java.lang.reflect.Field;
//import java.util.ArrayList;
@ -37,7 +38,8 @@
// * @Author scott
// */
//@Configuration
//@EnableSwagger2WebMvc
//@EnableSwagger2 //开启 Swagger2
//@EnableKnife4j //开启 knife4j可以不写
//@Import(BeanValidatorPluginsConfiguration.class)
//public class Swagger2Config implements WebMvcConfigurer {
//
@ -95,14 +97,6 @@
// List<Parameter> pars = new ArrayList<>();
// tokenPar.name(CommonConstant.X_ACCESS_TOKEN).description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tokenPar.build());
// //update-begin-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
// if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL){
// ParameterBuilder tenantPar = new ParameterBuilder();
// tenantPar.name(CommonConstant.TENANT_ID).description("租户ID").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
// pars.add(tenantPar.build());
// }
// //update-end-author:liusq---date:2024-08-15--for: 开启多租户时全局参数增加租户id
//
// return pars;
// }
//
@ -157,7 +151,7 @@
//
// @Override
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// if (bean instanceof WebMvcRequestHandlerProvider) {
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
// }
// return bean;

View File

@ -66,7 +66,7 @@ public class Swagger3Config implements WebMvcConfigurer {
// 全局添加鉴权参数
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
log.info("path: {}", path);
//log.debug("path: {}", path);
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
excludedPath.equals(path) ||
@ -90,7 +90,7 @@ public class Swagger3Config implements WebMvcConfigurer {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.7.4")
.version("3.8.0")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description( "后台API接口")
.termsOfService("NO terms of service")

View File

@ -1,19 +0,0 @@
package org.jeecg.config;
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class UndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
});
}
}

View File

@ -11,20 +11,18 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import jakarta.annotation.Resource;
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.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -34,12 +32,14 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Spring Boot 2.0 解决跨域问题
@ -58,6 +58,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* meterRegistryPostProcessor
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
*/
@Autowired(required = false)
@Qualifier("meterRegistryPostProcessor")
private BeanPostProcessor meterRegistryPostProcessor;
/**
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
*/
@ -70,6 +78,8 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
.addResourceLocations("file:" + jeecgBaseConfig.getPath().getWebapp() + "//");
}
resourceHandlerRegistration.addResourceLocations(staticLocations.split(","));
// 设置缓存控制标头 Cache-Control有效期为30天
resourceHandlerRegistration.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
}
/**
@ -144,11 +154,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 解决metrics端点不显示jvm信息的问题(zyf)
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
*/
@Bean
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
}
// /**

View File

@ -3,8 +3,8 @@ package org.jeecg.config.filter;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.config.sign.util.BodyReaderHttpServletRequestWrapper;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**

View File

@ -7,9 +7,9 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**

View File

@ -6,20 +6,17 @@ import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.CommonAPI;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.firewall.interceptor.enums.LowCodeUrlsEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
@ -70,6 +67,9 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
Set<String> hasRoles = null;
if (loginUser == null) {
loginUser = commonAPI.getUserByName(JwtUtil.getUserNameByToken(SpringContextUtils.getHttpServletRequest()));
}
if (loginUser != null) {
//当前登录人拥有的角色
hasRoles = commonAPI.queryUserRolesById(loginUser.getId());
}

View File

@ -60,7 +60,18 @@ public class MybatisPlusSaasConfig {
TENANT_TABLE.add("sys_category");
TENANT_TABLE.add("sys_data_source");
TENANT_TABLE.add("sys_position");
//TENANT_TABLE.add("sys_announcement");
//b-2.仪表盘
TENANT_TABLE.add("onl_drag_page");
TENANT_TABLE.add("onl_drag_dataset_head");
TENANT_TABLE.add("jimu_report_data_source");
TENANT_TABLE.add("jimu_report");
TENANT_TABLE.add("jimu_dict");
//b-4.AIRAG
TENANT_TABLE.add("airag_app");
TENANT_TABLE.add("airag_flow");
TENANT_TABLE.add("airag_knowledge");
TENANT_TABLE.add("airag_knowledge_doc");
TENANT_TABLE.add("airag_model");
}
//2.示例测试

View File

@ -11,7 +11,7 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.config.mybatis.ThreadLocalDataHelper;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**

View File

@ -6,8 +6,8 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 动态数据源切换拦截器

View File

@ -32,9 +32,9 @@ import org.springframework.web.filter.DelegatingFilterProxy;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import jakarta.annotation.Resource;
import jakarta.servlet.Filter;
import jakarta.servlet.DispatcherType;
import javax.annotation.Resource;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import java.lang.reflect.Method;
import java.util.*;
@ -217,6 +217,10 @@ public class ShiroConfig {
//update-begin---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/test/ai/chat/send");
//update-end---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/airag/flow/run");
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
@ -316,7 +320,7 @@ public class ShiroConfig {
return sentinelManager;
}
// redis 单机支持,在集群为空,或者集群无机器时候使用 add by jzyadmin@163.com
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();

View File

@ -23,8 +23,8 @@ import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;
/**

View File

@ -12,7 +12,7 @@ import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.beans.factory.BeanInitializationException;
import jakarta.servlet.Filter;
import javax.servlet.Filter;
import java.util.Map;
/**

View File

@ -13,10 +13,10 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description: 鉴权登录拦截器

View File

@ -1,9 +1,9 @@
package org.jeecg.config.shiro.filters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import lombok.extern.slf4j.Slf4j;

View File

@ -91,6 +91,10 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
if (bases.length > 0) {
for (String base : bases) {
for (String uri : uris) {
// 如果uri包含路径占位符, 则需要将其替换为*
if (uri.matches(".*\\{.*}.*")) {
uri = uri.replaceAll("\\{.*?}", "*");
}
urls.add(prefix(base) + prefix(uri));
}
}

View File

@ -1,5 +1,7 @@
package org.jeecg.config.shiro.ignore;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.ArrayList;
import java.util.List;
@ -12,6 +14,7 @@ import java.util.List;
public class InMemoryIgnoreAuth {
private static final List<String> IGNORE_AUTH_LIST = new ArrayList<>();
private static PathMatcher MATCHER = new AntPathMatcher();
public InMemoryIgnoreAuth() {}
public static void set(List<String> list) {
@ -28,7 +31,7 @@ public class InMemoryIgnoreAuth {
public static boolean contains(String url) {
for (String ignoreAuth : IGNORE_AUTH_LIST) {
if (url.endsWith(ignoreAuth)) {
if(MATCHER.match(ignoreAuth,url)){
return true;
}
}

View File

@ -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.WebMvcConfigurer;
import jakarta.annotation.Resource;
import javax.annotation.Resource;
/**
* 签名 拦截器配置

View File

@ -4,8 +4,8 @@ package org.jeecg.config.sign.interceptor;
import java.io.PrintWriter;
import java.util.SortedMap;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;

View File

@ -1,10 +1,10 @@
package org.jeecg.config.sign.util;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

View File

@ -10,7 +10,7 @@ import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import jakarta.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;

View File

@ -14,8 +14,8 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**

View File

@ -1,2 +1,3 @@
springdoc.auto-tag-classes: false
springdoc.packages-to-scan: org.jeecg
springdoc.packages-to-scan: org.jeecg
springdoc.default-flat-param-object: true

View File

@ -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.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.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)"));
}
}

View File

@ -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.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.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)"));
}
}

View File

@ -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.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.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"));
}
}

View File

@ -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.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();
}
}

View File

@ -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.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();
}
}

View File

@ -1,17 +1,12 @@
package org.jeecg.test.sqlparse;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.IpUtils;
import org.jeecg.common.util.oConvertUtils;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author: scott
* @date: 2024年04月29日 16:48

View 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.0</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.boot</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>1.0.4</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>

View File

@ -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);
// }
//}

View File

@ -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";
}

View File

@ -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" +
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
}

View File

@ -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 javax.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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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> {
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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 javax.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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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 javax.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);
}
}

View File

@ -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 javax.servlet.http.HttpServletRequest;
import javax.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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More