Compare commits

..

296 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
45f905ddac Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-02-19 18:45:26 +08:00
55696efcaa jimureport、jimubi升级到最新版1.9.4,补充权限注解 2025-02-19 18:45:14 +08:00
3671b81f01 Merge pull request #7564 from EightMonth/master
keys替换scan问题 修复 #6918 #6876
2025-02-19 18:42:09 +08:00
74ad7e6f92 jimureport、jimubi升级到最新版1.9.4 2025-02-19 15:03:07 +08:00
49d55a79f4 当用户没有角色保存操作权限时候操作角色保存仅保存和保存并关闭按钮会一直转圈不可用 2025-02-19 10:24:00 +08:00
8395d106a2 【issues/7773】合计没有跟着左右滚动条滚动 2025-02-19 10:23:10 +08:00
d716111ded 生成代码的时候,选择原生表单(form) 就会出错 #7817 2025-02-19 10:00:45 +08:00
81b4cbbe35 修复问题:线上ai助手后端内容一次性返回,消息不显示 2025-02-13 20:00:15 +08:00
86cfc18fe8 优化AI图标 2025-02-12 20:10:21 +08:00
98ba72ae4b 关注公众号了解官方动态 2025-02-12 10:45:52 +08:00
29248561d6 修复第三方登录接口通过token获取用户信息漏洞 2025-02-11 11:37:55 +08:00
ad74b78134 JSelectUserByDept组件的params可选参数似乎不起作用 #7758 2025-02-11 11:32:10 +08:00
bd716417e8 【issues/7738】文件中带"success"导出报错 2025-02-11 11:30:10 +08:00
1acb773adf 完善OpenApi模块文档 2025-02-10 15:51:17 +08:00
5753f3d1d9 xxl_job的sql脚本缺少字段 2025-02-10 13:51:39 +08:00
46fb1cce91 3.7.3版本启动提示连接不上mongodb #7787 2025-02-10 13:43:42 +08:00
d0487e7a9d 语言优化 2025-02-10 11:40:00 +08:00
b8c096eee5 语言调整 2025-02-10 11:26:13 +08:00
c84f62e418 升级vite6,要求必须node20+ 2025-02-08 17:28:49 +08:00
c2107ee74a 【issues/7781】修复系统设置icon点击显示错误 2025-02-08 11:34:44 +08:00
48a9ba6253 兼容deepseek-r1模型,ai助手支持think标签 2025-02-08 10:12:02 +08:00
1a4abfc89c 关注公众号了解最新资讯 2025-02-08 10:00:08 +08:00
9550c15d56 关注公众号新版抢先看 2025-02-08 09:48:00 +08:00
229256738a v3.7.3 DeepSeek版本发布 2025-02-07 18:31:15 +08:00
781432c678 AIGC功能清单 2025-02-07 18:24:56 +08:00
a4fba72da5 3.7.3 DeepSeek版本发布 2025-02-07 17:46:33 +08:00
65f90ea84a 3.7.3版本发布 2025-02-07 17:27:43 +08:00
b8ef121fe7 升级版本号3.7.3 2025-02-07 16:52:19 +08:00
d6c6a1c01f 租户套餐管理优化体验 2025-02-07 16:38:02 +08:00
7257dfe5ba AutoPoi Excel表格导入有问题,还会报个错。 #7703--- 2025-02-07 16:34:51 +08:00
18166a3c69 官网演示版本中“我的部门”功能数据展示异常 #7658--- 2025-02-06 22:47:55 +08:00
54abb67303 数据权限,查看自己拥有部门的权限中存在问题 #7288--- 2025-02-06 22:34:24 +08:00
84f46c2142 加上AI助手两个字 2025-02-06 22:24:06 +08:00
63d60092dc 【issues/7709】当dataSource是响应式时,单元格编辑输入会自动关闭 --- 2025-02-06 22:21:09 +08:00
dd9ec47cdb 【issues/7549】Online 表单开发 页面属性 查询选择模糊查询 结果生成的代码是 JRangeNumber 而且页面中不显示:父子表--] 2025-02-06 19:21:26 +08:00
49ce9e0763 AI功能效果图 2025-02-05 19:19:38 +08:00
a7089b7b16 AI新功能安排研发中(AI应用开发平台) 2025-02-05 18:50:20 +08:00
53f8c0c076 支持DeepSeek-R1本地化 2025-01-28 09:03:42 +08:00
6ad8c3d824 issues格式 2025-01-25 10:00:59 +08:00
6947f47bd3 v3.7.2发布,提供免费大屏 2025-01-25 09:57:06 +08:00
973804189e Update issue templates 2025-01-25 09:51:58 +08:00
1942980154 AI低代码平台 2025-01-20 13:49:08 +08:00
64b852e1c6 AI 低代码 2025-01-20 13:47:34 +08:00
0277a968f2 AI低代码平台 2025-01-20 13:41:51 +08:00
79ca5ebf96 JeecgBoot AI低代码平台 2025-01-20 12:24:02 +08:00
84a7ce9b7a 【issues/7706】顶部栏导航内部路由也可以支持采用新浏览器tab打开 2025-01-16 15:00:24 +08:00
ab2469ff98 AI模型支持DeepSeek 2025-01-14 21:57:37 +08:00
8fc27bb261 AI默认切换成DeepSeek v3模型,提供更优的速度和质量 2025-01-14 21:51:39 +08:00
d9737c891f 【issues/7658】我的部门无部门列表数据时,点击查询或者重置能查出数据 --- 2025-01-13 20:25:19 +08:00
17dc1916aa 【issues/7664】JVxeTable的textarea组件@blur会重复发生 --- 2025-01-13 20:23:56 +08:00
a00e4684b5 【issues/7661】vxetable用户组件maxTagCount通过业务传递 2025-01-13 20:23:14 +08:00
a2a3ff6709 【issues/7631】JVxeTable组件的getValues回调函数参数修正 ---
【issues/7624】JVxeTable 单选删除不生效 ---
2025-01-13 20:22:13 +08:00
266f49e72f 【issues/7588】选择后自动刷新表格 2025-01-13 20:20:59 +08:00
6be0f94ddc 项目说明 2025-01-12 17:55:39 +08:00
7159df8d18 默认放开AI助手,方便用户看到 2025-01-12 17:45:47 +08:00
b4cd341abc JimuReport 1.9.3版本支持权限,将平台权限传递给积木报表 2025-01-12 17:42:08 +08:00
327f034640 JimuReport和JimuBI大屏,升级到1.9.3版本 2025-01-12 17:23:15 +08:00
00b1250901 升级积木报表到1.9.3版本,解决v1.9.2严重权限BUG 2025-01-09 11:12:48 +08:00
964e9b3851 升级积木报表到1.9.3版本,解决v1.9.2严重权限BUG 2025-01-09 10:38:50 +08:00
9c5e71476f 升级积木报表到1.9.2版本,支持文件数据集 2025-01-08 16:02:03 +08:00
80a54746b1 升级vite6后要求node20+,pnpm9+ 2025-01-08 11:39:34 +08:00
9861e1a867 【issues/7601】ant-design-vue@4.2.6后弹窗全屏底部有空隙 2025-01-06 20:14:27 +08:00
5b2ac82e94 Merge pull request #7691 from liaozhiyang/up-vite6
升级到vite6且升级其它包
2025-01-06 19:51:19 +08:00
c5909cae34 升级到vite6且升级其它包 2025-01-06 19:40:57 +08:00
b0880c373b 缺少修改手机号接口 2025-01-03 11:32:56 +08:00
2b0bedd263 完善openapi,移除过期sql 2024-12-27 14:23:01 +08:00
ac30ed67bc 修复flyway自动升级失败 2024-12-25 15:38:39 +08:00
ee382c0a5b 全流程跑通 2024-12-21 23:50:07 +08:00
f7f6d0ff88 openapi全量功能 2024-12-20 17:53:45 +08:00
00252f392f keys替换scan问题 修复 #6918 #6876 2024-12-10 16:24:31 +08:00
754 changed files with 39894 additions and 21832 deletions

1
.gitattributes vendored
View File

@ -3,3 +3,4 @@
*.ts linguist-language=vue
*.html linguist-language=vue
*.sql linguist-language=Java
*.ftl linguist-language=Java

25
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,25 @@
---
name: 提交 Bug
about: 请告诉我框架存在的问题,我会协助你解决此问题!
labels: bug
assignees: getActivity
---
##### 版本号:
##### 问题描述:
##### 错误截图:
#### 友情提示:
- 未按格式要求发帖、描述过于简单的,会被直接删掉;
- 描述问题请图文并茂,方便我们理解并快速定位问题;
- 如果使用的不是master请说明你使用的分支;

View File

@ -1,3 +1,12 @@
---
name: 提交建议
about: 请告诉我框架的不足之处,让我做得更好!
labels: help wanted
assignees: getActivity
---
##### 版本号:
@ -13,4 +22,4 @@
#### 友情提示:
- 未按格式要求发帖、描述过于简单的,会被直接删掉;
- 描述问题请图文并茂,方便我们理解并快速定位问题;
- 如果使用的不是master请说明你使用的分支;
- 如果使用的不是master请说明你使用的分支;

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

@ -4,15 +4,15 @@
JEECG BOOT Low Code Development Platform
JEECG BOOT AI Low Code Platform
===============
Current version: 3.7.2 (Release date: 2024-12-12)
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.2-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,9 +21,9 @@ Current version: 3.7.2 (Release date: 2024-12-12)
Project introduction
-----------------------------------
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
<h3 align="center">Java AI Low Code Platform</h3>
JeecgBoot is a `low code development platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
JeecgBoot is a `AI low code platform` based on code `generators`! Front and back end separation architecture SpringBoot2.x, SpringCloud, Ant Design&Vue, Mybatis plus, Shiro, JWT, support for microservices. The powerful code generator makes the front and back end of the code generation, low code development! JeecgBoot leads a new low-code development paradigm (OnlineCoding-> Code Generator -> Manual MERGE) that helps resolve 70% of the duplication in Java projects and makes development more business-focused. Not only can quickly improve efficiency, save research and development costs, but also do not lose flexibility!
JeecgBoot provides a series of low code modules to make Online development truly zero code: Online form development, online reports, report configuration capabilities, online chart design, large screen design, mobile configuration capabilities, form designer, online design flow, process automation configuration, plug-in capabilities (pluggable) and more!
@ -32,11 +32,12 @@ The purpose of JEECG is: simple functions are implemented by OnlineCoding config
JEECG Business process: Using workflow to implement and extend the task interface for developing and writing business logic, forms provides a variety of solutions: form designer, online configuration form, and coding form. At the same time, the separation design of process and form (loose coupling) is realized, and the flexible configuration of task nodes is supported, which not only ensures the confidentiality of the company's process, but also reduces the workload of developers.
AI Empowering Low-Code: Currently, JeecgBoot supports AI large models such as ChatGPT and DeepSeek. The latest version defaults to using DeepSeek, which offers faster speed and higher quality. It now provides features such as AI chat assistant, AI table creation, and AI report generation.
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
@ -48,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
-----------------------------------
@ -57,14 +63,14 @@ Download other source code
For the project
-----------------------------------
Jeecg-Boot low code development platform can be applied in the development of any J2EE project, especially for SAAS projects, enterprise information management system (MIS), internal office system (OA), enterprise resource planning system (ERP), customer relationship management system (CRM), etc. Its semi-intelligent manual Merge development method, Can significantly improve the development efficiency of more than 70%, greatly reduce the development cost.
Jeecg-Boot AI low code platform can be applied in the development of any J2EE project, especially for SAAS projects, enterprise information management system (MIS), internal office system (OA), enterprise resource planning system (ERP), customer relationship management system (CRM), etc. Its semi-intelligent manual Merge development method, Can significantly improve the development efficiency of more than 70%, greatly reduce the development cost.
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)
@ -73,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 [http://help.jeecg.com](http://help.jeecg.com)
- 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
@ -175,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)
@ -192,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.1
- 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
@ -216,9 +222,11 @@ Technical Architecture:
#### Front-end environment requirements
* `Node.js 、npm 、pnpm`
* pnpm `v9+` is now required.
* Node.js Version suggestion: `v20.15.0`
` ( Since Vite5 no longer supports EOL Node.js 14/16/17/19, Node.js 18/20 + is now required )`
` ( Since Vite6 Node.js 18/20 + is now required )`
#### Support library
| database | support |
@ -233,6 +241,23 @@ Technical Architecture:
| TiDB | √ |
#### AI Support
| AI Model | Supported |
| --- | --- |
| DeepSeek | √ |
| ChatGPT | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |
AI Config https://help.jeecg.com/java/ai/aichat
AI APP: https://help.jeecg.com/aigc
## Microservice solutions
- 1. Service registration and discovery Nacos √
@ -243,7 +268,7 @@ Technical Architecture:
- 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
@ -260,8 +285,8 @@ Technical Architecture:
![功能蓝图](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

215
README.md
View File

@ -1,13 +1,13 @@
JeecgBoot 低代码开发平台
JeecgBoot AI低代码平台
===============
当前最新版本: 3.7.2发布日期2024-12-12
当前最新版本: 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.2-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,53 +16,57 @@ JeecgBoot 低代码开发平台
项目介绍
-----------------------------------
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
<h3 align="center">Java AI Low Code Platform</h3>
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.xSpringCloudAnt Design Vue3Mybatis-plusShiroJWT支持微服务。强大的代码生成器让前后端代码一键生成实现低代码开发! JeecgBoot集成AI模型能力引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE) 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro强大的代码生成器让前后端代码一键生成无需写任何代码
成套AI大模型功能: AI模型管理、AI应用、知识库、AI流程编排、AI对话助手等
引领AI低代码开发模式: AIGC生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作,让开发更多关注业务,快速提高效率 节省成本,同时又不失灵活性!
JeecgBoot 提供了一系列`低代码模块`,实现在线开发`真正的零代码`Online表单开发、Online报表、报表配置能力、在线图表设计、仪表盘设计、大屏设计、移动配置能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔等等
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
### 视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
适用项目
-----------------------------------
Jeecg-Boot低代码开发平台可以应用在任何J2EE项目的开发中支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM其半智能手工Merge的开发方式可以显著提高开发效率70%以上,极大降低开发成本。
JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中支持信创国产化。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM其半智能手工Merge的开发方式可以显著提高开发效率70%以上,极大降低开发成本。
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
#### 项目说明
信创国产化
-----------------------------------
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
通过这些适配JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
项目说明
-----------------------------------
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite5+ts最新技术栈 |
| `jeecg-uniapp` | [配套APP框架](https://github.com/jeecgboot/jeecg-uniapp) 适配多个终端支持APP、小程序、H5 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
开源协议说明
-----------------------------------
JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0协议,详细见 https://github.com/jeecgboot/JeecgBoot#Apache-2.0-1-ov-file
开源协议中文释意如下:
- 1.JeecgBoot开源版本无任何限制在遵循本开源协议条款下允许商用使用不会造成侵权行为。
- 2.允许基于本平台软件开展业务系统开发。
- 3.在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件。
- 4.针对企业用户我们也提供“企业级版本”,详细见 https://jeecg.com/vip
技术文档
-----------------------------------
- 官方网站: [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](https://help.jeecg.com)
- 反馈问题: [在Github上提Issues](https://github.com/jeecgboot/JeecgBoot/issues/new)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
@ -70,8 +74,88 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
启动项目
-----------------------------------
- [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)
在线体验
-----------------------------------
> 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大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地模型 | √ |
| 等等。。 | √ |
技术架构:
@ -80,15 +164,16 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
#### 后端
- 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.1
- 安全框架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
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
@ -99,20 +184,23 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
#### 前端
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite5+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理node、npm、pnpm
#### 前端环境要求
* 本地环境安装 `Node.js 、npm 、pnpm`
* pnpm 要求`9+` 版本以上
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
` ( 因为Vite5 不再支持已 EOL 的 Node.js 14 / 16 / 17 / 19现在需要 Node.js 18 / 20+ )`
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
#### 支持
#### 平台支持数据
> jeecgboot平台支持以下数据库默认我们只提供mysql脚本其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
| 数据库 | 支持 |
| --- | --- |
@ -121,10 +209,11 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
| Sqlserver2017 | √ |
| PostgreSQL | √ |
| MariaDB | √ |
| MariaDB | √ |
| 达梦 | √ |
| 人大金仓 | √ |
| TiDB | √ |
| kingbase8 | √ |
## 微服务解决方案
@ -138,7 +227,7 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
- 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
@ -150,8 +239,8 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
#### 微服务方式启动
- [单体快速切换微服务](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)
#### 微服务架构图
@ -166,9 +255,9 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
* 3.开发效率高,采用代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用引入AI能力支持自动建表等功能
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
* 5.代码生成器非常智能在线业务建模、在线配置、所见即所得支持23种类控件一键生成前后端代码大幅度提升开发效率不再为重复工作发愁。
* 6.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表等功能。
* 6.低代码能力Online在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码
* 7.低代码能力Online在线报表无需编码通过在线配置方式实现数据报表可以快速抽取数据减轻开发压力实现人人皆可编码
* 8.低代码能力Online在线图表无需编码通过在线配置方式实现曲线图柱状图数据报表等支持自定义排版布局实现人人皆可编码
* 7.低代码能力Online在线报表、Online在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
* 11.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能
@ -227,12 +316,20 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
### 功能模块
```
├─AI开发
│ ├─支持AI大模型ChatGPT和DeepSeek
│ ├─AI对话助手
│ ├─AI建表
│ ├─AI写文章
│ ├─AI流程编排
│ ├─AI知识库问答系统
│ ├─AI应用开发平台
│ ├─AI聊天窗口支持嵌入第三方
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─AI助手
│ ├─系统编码规则
│ ├─系统校验规则
├─积木报表设计器
@ -367,6 +464,24 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
### 系统效果
##### AI功能
AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI建表
![](https://oscimg.oschina.net/oscnet/up-381423599f219a67def45dfd9a99df8ef3f.png)
![](https://oscimg.oschina.net/oscnet/up-1508c2b0708c365605f68893044ee11f20d.png)
AI写文章
![](https://oscimg.oschina.net/oscnet/up-e3ee5b1fe497308805aa5e324b72994af79.png)
##### PC端
![](https://oscimg.oschina.net/oscnet/up-000530d95df337b43089ac77e562494f454.png)
@ -385,8 +500,6 @@ JeecgBoot开源版本底层完全开源可以自主开发遵循Apache2.0
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### AI助手
![](https://oscimg.oschina.net/oscnet/up-7c6405641a40f56638999d52da0cb5b4343.png)
##### 仪表盘设计器

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.7.2发布日期2024-12-12
当前最新版本: 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.2-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

@ -5,115 +5,356 @@
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;
use `xxl_job`;
SET NAMES utf8mb4;
/*
Navicat Premium Data Transfer
CREATE TABLE `xxl_job_info` (
Source Server : mysql5.7
Source Server Type : MySQL
Source Server Version : 50738 (5.7.38)
Source Host : 127.0.0.1:3306
Source Schema : xxl_job
Target Server Type : MySQL
Target Server Version : 50738 (5.7.38)
File Encoding : 65001
Date: 10/02/2025 13:49:31
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for xxl_job_group
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_group`;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器AppName',
`title` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '执行器地址类型0=自动注册1=手动录入',
`address_list` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行器地址列表多地址逗号分隔',
`update_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of xxl_job_group
-- ----------------------------
INSERT INTO `xxl_job_group` VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2025-02-10 13:49:04');
INSERT INTO `xxl_job_group` VALUES (2, 'jeecg-demo', '测试Demo模块', 0, NULL, '2025-02-10 13:49:04');
INSERT INTO `xxl_job_group` VALUES (3, 'jeecg-system', '系统System模块', 0, NULL, '2025-02-10 13:49:04');
-- ----------------------------
-- Table structure for xxl_job_info
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_info`;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_cron` varchar(128) NOT NULL COMMENT '任务执行CRON',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态0-停止1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`job_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`add_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '调度配置值含义取决于调度类型',
`misfire_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT 0 COMMENT '任务执行超时时间单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数',
`glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码',
`glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime NULL DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '子任务ID多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '调度状态0-停止1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT 0 COMMENT '下次调度时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_log` (
-- ----------------------------
-- Records of xxl_job_info
-- ----------------------------
INSERT INTO `xxl_job_info` VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2024-08-21 22:30:30', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '', 1, 1729353600000, 1739203200000);
INSERT INTO `xxl_job_info` VALUES (2, 3, '测试jeecg xxljob', '2024-08-21 22:41:10', '2024-08-21 22:41:30', 'JEECG', '', 'CRON', '* * * * * ?', 'DO_NOTHING', 'FIRST', 'demoJob', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2024-08-21 22:41:10', '', 1, 1739166572000, 1739166573000);
-- ----------------------------
-- Table structure for xxl_job_lock
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_lock`;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of xxl_job_lock
-- ----------------------------
INSERT INTO `xxl_job_lock` VALUES ('schedule_lock');
-- ----------------------------
-- Table structure for xxl_job_log
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_log`;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`executor_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器地址本次执行的地址',
`executor_handler` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '执行器任务分片参数格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT 0 COMMENT '失败重试次数',
`trigger_time` datetime NULL DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`trigger_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '调度-日志',
`handle_time` datetime NULL DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态0-默认1-无需告警2-告警成功3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`handle_msg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '告警状态0-默认1-无需告警2-告警成功3-告警失败',
PRIMARY KEY (`id`) USING BTREE,
INDEX `I_trigger_time`(`trigger_time`) USING BTREE,
INDEX `I_handle_code`(`handle_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6761 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_log_report` (
-- ----------------------------
-- Records of xxl_job_log
-- ----------------------------
INSERT INTO `xxl_job_log` VALUES (6618, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:09', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式自动注册<br>执行器-地址列表null<br>路由策略第一个<br>阻塞处理策略单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6619, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:10', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6620, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:11', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6621, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:12', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6622, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:13', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6623, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:14', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6624, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:15', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6625, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:16', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6626, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:17', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6627, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:18', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6628, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:19', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6629, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:20', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6630, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:21', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6631, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:22', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6632, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:23', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6633, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:24', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6634, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:25', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6635, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:26', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6636, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:27', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6637, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:28', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6638, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:29', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6639, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:30', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6640, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:31', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6641, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:32', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6642, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:33', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6643, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:34', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6644, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:35', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6645, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:36', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6646, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:37', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6647, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:38', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6648, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:39', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6649, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:40', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6650, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:41', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6651, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:42', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6652, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:43', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6653, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:44', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6654, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:45', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6655, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:46', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6656, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:47', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6657, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:48', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6658, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:49', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6659, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:50', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6660, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:51', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6661, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:52', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6662, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:53', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6663, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:54', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6664, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:55', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6665, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:56', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6666, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:57', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6667, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:58', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6668, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:47:59', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6669, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:00', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6670, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:01', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6671, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:02', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6672, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:03', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6673, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:04', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6674, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:05', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6675, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:06', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6676, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:07', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6677, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:08', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6678, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:09', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6679, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:10', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6680, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:11', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6681, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:12', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6682, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:13', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6683, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:14', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6684, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:15', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6685, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:16', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6686, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:17', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6687, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:18', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6688, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:19', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6689, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:20', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6690, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:21', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6691, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:22', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6692, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:23', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6693, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:24', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6694, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:25', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6695, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:26', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6696, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:27', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6697, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:28', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6698, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:29', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6699, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:30', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6700, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:31', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6701, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:32', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6702, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:33', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6703, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:34', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6704, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:35', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6705, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:36', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6706, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:37', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6707, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:38', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6708, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:39', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6709, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:40', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6710, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:41', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6711, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:42', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6712, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:43', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6713, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:44', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6714, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:45', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6715, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:46', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6716, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:47', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6717, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:48', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6718, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:49', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6719, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:50', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6720, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:51', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6721, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:52', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6722, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:53', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6723, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:54', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6724, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:55', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6725, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:56', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6726, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:57', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6727, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:58', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6728, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:48:59', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6729, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:00', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6730, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:01', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6731, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:02', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6732, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:03', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6733, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:04', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6734, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:05', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6735, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:06', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6736, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:07', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6737, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:08', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6738, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:09', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6739, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:10', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6740, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:11', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6741, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:12', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6742, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:13', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6743, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:14', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6744, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:15', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6745, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:16', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6746, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:17', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6747, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:18', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6748, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:19', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6749, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:20', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6750, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:21', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6751, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:22', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6752, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:23', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6753, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:24', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 2);
INSERT INTO `xxl_job_log` VALUES (6754, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:25', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6755, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:26', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6756, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:27', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6757, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:28', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6758, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:29', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6759, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:30', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
INSERT INTO `xxl_job_log` VALUES (6760, 3, 2, NULL, 'demoJob', '', NULL, 0, '2025-02-10 13:49:31', 500, '任务触发类型Cron触发<br>调度机器192.168.1.11<br>执行器-注册方式:自动注册<br>执行器-地址列表null<br>路由策略:第一个<br>阻塞处理策略:单机串行<br>任务超时时间0<br>失败重试次数0<br><br><span style=\"color:#00c0ef;\" > >>>>>>>>>>>触发调度<<<<<<<<<<< </span><br>调度失败:执行器地址为空<br><br>', NULL, 0, NULL, 0);
-- ----------------------------
-- Table structure for xxl_job_log_report
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_log_report`;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`trigger_day` datetime NULL DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT 0 COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT 0 COMMENT '执行失败-日志数量',
`update_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `i_trigger_day`(`trigger_day`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_logglue` (
-- ----------------------------
-- Records of xxl_job_log_report
-- ----------------------------
INSERT INTO `xxl_job_log_report` VALUES (1, '2024-08-21 00:00:00', 70, 0, 5, NULL);
INSERT INTO `xxl_job_log_report` VALUES (2, '2024-08-20 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (3, '2024-08-19 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (4, '2024-09-10 00:00:00', 0, 0, 56, NULL);
INSERT INTO `xxl_job_log_report` VALUES (5, '2024-09-09 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (6, '2024-09-08 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (7, '2024-10-19 00:00:00', 0, 0, 6391, NULL);
INSERT INTO `xxl_job_log_report` VALUES (8, '2024-10-18 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (9, '2024-10-17 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (10, '2025-02-10 00:00:00', 0, 0, 116, NULL);
INSERT INTO `xxl_job_log_report` VALUES (11, '2025-02-09 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` VALUES (12, '2025-02-08 00:00:00', 0, 0, 0, NULL);
-- ----------------------------
-- Table structure for xxl_job_logglue
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_logglue`;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`glue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'GLUE源代码',
`glue_remark` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'GLUE备注',
`add_time` datetime NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_logglue
-- ----------------------------
CREATE TABLE `xxl_job_group` (
-- ----------------------------
-- Table structure for xxl_job_registry
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_registry`;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型0=自动注册1=手动录入',
`address_list` varchar(512) DEFAULT NULL COMMENT '执行器地址列表多地址逗号分隔',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`registry_group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`registry_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`registry_value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`update_time` datetime NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `i_g_k_v`(`registry_group`, `registry_key`, `registry_value`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_user` (
-- ----------------------------
-- Records of xxl_job_registry
-- ----------------------------
-- ----------------------------
-- Table structure for xxl_job_user
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_user`;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号',
`password` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限执行器ID列表多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限:执行器ID列表多个逗号分割',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `i_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_user
-- ----------------------------
INSERT INTO `xxl_job_user` VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL);
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_cron`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '0 0 0 * * ? *', '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;
SET FOREIGN_KEY_CHECKS = 1;

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,7 +4,7 @@
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.7.2</version>
<version>3.8.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -49,6 +49,16 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- websocket -->
<dependency>
@ -163,7 +173,7 @@
<artifactId>DmDialect-for-hibernate5.0</artifactId>
<version>${dm8.version}</version>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -201,14 +211,9 @@
</dependency>
<!-- knife4j -->
<!-- <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>
@ -218,6 +223,16 @@
<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工具类-->
@ -247,6 +262,12 @@
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<exclusions>
<exclusion>
<artifactId>checker-qual</artifactId>
<groupId>org.checkerframework</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 阿里云短信 -->
@ -288,11 +309,15 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,8 +1,7 @@
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;
@ -15,7 +14,7 @@ import java.io.Serializable;
* @date 2019年1月19日
*/
@Data
@ApiModel(value="接口返回对象", description="接口返回对象")
@Schema(description="接口返回对象")
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
@ -23,31 +22,31 @@ public class Result<T> implements Serializable {
/**
* 成功标志
*/
@ApiModelProperty(value = "成功标志")
@Schema(description = "成功标志")
private boolean success = true;
/**
* 返回处理消息
*/
@ApiModelProperty(value = "返回处理消息")
@Schema(description = "返回处理消息")
private String message = "";
/**
* 返回代码
*/
@ApiModelProperty(value = "返回代码")
@Schema(description = "返回代码")
private Integer code = 0;
/**
* 返回数据对象 data
*/
@ApiModelProperty(value = "返回数据对象")
@Schema(description = "返回数据对象")
private T result;
/**
* 时间戳
*/
@ApiModelProperty(value = "时间戳")
@Schema(description = "时间戳")
private long timestamp = System.currentTimeMillis();
public Result() {

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

@ -23,7 +23,9 @@ 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;
@ -33,6 +35,7 @@ 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

@ -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;
@ -9,7 +10,6 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -30,20 +30,20 @@ public class JeecgEntity implements Serializable {
* ID
*/
@TableId(type = IdType.ASSIGN_ID)
@ApiModelProperty(value = "ID")
@Schema(description = "ID")
private java.lang.String id;
/**
* 创建人
*/
@ApiModelProperty(value = "创建人")
@Schema(description = "创建人")
@Excel(name = "创建人", width = 15)
private java.lang.String createBy;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
@Schema(description = "创建时间")
@Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ -52,14 +52,14 @@ public class JeecgEntity implements Serializable {
/**
* 更新人
*/
@ApiModelProperty(value = "更新人")
@Schema(description = "更新人")
@Excel(name = "更新人", width = 15)
private java.lang.String updateBy;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
@Schema(description = "更新时间")
@Excel(name = "更新时间", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")

View File

@ -861,7 +861,9 @@ public class QueryGenerator {
return null;
}
Set<String> varParams = new HashSet<String>();
String regex = "\\#\\{\\w+\\}";
//update-begin---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
String regex = "#\\{\\[*\\w+]*}";
//update-end---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(sql);

View File

@ -6,11 +6,12 @@ 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 javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ -197,6 +198,16 @@ public class JwtUtil {
} else {
key = key;
}
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 是否存在字符串标志
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)) {
returnValue = DateUtils.formatDate();
@ -265,11 +276,30 @@ public class JwtUtil {
if(user==null){
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
returnValue = sysUser.getOrgCode();
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else{
if(user.isOneDepart()) {
returnValue = user.getSysMultiOrgCode().get(0);
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else {
returnValue = Joiner.on(",").join(user.getSysMultiOrgCode());
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull)
//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

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

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

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

@ -13,6 +13,7 @@ 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 = childArray.toArray(new String[]{});
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

@ -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,188 +1,182 @@
package org.jeecg.config;
import io.swagger.annotations.ApiOperation;
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;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
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.WebMvcRequestHandlerProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author scott
*/
@Configuration
@EnableSwagger2WebMvc
@Import(BeanValidatorPluginsConfiguration.class)
public class Swagger2Config implements WebMvcConfigurer {
/**
*
* 显示swagger-ui.html文档展示页还必须注入swagger资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* swagger2的配置文件这里可以配置swagger2的一些基本的内容比如扫描的包等等
*
* @return Docket
*/
@Bean(value = "defaultApi2")
public Docket defaultApi2() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//此包路径下的类,才生成接口文档
.apis(RequestHandlerSelectors.basePackage("org.jeecg"))
//加了ApiOperation注解的类,才生成接口文档
.apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()))
.securityContexts(securityContexts())
.globalOperationParameters(setHeaderToken());
}
/***
* oauth2配置
* 需要增加swagger授权回调地址
* http://localhost:8888/webjars/springfox-swagger-ui/o2c.html
* @return
*/
@Bean
SecurityScheme securityScheme() {
return new ApiKey(CommonConstant.X_ACCESS_TOKEN, CommonConstant.X_ACCESS_TOKEN, "header");
}
/**
* JWT token
* @return
*/
private List<Parameter> setHeaderToken() {
ParameterBuilder tokenPar = new ParameterBuilder();
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;
}
/**
* api文档的详细信息函数,注意这里的注解引用的是哪个
*
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
// //大标题
.title("JeecgBoot 后台服务API接口文档")
// 版本号
.version("1.0")
// .termsOfServiceUrl("NO terms of service")
// 描述
.description("后台API接口")
// 作者
.contact(new Contact("北京国炬信息技术有限公司","www.jeccg.com","jeecgos@163.com"))
.license("The Apache License, Version 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.build();
}
/**
* 新增 securityContexts 保持登录状态
*/
private List<SecurityContext> securityContexts() {
return new ArrayList(
Collections.singleton(SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build())
);
}
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
return new ArrayList(
Collections.singleton(new SecurityReference(CommonConstant.X_ACCESS_TOKEN, authorizationScopes)));
}
/**
* 解决springboot2.6 和springfox不兼容问题
* @return
*/
@Bean
public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebMvcRequestHandlerProvider) {
customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
}
return bean;
}
private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
List<T> copy = mappings.stream()
.filter(mapping -> mapping.getPatternParser() == null)
.collect(Collectors.toList());
mappings.clear();
mappings.addAll(copy);
}
@SuppressWarnings("unchecked")
private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
try {
Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
field.setAccessible(true);
return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
};
}
}
//package org.jeecg.config;
//
//
//import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
//import org.jeecg.common.constant.CommonConstant;
//import org.springframework.beans.BeansException;
//import org.springframework.beans.factory.config.BeanPostProcessor;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.Import;
//import org.springframework.util.ReflectionUtils;
//import org.springframework.web.bind.annotation.RestController;
//import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
//import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
//import springfox.documentation.builders.ApiInfoBuilder;
//import springfox.documentation.builders.ParameterBuilder;
//import springfox.documentation.builders.PathSelectors;
//import springfox.documentation.builders.RequestHandlerSelectors;
//import springfox.documentation.oas.annotations.EnableOpenApi;
//import springfox.documentation.schema.ModelRef;
//import springfox.documentation.service.*;
//import springfox.documentation.spi.DocumentationType;
//import springfox.documentation.spi.service.contexts.SecurityContext;
//import springfox.documentation.spring.web.plugins.Docket;
//import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
//import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
//import springfox.documentation.swagger2.annotations.EnableSwagger2;
//
//import java.lang.reflect.Field;
//import java.util.ArrayList;
//import java.util.Collections;
//import java.util.List;
//import java.util.stream.Collectors;
//
///**
// * @Author scott
// */
//@Configuration
//@EnableSwagger2 //开启 Swagger2
//@EnableKnife4j //开启 knife4j可以不写
//@Import(BeanValidatorPluginsConfiguration.class)
//public class Swagger2Config implements WebMvcConfigurer {
//
// /**
// *
// * 显示swagger-ui.html文档展示页还必须注入swagger资源
// *
// * @param registry
// */
// @Override
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
// registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
// registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
// registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
// }
//
// /**
// * swagger2的配置文件这里可以配置swagger2的一些基本的内容比如扫描的包等等
// *
// * @return Docket
// */
// @Bean(value = "defaultApi2")
// public Docket defaultApi2() {
// return new Docket(DocumentationType.SWAGGER_2)
// .apiInfo(apiInfo())
// .select()
// //此包路径下的类,才生成接口文档
// .apis(RequestHandlerSelectors.basePackage("org.jeecg"))
// //加了ApiOperation注解的类才生成接口文档
// .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
// .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// .paths(PathSelectors.any())
// .build()
// .securitySchemes(Collections.singletonList(securityScheme()))
// .securityContexts(securityContexts())
// .globalOperationParameters(setHeaderToken());
// }
//
// /***
// * oauth2配置
// * 需要增加swagger授权回调地址
// * http://localhost:8888/webjars/springfox-swagger-ui/o2c.html
// * @return
// */
// @Bean
// SecurityScheme securityScheme() {
// return new ApiKey(CommonConstant.X_ACCESS_TOKEN, CommonConstant.X_ACCESS_TOKEN, "header");
// }
// /**
// * JWT token
// * @return
// */
// private List<Parameter> setHeaderToken() {
// ParameterBuilder tokenPar = new ParameterBuilder();
// 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());
// return pars;
// }
//
// /**
// * api文档的详细信息函数,注意这里的注解引用的是哪个
// *
// * @return
// */
// private ApiInfo apiInfo() {
// return new ApiInfoBuilder()
// // //大标题
// .title("JeecgBoot 后台服务API接口文档")
// // 版本号
// .version("1.0")
//// .termsOfServiceUrl("NO terms of service")
// // 描述
// .description("后台API接口")
// // 作者
// .contact(new Contact("北京国炬信息技术有限公司","www.jeccg.com","jeecgos@163.com"))
// .license("The Apache License, Version 2.0")
// .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
// .build();
// }
//
// /**
// * 新增 securityContexts 保持登录状态
// */
// private List<SecurityContext> securityContexts() {
// return new ArrayList(
// Collections.singleton(SecurityContext.builder()
// .securityReferences(defaultAuth())
// .forPaths(PathSelectors.regex("^(?!auth).*$"))
// .build())
// );
// }
//
// private List<SecurityReference> defaultAuth() {
// AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
// AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
// authorizationScopes[0] = authorizationScope;
// return new ArrayList(
// Collections.singleton(new SecurityReference(CommonConstant.X_ACCESS_TOKEN, authorizationScopes)));
// }
//
// /**
// * 解决springboot2.6 和springfox不兼容问题
// * @return
// */
// @Bean
// public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
// return new BeanPostProcessor() {
//
// @Override
// public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
// customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
// }
// return bean;
// }
//
// private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
// List<T> copy = mappings.stream()
// .filter(mapping -> mapping.getPatternParser() == null)
// .collect(Collectors.toList());
// mappings.clear();
// mappings.addAll(copy);
// }
//
// @SuppressWarnings("unchecked")
// private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
// try {
// Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
// field.setAccessible(true);
// return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
// } catch (IllegalArgumentException | IllegalAccessException e) {
// throw new IllegalStateException(e);
// }
// }
// };
// }
//
//
//}

View File

@ -0,0 +1,102 @@
package org.jeecg.config;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author eightmonth
*/
@Slf4j
@Configuration
@PropertySource("classpath:config/default-spring-doc.properties")
public class Swagger3Config implements WebMvcConfigurer {
// 定义不需要注入安全要求的路径集合
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/{key}",
"/sys/login",
"/sys/phoneLogin",
"/sys/mLogin",
"/sys/sms",
"/sys/cas/client/validateLogin",
"/test/jeecgDemo/demo3",
"/sys/thirdLogin/**",
"/sys/user/register"
));
/**
*
* 显示swagger-ui.html文档展示页还必须注入swagger资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public GlobalOpenApiMethodFilter globalOpenApiMethodFilter() {
return method -> method.isAnnotationPresent(Operation.class);
}
@Bean
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
//log.debug("path: {}", path);
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
excludedPath.equals(path) ||
(excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
);
if (!isExcluded) {
// 接口添加鉴权参数
pathItem.readOperations()
.forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
);
}
});
}
};
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.8.0")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description( "后台API接口")
.termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
.components(new Components().addSecuritySchemes(CommonConstant.X_ACCESS_TOKEN,
new SecurityScheme().name(CommonConstant.X_ACCESS_TOKEN).type(SecurityScheme.Type.HTTP)));
}
}

View File

@ -11,15 +11,17 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
@ -37,6 +39,7 @@ 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 解决跨域问题
@ -55,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、图片、视频、音频等
*/
@ -67,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));
}
/**
@ -141,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

@ -6,15 +6,12 @@ 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 javax.annotation.Resource;
@ -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

@ -134,7 +134,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger**/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
// update-begin--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
@ -145,7 +145,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.js.map", "anon");
filterChainDefinitionMap.put("/**/*.css.map", "anon");
//拖拽仪表盘设计器排除
//积木BI大屏和仪表盘排除
filterChainDefinitionMap.put("/drag/view", "anon");
filterChainDefinitionMap.put("/drag/page/queryById", "anon");
filterChainDefinitionMap.put("/drag/page/addVisitsNumber", "anon");
@ -180,6 +180,8 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -215,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);
@ -314,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

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

@ -0,0 +1,3 @@
springdoc.auto-tag-classes: false
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;
}
}

View File

@ -0,0 +1,139 @@
package org.jeecg.modules.airag.llm.handler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Description: 命令行执行工具类
* @Author: chenrui
* @Date: 2024/4/8 10:11
*/
@Slf4j
public class CommandExecUtil {
/**
* 执行命令行
*
* @param command
* @param args
* @return
* @throws IOException
* @author chenrui
* @date 2024/4/9 10:59
*/
public static String execCommand(String command, String[] args) throws IOException {
if (null == command || command.isEmpty()) {
throw new IllegalArgumentException("命令不能为空");
}
return execCommand(command.split(" "), args);
}
/**
* 执行命令行
*
* @param command 脚本目录
* @param args 参数
* @author chenrui
* @date 2024/4/09 10:30
*/
public static String execCommand(String[] command, String[] args) throws IOException {
if (null == command || command.length == 0) {
throw new IllegalArgumentException("命令不能为空");
}
if (null != args && args.length > 0) {
command = (String[]) ArrayUtils.addAll(command, args);
}
// windows系统处理文件夹空格问题
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
List<String> commandNew = new ArrayList<>(command.length + 2);
commandNew.addAll(Arrays.asList("cmd.exe", "/c"));
for (String tempCommand : command) {
if (tempCommand.contains(" ")) {
tempCommand = "\"" + tempCommand.replaceAll("\"", "'") + "\"";
}
commandNew.add(tempCommand);
}
command = commandNew.toArray(new String[0]);
}
Process process = null;
try {
log.debug(" =============================== Runtime command Script ===============================" );
log.debug(String.join(" ", command));
log.debug(" =============================== Runtime command Script =============================== " );
process = Runtime.getRuntime().exec(command);
try (ByteArrayOutputStream resultOutStream = new ByteArrayOutputStream();
InputStream processInStream = new BufferedInputStream(process.getInputStream())) {
new Thread(new InputStreamRunnable(process.getErrorStream(), "ErrorStream")).start();
int num;
byte[] bs = new byte[1024];
while ((num = processInStream.read(bs)) != -1) {
resultOutStream.write(bs, 0, num);
String stepMsg = new String(bs);
// log.debug("命令行日志:" + stepMsg);
if (stepMsg.contains("input any key to continue...")) {
process.destroy();
}
}
String result = resultOutStream.toString();
log.debug("执行命令完成:" + result);
return result;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
throw e;
} finally {
if (process != null) {
process.destroy();
}
}
}
/**
* exec 控制台输出获取线程类
* 使用单独的线程获取控制台输出,防止输入流阻塞
*
* @author chenrui
* @date 2024/4/09 10:30
*/
static class InputStreamRunnable implements Runnable {
BufferedReader bReader = null;
String type = null;
public InputStreamRunnable(InputStream is, String _type) {
try {
bReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(is), StandardCharsets.UTF_8));
type = _type;
} catch (Exception ex) {
}
}
@SuppressWarnings("unused")
public void run() {
String line;
int lineNum = 0;
try {
while ((line = bReader.readLine()) != null) {
lineNum++;
// Thread.sleep(200);
}
bReader.close();
} catch (Exception ignored) {
}
}
}
}

View File

@ -0,0 +1,582 @@
package org.jeecg.modules.airag.llm.handler;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiTokenizer;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.rag.query.router.DefaultQueryRouter;
import dev.langchain4j.rag.query.router.QueryRouter;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.llm.config.EmbedStoreConfigBean;
import org.jeecg.modules.airag.llm.config.KnowConfigBean;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_FILE;
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_WEB;
/**
* 向量工具类
*
* @Author: chenrui
* @Date: 2025/2/18 14:31
*/
@Slf4j
@Component
public class EmbeddingHandler implements IEmbeddingHandler {
@Autowired
EmbedStoreConfigBean embedStoreConfigBean;
@Autowired
@Lazy
private IAiragModelService airagModelService;
@Autowired
@Lazy
private IAiragKnowledgeService airagKnowledgeService;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
@Autowired
KnowConfigBean knowConfigBean;
/**
* 默认分段长度
*/
private static final int DEFAULT_SEGMENT_SIZE = 1000;
/**
* 默认分段重叠长度
*/
private static final int DEFAULT_OVERLAP_SIZE = 50;
/**
* 向量存储元数据:knowledgeId
*/
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
/**
* 向量存储元数据:docId
*/
public static final String EMBED_STORE_METADATA_DOCID = "docId";
/**
* 向量存储元数据:docName
*/
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
/**
* 向量存储缓存
*/
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
/**
* 向量化文档
*
* @param knowId
* @param doc
* @return
* @author chenrui
* @date 2025/2/18 11:52
*/
public Map<String, Object> embeddingDocument(String knowId, AiragKnowledgeDoc doc) {
AiragKnowledge airagKnowledge = airagKnowledgeService.getById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
AssertUtils.assertNotEmpty("文档不能为空", doc);
// 读取文档
String content = doc.getContent();
// 向量化并存储
if (oConvertUtils.isEmpty(content)) {
switch (doc.getType()) {
case KNOWLEDGE_DOC_TYPE_FILE:
//解析文件
if (knowConfigBean.isEnableMinerU()) {
parseFileByMinerU(doc);
}
content = parseFile(doc);
break;
case KNOWLEDGE_DOC_TYPE_WEB:
// TODO author: chenrui for:读取网站内容 date:2025/2/18
break;
}
}
//update-begin---author:chenrui ---date:20250307 for[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里标题一般是有意义的------------
if (oConvertUtils.isNotEmpty(doc.getTitle())) {
content = doc.getTitle() + "\n\n" + content;
}
//update-end---author:chenrui ---date:20250307 for[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里标题一般是有意义的------------
// 向量化 date:2025/2/18
AiragModel model = getEmbedModelData(airagKnowledge.getEmbedId());
AiModelOptions modelOp = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
// 删除旧数据
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isEqualTo(doc.getId()));
// 分段器
DocumentSplitter splitter = DocumentSplitters.recursive(DEFAULT_SEGMENT_SIZE, DEFAULT_OVERLAP_SIZE, new OpenAiTokenizer());
// 分段并存储
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(splitter)
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()));
Document from = Document.from(content, metadata);
ingestor.ingest(from);
return metadata.toMap();
}
/**
* 向量查询(多知识库)
*
* @param knowIds
* @param queryText
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/2/18 16:52
*/
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
topNumber = oConvertUtils.getInteger(topNumber, 5);
//命中的文档列表
List<Map<String, Object>> documents = new ArrayList<>(16);
for (String knowId : knowIds) {
List<Map<String, Object>> searchResp = searchEmbedding(knowId, queryText, topNumber, similarity);
if (oConvertUtils.isObjectNotEmpty(searchResp)) {
documents.addAll(searchResp);
}
}
//命中的文档内容
StringBuilder data = new StringBuilder();
// 对documents按score降序排序并取前topNumber个
List<Map<String, Object>> sortedDocuments = documents.stream()
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
.limit(topNumber)
.peek(doc -> data.append(doc.get("content")).append("\n"))
.collect(Collectors.toList());
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
}
/**
* 向量查询
*
* @param knowId
* @param queryText
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/2/18 16:52
*/
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowId);
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
AiModelOptions modelOp = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
Embedding queryEmbedding = embeddingModel.embed(queryText).content();
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.build();
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(embeddingSearchRequest).matches();
List<Map<String, Object>> result = new ArrayList<>();
if (oConvertUtils.isObjectNotEmpty(relevant)) {
result = relevant.stream().map(matchRes -> {
Map<String, Object> data = new HashMap<>();
data.put("score", matchRes.score());
data.put("content", matchRes.embedded().text());
Metadata metadata = matchRes.embedded().metadata();
data.put("chunk", metadata.getInteger("index"));
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
return data;
}).collect(Collectors.toList());
}
return result;
}
/**
* 获取向量查询路由
*
* @param knowIds
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/2/20 21:03
*/
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
List<ContentRetriever> retrievers = Lists.newArrayList();
for (String knowId : knowIds) {
if (oConvertUtils.isEmpty(knowId)) {
continue;
}
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
AiModelOptions modelOptions = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOptions);
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
topNumber = oConvertUtils.getInteger(topNumber, 5);
similarity = oConvertUtils.getDou(similarity, 0.75);
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(topNumber)
.minScore(similarity)
.filter(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
.build();
retrievers.add(contentRetriever);
}
if (retrievers.isEmpty()) {
return null;
} else {
return new DefaultQueryRouter(retrievers);
}
}
/**
* 删除向量化文档
*
* @param knowId
* @param modelId
* @author chenrui
* @date 2025/2/18 19:07
*/
public void deleteEmbedDocsByKnowId(String knowId, String modelId) {
AssertUtils.assertNotEmpty("选择知识库", knowId);
AiragModel model = getEmbedModelData(modelId);
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
// 删除数据
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId));
}
/**
* 删除向量化文档
*
* @param docIds
* @param modelId
* @author chenrui
* @date 2025/2/18 19:07
*/
public void deleteEmbedDocsByDocIds(List<String> docIds, String modelId) {
AssertUtils.assertNotEmpty("选择文档", docIds);
AiragModel model = getEmbedModelData(modelId);
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
// 删除数据
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isIn(docIds));
}
/**
* 查询向量模型数据
*
* @param modelId
* @return
* @author chenrui
* @date 2025/2/20 20:08
*/
private AiragModel getEmbedModelData(String modelId) {
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
AiragModel model = airagModelService.getById(modelId);
AssertUtils.assertNotEmpty("向量模型不存在", model);
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
return model;
}
/**
* 获取向量存储
*
* @param model
* @return
* @author chenrui
* @date 2025/2/18 14:56
*/
private EmbeddingStore<TextSegment> getEmbedStore(AiragModel model) {
AssertUtils.assertNotEmpty("未配置模型", model);
String modelId = model.getId();
String connectionInfo = embedStoreConfigBean.getHost() + embedStoreConfigBean.getPort() + embedStoreConfigBean.getDatabase();
String key = modelId + connectionInfo;
if (EMBED_STORE_CACHE.containsKey(key)) {
return EMBED_STORE_CACHE.get(key);
}
AiModelOptions modelOp = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
// Connection and table parameters
.host(embedStoreConfigBean.getHost())
.port(embedStoreConfigBean.getPort())
.database(embedStoreConfigBean.getDatabase())
.user(embedStoreConfigBean.getUser())
.password(embedStoreConfigBean.getPassword())
.table(embedStoreConfigBean.getTable())
// Embedding dimension
// Required: Must match the embedding models output dimension
.dimension(embeddingModel.dimension())
// Indexing and performance options
// Enable IVFFlat index
.useIndex(true)
// Number of lists
// for IVFFlat index
.indexListSize(100)
// Table creation options
// Automatically create the table if it doesnt exist
.createTable(true)
//Dont drop the table first (set to true if you want a fresh start)
.dropTableFirst(false)
.build();
EMBED_STORE_CACHE.put(key, embeddingStore);
return embeddingStore;
}
/**
* 构造ModelOptions
*
* @param model
* @return
* @author chenrui
* @date 2025/3/11 17:45
*/
public static AiModelOptions buildModelOptions(AiragModel model) {
AiModelOptions.AiModelOptionsBuilder modelOpBuilder = AiModelOptions.builder()
.provider(model.getProvider())
.modelName(model.getModelName())
.baseUrl(model.getBaseUrl());
if (oConvertUtils.isObjectNotEmpty(model.getCredential())) {
JSONObject modelCredential = JSONObject.parseObject(model.getCredential());
modelOpBuilder.apiKey(oConvertUtils.getString(modelCredential.getString("apiKey"), null));
modelOpBuilder.secretKey(oConvertUtils.getString(modelCredential.getString("secretKey"), null));
}
modelOpBuilder.topNumber(5);
modelOpBuilder.similarity(0.75);
return modelOpBuilder.build();
}
/**
* 解析文件
*
* @param doc
* @author chenrui
* @date 2025/3/5 11:31
*/
private String parseFile(AiragKnowledgeDoc doc) {
String metadata = doc.getMetadata();
AssertUtils.assertNotEmpty("请先上传文件", metadata);
JSONObject metadataJson = JSONObject.parseObject(metadata);
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
throw new JeecgBootException("请先上传文件");
}
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
AssertUtils.assertNotEmpty("请先上传文件", filePath);
// 网络资源,先下载到临时目录
filePath = ensureFile(filePath);
// 提取文档内容
File docFile = new File(filePath);
if (docFile.exists()) {
Document document = new TikaDocumentParser(AutoDetectParser::new, null, null, null).parse(docFile);
if (null != document) {
String content = document.text();
// 判断是否md文档
String fileType = FilenameUtils.getExtension(docFile.getName());
if ("md".contains(fileType)) {
// 如果是md文件查找所有图片语法如果是本地图片替换成网络图片
String baseUrl = doc.getBaseUrl() + "/sys/common/static/";
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
if(oConvertUtils.isNotEmpty(sourcePath)) {
String escapedPath = uploadpath;
if (File.separator.equals("\\")){
escapedPath = uploadpath.replace("//", "\\\\");
}
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
baseUrl = baseUrl + sourcePath + "/";
StringBuffer sb = replaceImageUrl(content, baseUrl);
content = sb.toString();
}
}
return content;
}
}
return null;
}
@NotNull
private static StringBuffer replaceImageUrl(String content, String baseUrl) {
// 正则表达式匹配md文件中的图片语法 ![alt text](image url)
String mdImagePattern = "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)";
Pattern pattern = Pattern.compile(mdImagePattern);
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String imageUrl = matcher.group(2);
// 检查是否是本地图片路径
if (!imageUrl.startsWith("http")) {
// 替换成网络图片路径
String networkImageUrl = baseUrl + imageUrl;
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + networkImageUrl + ")");
} else {
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + imageUrl + ")");
}
}
matcher.appendTail(sb);
return sb;
}
/**
* 通过MinerU解析文件
*
* @param doc
* @author chenrui
* @date 2025/4/1 17:37
*/
private void parseFileByMinerU(AiragKnowledgeDoc doc) {
String metadata = doc.getMetadata();
AssertUtils.assertNotEmpty("请先上传文件", metadata);
JSONObject metadataJson = JSONObject.parseObject(metadata);
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
throw new JeecgBootException("请先上传文件");
}
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
AssertUtils.assertNotEmpty("请先上传文件", filePath);
filePath = ensureFile(filePath);
File docFile = new File(filePath);
String fileType = FilenameUtils.getExtension(filePath);
if (!docFile.exists()
|| "txt".equalsIgnoreCase(fileType)
|| "md".equalsIgnoreCase(fileType)) {
return ;
}
String command = "magic-pdf";
if (oConvertUtils.isNotEmpty(knowConfigBean.getCondaEnv())) {
command = "conda run -n " + knowConfigBean.getCondaEnv() + " " + command;
}
String outputPath = docFile.getParentFile().getAbsolutePath();
String[] args = {
"-p", docFile.getAbsolutePath(),
"-o", outputPath,
};
try {
String execLog = CommandExecUtil.execCommand(command, args);
log.info("执行命令行:" + command + " args:" + Arrays.toString(args) + "\n log::" + execLog);
// 如果成功,替换文件路径和静态资源路径
String fileBaseName = FilenameUtils.getBaseName(docFile.getName());
String newFileDir = outputPath + File.separator + fileBaseName + File.separator + "auto" + File.separator ;
// 先检查文件是否存在,存在才替换
File convertedFile = new File(newFileDir + fileBaseName + ".md");
if (convertedFile.exists()) {
log.info("文件转换成md成功,替换文件路径和静态资源路径");
newFileDir = newFileDir.replaceFirst("^" + uploadpath, "");
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH, newFileDir + fileBaseName + ".md");
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH, newFileDir);
doc.setMetadata(metadataJson.toJSONString());
}
} catch (IOException e) {
log.error("文件转换md失败,使用传统提取方案{}", e.getMessage(), e);
}
}
/**
* 确保文件存在
* @param filePath
* @return
* @author chenrui
* @date 2025/4/1 17:36
*/
@NotNull
private String ensureFile(String filePath) {
// 网络资源,先下载到临时目录
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(filePath);
if (matcher.matches()) {
log.info("网络资源,下载到临时目录:" + filePath);
// 准备文件
String tempFilePath = uploadpath + File.separator + "tmp" + File.separator + UUIDGenerator.generate() + File.separator;
String fileName = filePath;
if (fileName.contains("?")) {
fileName = fileName.substring(0, fileName.indexOf("?"));
}
fileName = FilenameUtils.getName(fileName);
tempFilePath = tempFilePath + fileName;
FileDownloadUtils.download2DiskFromNet(filePath, tempFilePath);
filePath = tempFilePath;
} else {
//本地文件
filePath = uploadpath + File.separator + filePath;
}
return filePath;
}
}

View File

@ -0,0 +1,23 @@
package org.jeecg.modules.airag.llm.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
/**
* @Description: airag知识库文档
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
public interface AiragKnowledgeDocMapper extends BaseMapper<AiragKnowledgeDoc> {
/**
* 通过主表id删除子表数据
*
* @param mainId 主表id
* @return boolean
*/
public boolean deleteByMainId(@Param("mainId") String mainId);
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.airag.llm.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
/**
* @Description: AIRag知识库
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
public interface AiragKnowledgeMapper extends BaseMapper<AiragKnowledge> {
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.airag.llm.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.llm.entity.AiragModel;
/**
* @Description: AiRag模型配置
* @Author: jeecg-boot
* @Date: 2025-02-14
* @Version: V1.0
*/
public interface AiragModelMapper extends BaseMapper<AiragModel> {
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.llm.mapper.AiragKnowledgeDocMapper">
<delete id="deleteByMainId" parameterType="java.lang.String">
DELETE
FROM airag_knowledge_doc
WHERE knowledge_id = #{mainId}
</delete>
</mapper>

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.llm.mapper.AiragKnowledgeMapper">
</mapper>

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.llm.mapper.AiragModelMapper">
</mapper>

View File

@ -0,0 +1,79 @@
package org.jeecg.modules.airag.llm.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* airag知识库文档
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
public interface IAiragKnowledgeDocService extends IService<AiragKnowledgeDoc> {
/**
* 重建文档
*
* @param docIds
* @return
* @author chenrui
* @date 2025/2/18 11:14
*/
Result<?> rebuildDocument(String docIds);
/**
* 添加文档
*
* @param airagKnowledgeDoc
* @return
* @author chenrui
* @date 2025/2/18 15:30
*/
Result<?> editDocument(AiragKnowledgeDoc airagKnowledgeDoc);
/**
* 通过知识库id重建文档
*
* @param knowId
* @return
* @author chenrui
* @date 2025/2/18 18:54
*/
Result<?> rebuildDocumentByKnowId(String knowId);
/**
* 通过知识库id删除文档
*
* @param knowIds
* @return
* @author chenrui
* @date 2025/2/18 18:59
*/
Result<?> removeByKnowIds(List<String> knowIds);
/**
* 通过文档id批量删除文档
*
* @param docIds
* @return
* @author chenrui
* @date 2025/2/18 19:16
*/
Result<?> removeDocByIds(List<String> docIds);
/**
* 从zip包导入文档
* @param knowId
* @param file
* @return
* @author chenrui
* @date 2025/3/20 13:50
*/
Result<?> importDocumentFromZip(String knowId, MultipartFile file);
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.airag.llm.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
/**
* AIRag知识库
*
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
}

View File

@ -0,0 +1,17 @@
package org.jeecg.modules.airag.llm.service;
import com.baomidou.mybatisplus.extension.service.IService;
import dev.langchain4j.data.message.ChatMessage;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import java.util.List;
/**
* @Description: AiRag模型配置
* @Author: jeecg-boot
* @Date: 2025-02-14
* @Version: V1.0
*/
public interface IAiragModelService extends IService<AiragModel> {
}

View File

@ -0,0 +1,355 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.config.mqtoken.UserTokenContext;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.*;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
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.mapper.AiragKnowledgeDocMapper;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static org.jeecg.modules.airag.llm.consts.LLMConsts.*;
/**
* @Description: airag知识库文档
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
@Slf4j
@Service
public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocMapper, AiragKnowledgeDoc> implements IAiragKnowledgeDocService {
@Autowired
private AiragKnowledgeDocMapper airagKnowledgeDocMapper;
@Autowired
private AiragKnowledgeMapper airagKnowledgeMapper;
@Autowired
EmbeddingHandler embeddingHandler;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
/**
* 支持的文档类型
*/
private static final List<String> SUPPORT_DOC_TYPE = Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md");
/**
* 向量化线程池大小
*/
private static final int THREAD_POOL_SIZE = 10;
/**
* 向量化文档线程池
*/
private static final ExecutorService buildDocExecutorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
@Transactional(rollbackFor = {Exception.class})
@Override
public Result<?> editDocument(AiragKnowledgeDoc airagKnowledgeDoc) {
AssertUtils.assertNotEmpty("文档不能未空", airagKnowledgeDoc);
AssertUtils.assertNotEmpty("知识库不能未空", airagKnowledgeDoc.getKnowledgeId());
AssertUtils.assertNotEmpty("文档标题不能未空", airagKnowledgeDoc.getTitle());
AssertUtils.assertNotEmpty("文档类型不能未空", airagKnowledgeDoc.getType());
if (KNOWLEDGE_DOC_TYPE_TEXT.equals(airagKnowledgeDoc.getType())) {
AssertUtils.assertNotEmpty("文档内容不能为空", airagKnowledgeDoc.getContent());
}
airagKnowledgeDoc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
// 保存到数据库
if (this.saveOrUpdate(airagKnowledgeDoc)) {
// 重建向量
return this.rebuildDocument(airagKnowledgeDoc.getId());
} else {
return Result.error("保存失败");
}
}
@Override
public Result<?> rebuildDocumentByKnowId(String knowId) {
AssertUtils.assertNotEmpty("知识库id不能为空", knowId);
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectList(Wrappers.lambdaQuery(AiragKnowledgeDoc.class).eq(AiragKnowledgeDoc::getKnowledgeId, knowId));
if (oConvertUtils.isObjectEmpty(docList)) {
return Result.OK();
}
String docIds = docList.stream().map(AiragKnowledgeDoc::getId).collect(Collectors.joining(","));
return rebuildDocument(docIds);
}
@Transactional(rollbackFor = {java.lang.Exception.class})
@Override
public Result<?> rebuildDocument(String docIds) {
AssertUtils.assertNotEmpty("请选择要重建的文档", docIds);
List<String> docIdList = Arrays.asList(docIds.split(","));
// 查询数据
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
AssertUtils.assertNotEmpty("文档不存在", docList);
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
String baseUrl = CommonUtils.getBaseUrl(request);
// 检查状态
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
.filter(doc -> {
//update-begin---author:chenrui ---date:20250410 for[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
if(KNOWLEDGE_DOC_STATUS_BUILDING.equalsIgnoreCase(doc.getStatus())){
Date updateTime = doc.getUpdateTime();
if (updateTime != null) {
// 向量化超过了5分钟,重新向量化
long timeDifference = System.currentTimeMillis() - updateTime.getTime();
return timeDifference > 5 * 60 * 1000;
}else{
return true;
}
} else {
return true;
}
//update-end---author:chenrui ---date:20250410 for[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
})
.peek(doc -> {
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
doc.setBaseUrl(baseUrl);
})
.collect(Collectors.toList());
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
return Result.ok("操作成功");
}
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
return Result.ok("操作成功");
}
// 更新状态
this.updateBatchById(knowledgeDocs);
// 异步重建文档
String tenantId = TenantContext.getTenant();
String token = TokenUtils.getTokenByRequest();
knowledgeDocs.forEach((doc) -> {
CompletableFuture.runAsync(() -> {
UserTokenContext.setToken(token);
TenantContext.setTenant(tenantId);
String knowId = doc.getKnowledgeId();
log.info("开始重建文档, 知识库id: {}, 文档id: {}", knowId, doc.getId());
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
this.updateById(doc);
//update-begin---author:chenrui ---date:20250410 for[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
try {
Map<String, Object> metadata = embeddingHandler.embeddingDocument(knowId, doc);
// 更新数据 date:2025/2/18
if (null != metadata) {
doc.setStatus(KNOWLEDGE_DOC_STATUS_COMPLETE);
this.updateById(doc);
log.info("重建文档成功, 知识库id: {}, 文档id: {}", knowId, doc.getId());
} else {
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
this.updateById(doc);
log.info("重建文档失败, 知识库id: {}, 文档id: {}", knowId, doc.getId());
}
}catch (Throwable t){
doc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
this.updateById(doc);
log.error("重建文档失败:" + t.getMessage() + ", 知识库id: " + knowId + ", 文档id: " + doc.getId(), t);
}
//update-end---author:chenrui ---date:20250410 for[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
}, buildDocExecutorService);
});
log.info("返回操作成功");
return Result.ok("操作成功");
}
@Override
public Result<?> removeByKnowIds(List<String> knowIds) {
AssertUtils.assertNotEmpty("选择知识库", knowIds);
for (String knowId : knowIds) {
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
// 删除数据
embeddingHandler.deleteEmbedDocsByKnowId(knowId, airagKnowledge.getEmbedId());
airagKnowledgeDocMapper.deleteByMainId(knowId);
}
return Result.OK();
}
@Override
public Result<?> removeDocByIds(List<String> docIds) {
AssertUtils.assertNotEmpty("请选择要删除的文档", docIds);
// 查询数据
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIds);
AssertUtils.assertNotEmpty("文档不存在", docList);
// 整理数据
Map<String, List<String>> knowledgeDocs = docList.stream().collect(Collectors.groupingBy(
AiragKnowledgeDoc::getKnowledgeId,
Collectors.mapping(AiragKnowledgeDoc::getId, Collectors.toList())
));
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
return Result.ok("success");
}
knowledgeDocs.forEach((knowId, groupedDocIds) -> {
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
// 删除数据
embeddingHandler.deleteEmbedDocsByDocIds(groupedDocIds, airagKnowledge.getEmbedId());
airagKnowledgeDocMapper.deleteBatchIds(groupedDocIds);
});
return Result.ok("success");
}
@Transactional(rollbackFor = {java.lang.Exception.class})
@Override
public Result<?> importDocumentFromZip(String knowId, MultipartFile zipFile) {
AssertUtils.assertNotEmpty("请先选择知识库", knowId);
AssertUtils.assertNotEmpty("请上传文件", zipFile);
long startTime = System.currentTimeMillis();
log.info("开始上传知识库文档(zip), 知识库id: {}, 文件名: {}", knowId, zipFile.getOriginalFilename());
try {
String bizPath = knowId + File.separator + UUIDGenerator.generate();
String workDir = uploadpath + File.separator + bizPath + File.separator;
String sourcesPath = workDir + "files";
SsrfFileTypeFilter.checkUploadFileType(zipFile);
// 通过filePath 检查文件是不是压缩包(zip)
String zipFileName = FilenameUtils.getBaseName(zipFile.getOriginalFilename());
String fileExt = FilenameUtils.getExtension(zipFile.getOriginalFilename());
if (null == fileExt || !fileExt.equalsIgnoreCase("zip")) {
throw new JeecgBootException("请上传zip压缩包");
}
String uploadedZipPath = CommonUtils.uploadLocal(zipFile, bizPath, uploadpath);
// 解压缩文件
List<AiragKnowledgeDoc> docList = new ArrayList<>();
AtomicInteger fileCount = new AtomicInteger(0);
unzipFile(uploadpath + File.separator + uploadedZipPath, sourcesPath, uploadedFile -> {
// 仅支持txt、pdf、docx、pptx、html、md文件
String fileName = uploadedFile.getName();
if (!SUPPORT_DOC_TYPE.contains(FilenameUtils.getExtension(fileName).toLowerCase())) {
log.warn("不支持的文件类型: {}", fileName);
return;
}
String baseName = FilenameUtils.getBaseName(fileName);
AiragKnowledgeDoc doc = new AiragKnowledgeDoc();
doc.setKnowledgeId(knowId);
doc.setTitle(baseName);
doc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_FILE);
doc.setStatus(LLMConsts.KNOWLEDGE_DOC_STATUS_DRAFT);
String relativePath;
if (File.separator.equals("\\")) {
// Windows path handling
String escapedPath = uploadpath.replace("//", "\\\\");
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
} else {
// Unix path handling
relativePath = uploadedFile.getPath().replaceFirst("^" + uploadpath, "");
}
JSONObject metadata = new JSONObject();
metadata.put(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH, relativePath);
metadata.put(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH, sourcesPath);
doc.setMetadata(metadata.toJSONString());
docList.add(doc);
});
// 保存数据
this.saveBatch(docList);
// 重建文档
String docIds = docList.stream().map(AiragKnowledgeDoc::getId).filter(oConvertUtils::isObjectNotEmpty).collect(Collectors.joining(","));
rebuildDocument(docIds);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
log.info("上传知识库文档(zip)成功, 知识库id: {}, 文件名: {}, 耗时: {}ms", knowId, zipFile.getOriginalFilename(), (System.currentTimeMillis() - startTime));
return Result.ok("上传成功");
}
/**
* 解压缩文件
*
* @param zipFilePath
* @param destDir
* @param afterExtract
* @throws IOException
* @author chenrui
* @date 2025/3/20 14:37
*/
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws
IOException {
// 创建目标目录
File dir = new File(destDir);
if (!dir.exists()) {
dir.mkdirs();
}
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
byte[] buffer = new byte[1024];
while (entries.hasMoreElements()) {
ZipEntry ze = entries.nextElement();
File newFile = new File(destDir, ze.getName());
// 预防 ZIP 路径穿越攻击
String canonicalDestDirPath = dir.getCanonicalPath();
String canonicalFilePath = newFile.getCanonicalPath();
if (!canonicalFilePath.startsWith(canonicalDestDirPath + File.separator)) {
throw new IOException("ZIP 路径穿越攻击被阻止: " + ze.getName());
}
if (ze.isDirectory()) {
newFile.mkdirs();
} else {
// 创建父目录
new File(newFile.getParent()).mkdirs();
// 读取 ZIP 文件并写入新文件
try (InputStream zis = zipFile.getInputStream(ze);
FileOutputStream fos = new FileOutputStream(newFile)) {
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}
if (afterExtract != null) {
afterExtract.accept(newFile);
}
}
}
}
}
}

View File

@ -0,0 +1,18 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.stereotype.Service;
/**
* @Description: AIRag知识库
* @Author: jeecg-boot
* @Date: 2025-02-18
* @Version: V1.0
*/
@Service
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
}

View File

@ -0,0 +1,19 @@
package org.jeecg.modules.airag.llm.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.springframework.stereotype.Service;
/**
* @Description: AiRag模型配置
* @Author: jeecg-boot
* @Date: 2025-02-14
* @Version: V1.0
*/
@Service
public class AiragModelServiceImpl extends ServiceImpl<AiragModelMapper, AiragModel> implements IAiragModelService {
}

View File

@ -0,0 +1,33 @@
package org.jeecg.modules.airag.llm.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 知识库查询返回结果
*
* @Author: chenrui
* @Date: 2025/2/18 17:53
*/
@Data
@NoArgsConstructor
public class KnowledgeSearchResult {
/**
* 命中的文档内容
*/
String data;
/**
* 命中的文档列表
*/
List<Map<String, Object>> documents;
public KnowledgeSearchResult(String data, List<Map<String, Object>> documents) {
this.data = data;
this.documents = documents;
}
}

View File

@ -0,0 +1,72 @@
server:
port: 7008
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
autoconfigure:
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
- org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
allow:
web-stat-filter:
enabled: true
dynamic:
druid: # 全局druid参数绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 1000
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters去掉后监控界面sql无法统计'wall'用于防火墙
# !!!!!mysql
# filters: stat,slf4j,wall
# !!!!!DM
filters: stat,slf4j
# 允许SELECT语句的WHERE子句是一个永真条件
# wall:
# selectWhereAlwayTrueCheck: false
# 打开mergeSql功能慢SQL记录
stat:
merge-sql: true
slow-sql-millis: 5000
datasource:
master:
## !!!!!MYSQL
url: jdbc:mysql://localhost:3306/jeecg-boot-dev?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
database: 0
host: 192.168.1.188
port: 6379
password: 'res983'
jeecg:
ai-rag:
embed-store:
host: "localhost"
port: 15432
database: "postgres"
user: "postgres"
password: "123456"
table: "embeddings"

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 -->
<property name="LOG_HOME" value="../logs" />
<!--<property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />-->
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
</encoder>
</appender>
<!-- 生成 error html格式日志开始 -->
<appender name="HTML" class="ch.qos.logback.core.FileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--设置日志级别,过滤掉info日志,只输入error日志-->
<level>ERROR</level>
</filter>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%p%d%msg%M%F{32}%L</pattern>
</layout>
</encoder>
<file>${LOG_HOME}/error-log.html</file>
</appender>
<!-- 生成 error html格式日志结束 -->
<!-- 每天生成一个html格式的日志开始 -->
<appender name="FILE_HTML" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${LOG_HOME}/jeecgboot-%d{yyyy-MM-dd}.%i.html</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>30</MaxHistory>
<MaxFileSize>10MB</MaxFileSize>
</rollingPolicy>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%p%d%msg%M%F{32}%L</pattern>
</layout>
</encoder>
</appender>
<!-- 每天生成一个html格式的日志结束 -->
<!--myibatis log configure -->
<logger name="com.apache.ibatis" level="TRACE" />
<logger name="java.sql.Connection" level="DEBUG" />
<logger name="java.sql.Statement" level="DEBUG" />
<logger name="java.sql.PreparedStatement" level="DEBUG" />
<logger name="logging.level.dev.langchain4j" level="DEBUG" />
<logger name="logging.level.dev.ai4j.openai4j" level="DEBUG" />
<!-- 日志输出级别 -->
<root level="info">
<appender-ref ref="STDOUT" />
<!-- <appender-ref ref="FILE" />-->
<!-- <appender-ref ref="HTML" />-->
<!-- <appender-ref ref="FILE_HTML" />-->
</root>
</configuration>

View File

@ -0,0 +1,82 @@
package org.jeecg.modules.airag.test;
import dev.langchain4j.data.document.Document;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.parser.AutoDetectParser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.wildfly.common.Assert;
import java.io.File;
import java.io.IOException;
/**
* @Description: 文件解析测试
* @Author: chenrui
* @Date: 2025/2/11 16:11
*/
@Slf4j
public class TestFileParse {
@Test
public void testParseTxt() {
readFile("test.txt");
}
@Test
public void testParsePdf() {
readFile("test.pdf");
}
@Test
public void testParseMd() {
readFile("test.md");
}
@Test
public void testParseDoc() {
readFile("test.docx");
}
@Test
public void testParseDoc2003() {
readFile("test.doc");
}
@Test
public void testParseExcel() {
readFile("test.xlsx");
}
@Test
public void testParseExcel2003() {
readFile("test.xls");
}
@Test
public void testParsePPT() {
readFile("test.pptx");
}
@Test
public void testParsePPT2003() {
readFile("test.ppt");
}
private static void readFile(String filePath) {
try {
ClassPathResource resource = new ClassPathResource(filePath);
File file = resource.getFile();
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
Document document = parser.parse(file);
Assert.assertNotNull(document);
System.out.println(filePath + "----" + document.text());
Assert.assertTrue(oConvertUtils.isNotEmpty(document));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,54 @@
package org.jeecg.modules.airag.test;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @Description: 流程测试
* @Author: chenrui
* @Date: 2025/2/11 16:11
*/
@Slf4j
public class TestFlows {
@Test
public void testRunFlow(){
String id = "1889499701976358913";
// String id = "1889571074002247682"; //switch
// String id = "1889608218175463425"; //脚本
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3Mzk1NDY0NDIsInVzZXJuYW1lIjoiamVlY2cifQ.CFIV79PUYmOAiqBKT3yjwihHWwf954DvS-4oKERmJVU";
String request = request(id,token);
System.out.println(request);
}
private String request(String id,String token) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("http://localhost:7008/airag/airagFlow/flow/run/" + id + "?field1=%25E5%2593%2588%25E5%2593%2588&field2=%25E4%25B8%25AD%25E5%259B%25BD")
.get()
.addHeader("X-Access-Token", token)
.addHeader("Accept", "*/*")
.addHeader("Accept-Encoding", "gzip, deflate, br")
.addHeader("User-Agent", "PostmanRuntime-ApipostRuntime/1.1.0")
.addHeader("Connection", "keep-alive")
.addHeader("Cookie", "JSESSIONID=442C48D3D1D0B2878A597AB6EBF2A07E")
.build();
try {
Response response = client.newCall(request).execute();
return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// TODO author: chenrui for:完善用例,使用java方式调用 date:2025/2/14
}

View File

@ -0,0 +1,177 @@
//package org.jeecg.modules.airag.test;
//
//import dev.langchain4j.data.message.*;
//import dev.langchain4j.model.chat.ChatLanguageModel;
//import dev.langchain4j.model.openai.OpenAiChatModel;
//import dev.langchain4j.model.output.Response;
//import dev.langchain4j.service.AiServices;
//import dev.langchain4j.service.TokenStream;
//import lombok.extern.slf4j.Slf4j;
//import org.jeecg.ai.assistant.AiChatAssistant;
//import org.jeecg.ai.factory.AiModelFactory;
//import org.jeecg.ai.factory.AiModelOptions;
//import org.jeecg.ai.handler.AIParams;
//import org.jeecg.ai.handler.LLMHandler;
//import org.jeecg.modules.airag.llm.handler.AIChatParams;
//import org.junit.Test;
//
//import java.io.IOException;
//import java.nio.file.Files;
//import java.nio.file.Paths;
//import java.util.ArrayList;
//import java.util.Base64;
//import java.util.Collections;
//import java.util.concurrent.ConcurrentHashMap;
//import java.util.concurrent.CountDownLatch;
//
///**
// * @Description: 流程测试
// * @Author: chenrui
// * @Date: 2025/2/11 16:11
// */
//@Slf4j
//public class TestLLM {
//
// @Test
// public void sendByModel() {
// String apiKey = "sk-xxx";
// String baseUrl = "https://api.v3.cm/v1";
// String modelName = "gpt-3.5-turbo";
// double temperature = 0.7;
// ChatLanguageModel llmModel = OpenAiChatModel.builder()
// .apiKey(apiKey)
// .baseUrl(baseUrl)
// .modelName(modelName)
// .temperature(temperature)
// .build();
// String hello = llmModel.generate("hello");
// System.out.println(hello);
// }
//
// @Test
// public void testChat() {
// AiModelOptions options = AiModelOptions.builder()
// .provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
// .apiKey("sk-xxx")
// .baseUrl("https://api.v3.cm/v1")
// .build();
// ChatLanguageModel chatModel = AiModelFactory.createChatModel(options);
// AiServices<AiChatAssistant> chatAssistantBuilder = AiServices.builder(AiChatAssistant.class);
// chatAssistantBuilder.chatLanguageModel(chatModel);
// AiChatAssistant chatAssistant = chatAssistantBuilder.build();
// String str = chatAssistant.chat("hello");
// System.out.println(str);
// }
//
// @Test
// public void sendImg() throws IOException {
// AiModelOptions options = AiModelOptions.builder()
// .provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
// .modelName("gpt-4o-mini")
// .apiKey("sk-xxx")
// .baseUrl("https://api.v3.cm/v1")
// .build();
// ChatLanguageModel chatModel = AiModelFactory.createChatModel(options);
//
// // 读取文件并转换为 base64 编码字符串
// byte[] fileContent = Files.readAllBytes(Paths.get("src/test/resources/test.jpg"));
// String base64Data = Base64.getEncoder().encodeToString(fileContent);
//
// // 获取文件的 MIME 类型
// String mimeType = Files.probeContentType(Paths.get("src/test/resources/test.jpg"));
//
// // 构建 ImageContent 对象
// ImageContent imageContent = new ImageContent(base64Data, mimeType);
//
// UserMessage userMessage = UserMessage.from(
// TextContent.from("你看到了什么"),
// // 构建 ImageContent 对象
// new ImageContent(base64Data, mimeType)
// );
// Response<AiMessage> generate = chatModel.generate(userMessage);
// System.out.println(generate.content());
// }
//
// @Test
// public void testSendImsWithLLMHandler() throws IOException {
// AiModelOptions options = AiModelOptions.builder()
// .provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
// .modelName("gpt-4o-mini")
// .apiKey("sk-xxx")
// .baseUrl("https://api.v3.cm/v1")
// .build();
// ChatLanguageModel chatModel = AiModelFactory.createChatModel(options);
//
// // 读取文件并转换为 base64 编码字符串
// byte[] fileContent = Files.readAllBytes(Paths.get("src/test/resources/test.jpg"));
// String base64Data = Base64.getEncoder().encodeToString(fileContent);
//
// // 获取文件的 MIME 类型
// String mimeType = Files.probeContentType(Paths.get("src/test/resources/test.jpg"));
//
// // 构建 ImageContent 对象
// ImageContent imageContent = new ImageContent(base64Data, mimeType);
//
// UserMessage userMessage = UserMessage.from(
// TextContent.from("你看到了什么"),
// // 构建 ImageContent 对象
// new ImageContent(base64Data, mimeType)
// );
// LLMHandler llmHandler = new LLMHandler();
// AIParams aiParams = new AIParams();
// aiParams.setProvider(AiModelFactory.AIMODEL_TYPE_OPENAI);
// aiParams.setModelName("gpt-4o-mini");
// aiParams.setApiKey("sk-xxx");
// aiParams.setBaseUrl("https://api.v3.cm/v1");
// String completions = llmHandler.completions(Collections.singletonList(userMessage), aiParams);
// System.out.println(completions);
// }
//
// @Test
// public void testChatImsWithLLMHandler() throws IOException, InterruptedException {
// AiModelOptions options = AiModelOptions.builder()
// .provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
// .modelName("gpt-4o-mini")
// .apiKey("sk-xxx")
// .baseUrl("https://api.v3.cm/v1")
// .build();
// ChatLanguageModel chatModel = AiModelFactory.createChatModel(options);
//
// // 读取文件并转换为 base64 编码字符串
// byte[] fileContent = Files.readAllBytes(Paths.get("src/test/resources/test.jpg"));
// String base64Data = Base64.getEncoder().encodeToString(fileContent);
//
// // 获取文件的 MIME 类型
// String mimeType = Files.probeContentType(Paths.get("src/test/resources/test.jpg"));
//
// // 构建 ImageContent 对象
// ImageContent imageContent = new ImageContent(base64Data, mimeType);
// UserMessage userMessage = UserMessage.from(
// TextContent.from("你看到了什么"),
// // 构建 ImageContent 对象
// imageContent,
// ImageContent.from("https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/logo-qqy_1741658353407.png")
// );
// LLMHandler llmHandler = new LLMHandler();
// AIParams aiParams = new AIParams();
// aiParams.setProvider(AiModelFactory.AIMODEL_TYPE_OPENAI);
// aiParams.setModelName("gpt-4o-mini");
// aiParams.setApiKey("sk-xxx");
// aiParams.setBaseUrl("https://api.v3.cm/v1");
// TokenStream chat = llmHandler.chat(Collections.singletonList(userMessage), aiParams);
// CountDownLatch countDownLatch = new CountDownLatch(1);
// chat.onNext(s -> System.out.println(s))
// .onComplete(s -> {
// System.out.println(s);
// countDownLatch.countDown();
// })
// .onError(e -> {
// System.out.println(e.getMessage());
// countDownLatch.countDown();
// }).start();
// countDownLatch.await();
//
// }
//
//
//}

View File

@ -0,0 +1,407 @@
//package org.jeecg.modules.airag.test;
//
//import dev.langchain4j.data.document.Document;
//import dev.langchain4j.data.document.DocumentSplitter;
//import dev.langchain4j.data.document.Metadata;
//import dev.langchain4j.data.document.splitter.DocumentSplitters;
//import dev.langchain4j.data.embedding.Embedding;
//import dev.langchain4j.data.segment.TextSegment;
//import dev.langchain4j.memory.ChatMemory;
//import dev.langchain4j.memory.chat.MessageWindowChatMemory;
//import dev.langchain4j.model.chat.ChatLanguageModel;
//import dev.langchain4j.model.embedding.EmbeddingModel;
//import dev.langchain4j.model.input.PromptTemplate;
//import dev.langchain4j.model.openai.OpenAiTokenizer;
//import dev.langchain4j.rag.DefaultRetrievalAugmentor;
//import dev.langchain4j.rag.RetrievalAugmentor;
//import dev.langchain4j.rag.content.injector.DefaultContentInjector;
//import dev.langchain4j.rag.content.retriever.ContentRetriever;
//import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
//import dev.langchain4j.rag.query.router.DefaultQueryRouter;
//import dev.langchain4j.rag.query.router.QueryRouter;
//import dev.langchain4j.service.AiServices;
//import dev.langchain4j.store.embedding.EmbeddingMatch;
//import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
//import dev.langchain4j.store.embedding.EmbeddingStore;
//import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
//import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
//import lombok.extern.slf4j.Slf4j;
//import org.jeecg.ai.assistant.AiChatAssistant;
//import org.jeecg.ai.factory.AiModelFactory;
//import org.jeecg.ai.factory.AiModelOptions;
//import org.junit.Before;
//import org.junit.Test;
//
//import java.util.List;
//
//import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
//
///**
// * @Description: 流程测试
// * @Author: chenrui
// * @Date: 2025/2/11 16:11
// */
//@Slf4j
//public class TestVector {
//
// String openAIBaseUrl = "https://api.v3.cm/v1";
// String openAIApiKey = "sk-xxx";
//
// EmbeddingModel embeddingModel;
// EmbeddingStore<TextSegment> embeddingStore;
//
// @Before
// public void before() {
// AiModelOptions.AiModelOptionsBuilder modelOpBuilder = AiModelOptions.builder().provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
// .baseUrl(openAIBaseUrl).apiKey(openAIApiKey);
// embeddingModel = AiModelFactory.createEmbeddingModel(modelOpBuilder.build());
// embeddingStore = PgVectorEmbeddingStore.builder()
// // Connection and table parameters
// .host("localhost")
// .port(15432)
// .database("postgres")
// .user("postgres")
// .password("123456")
// .table("test_embeddings")
// // Embedding dimension
// .dimension(embeddingModel.dimension()) // Required: Must match the embedding models output dimension
// // Indexing and performance options
// .useIndex(true) // Enable IVFFlat index
// .indexListSize(100) // Number of lists for IVFFlat index
// // Table creation options
// .createTable(true) // Automatically create the table if it doesnt exist
// .dropTableFirst(false) // Dont drop the table first (set to true if you want a fresh start)
// .build();
// }
//
// @Test
// public void testSave2Vector() {
// embeddingStore.removeAll(metadataKey("id").isEqualTo("sdfsdf"));
// DocumentSplitter splitter = DocumentSplitters.recursive(200,
// 50,
// new OpenAiTokenizer());
// EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// .documentSplitter(splitter)
// .embeddingModel(embeddingModel)
// .embeddingStore(embeddingStore)
// .build();
// Document from = Document.from(doc, Metadata.metadata("id", "sdfsdf"));
// ingestor.ingest(from);
// }
//
// @Test
// public void testQueryByVector() {
// Embedding queryEmbedding = embeddingModel.embed("全日制工作与非全日制工作有什么区别?").content();
// EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
// .queryEmbedding(queryEmbedding)
// .maxResults(5)
// .filter(metadataKey("id").isEqualTo("sdfsdf"))
// .build();
//
// List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(embeddingSearchRequest).matches();
// for (int i = 0; i < relevant.size(); i++) {
// EmbeddingMatch<TextSegment> embeddingMatch = relevant.get(i);
// System.out.println("结果:" + i + "=================================================");
// System.out.println("分数:" + embeddingMatch.score()); // 0.8144288608390052
// System.out.println("内容:" + embeddingMatch.embedded().text()); // I like football.
// }
// }
//
//
// @Test
// public void testQueryByRAG() {
// ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
// .embeddingStore(embeddingStore)
// .embeddingModel(embeddingModel)
// .maxResults(5)
// // maxResults can also be specified dynamically depending on the query
//// .dynamicMaxResults(query -> 3)
// .minScore(0.75)
// // minScore can also be specified dynamically depending on the query
//// .dynamicMinScore(query -> 0.75)
// .filter(metadataKey("id").isEqualTo("sdfsdf"))
// // filter can also be specified dynamically depending on the query
//// .dynamicFilter(query -> {
//// String userId = getUserId(query.metadata().chatMemoryId());
//// return metadataKey("userId").isEqualTo(userId);
//// })
// .build();
//
// ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
// ChatLanguageModel chatModel = AiModelFactory.createChatModel(AiModelOptions.builder()
// .baseUrl(openAIBaseUrl).apiKey(openAIApiKey).provider(AiModelFactory.AIMODEL_TYPE_OPENAI).build());
//
// QueryRouter queryRouter = new DefaultQueryRouter(contentRetriever);
// RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder().queryRouter(queryRouter)
// .contentInjector(DefaultContentInjector.builder()
// .promptTemplate(PromptTemplate.from("{{userMessage}}\n\n用以下信息回答问题:\n{{contents}}\n\n"))
// .build()).build();
// AiChatAssistant assistant = AiServices.builder(AiChatAssistant.class)
// .chatLanguageModel(chatModel)
// .chatMemory(chatMemory)
// .retrievalAugmentor(retrievalAugmentor)
// .build();
// String chat = assistant.chat("未签订劳动合同?");
// System.out.println(chat);
//
// }
//
//
// String doc = "中华人民共和国劳动合同法\n" +
// "已根据2013.07.01实施的修正案修改\n" +
// "目录\n" +
// "第一章 总则\n" +
// "第二章 劳动合同的订立\n" +
// "第三章 劳动合同的履行和变更\n" +
// "第四章 劳动合同的解除和终止\n" +
// "第五章 特别规定\n" +
// "第六章 监督检查\n" +
// "第七章 法律责任\n" +
// "第八章 附则\n" +
// "第一章 总则\n" +
// "第一条 为了完善劳动合同制度,明确劳动合同双方当事人的权利和义务,保护劳动者的合法权益,构建和发展和谐稳定的劳动关系,制定本法。\n" +
// "第二条 中华人民共和国境内的企业、个体经济组织、民办非企业单位等组织(以下称用人单位)与劳动者建立劳动关系,订立、履行、变更、解除或者终止劳动合同,适用本法。\n" +
// "国家机关、事业单位、社会团体和与其建立劳动关系的劳动者,订立、履行、变更、解除或者终止劳动合同,依照本法执行。\n" +
// "第三条 订立劳动合同,应当遵循合法、公平、平等自愿、协商一致、诚实信用的原则。\n" +
// "依法订立的劳动合同具有约束力,用人单位与劳动者应当履行劳动合同约定的义务。\n" +
// "第四条 用人单位应当依法建立和完善劳动规章制度,保障劳动者享有劳动权利、履行劳动义务。\n" +
// "用人单位在制定、修改或者决定有关劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利、职工培训、劳动纪律以及劳动定额管理等直接涉及劳动者切身利益的规章制度或者重大事项时,应当经职工代表大会或者全体职工讨论,提出方案和意见,与工会或者职工代表平等协商确定。\n" +
// "在规章制度和重大事项决定实施过程中,工会或者职工认为不适当的,有权向用人单位提出,通过协商予以修改完善。\n" +
// "用人单位应当将直接涉及劳动者切身利益的规章制度和重大事项决定公示,或者告知劳动者。\n" +
// "第五条 县级以上人民政府劳动行政部门会同工会和企业方面代表,建立健全协调劳动关系三方机制,共同研究解决有关劳动关系的重大问题。\n" +
// "第六条 工会应当帮助、指导劳动者与用人单位依法订立和履行劳动合同,并与用人单位建立集体协商机制,维护劳动者的合法权益。\n" +
// "第二章 劳动合同的订立\n" +
// "第七条 用人单位自用工之日起即与劳动者建立劳动关系。用人单位应当建立职工名册备查。\n" +
// "第八条 用人单位招用劳动者时,应当如实告知劳动者工作内容、工作条件、工作地点、职业危害、安全生产状况、劳动报酬,以及劳动者要求了解的其他情况;用人单位有权了解劳动者与劳动合同直接相关的基本情况,劳动者应当如实说明。\n" +
// "第九条 用人单位招用劳动者,不得扣押劳动者的居民身份证和其他证件,不得要求劳动者提供担保或者以其他名义向劳动者收取财物。\n" +
// "第十条 建立劳动关系,应当订立书面劳动合同。\n" +
// "已建立劳动关系,未同时订立书面劳动合同的,应当自用工之日起一个月内订立书面劳动合同。\n" +
// "用人单位与劳动者在用工前订立劳动合同的,劳动关系自用工之日起建立。\n" +
// "第十一条 用人单位未在用工的同时订立书面劳动合同,与劳动者约定的劳动报酬不明确的,新招用的劳动者的劳动报酬按照集体合同规定的标准执行;没有集体合同或者集体合同未规定的,实行同工同酬。\n" +
// "第十二条 劳动合同分为固定期限劳动合同、无固定期限劳动合同和以完成一定工作任务为期限的劳动合同。\n" +
// "第十三条 固定期限劳动合同,是指用人单位与劳动者约定合同终止时间的劳动合同。\n" +
// "用人单位与劳动者协商一致,可以订立固定期限劳动合同。\n" +
// "第十四条 无固定期限劳动合同,是指用人单位与劳动者约定无确定终止时间的劳动合同。\n" +
// "用人单位与劳动者协商一致,可以订立无固定期限劳动合同。有下列情形之一,劳动者提出或者同意续订、订立劳动合同的,除劳动者提出订立固定期限劳动合同外,应当订立无固定期限劳动合同:\n" +
// "(一)劳动者在该用人单位连续工作满十年的;\n" +
// "(二)用人单位初次实行劳动合同制度或者国有企业改制重新订立劳动合同时,劳动者在该用人单位连续工作满十年且距法定退休年龄不足十年的;\n" +
// "(三)连续订立二次固定期限劳动合同,且劳动者没有本法第三十九条和第四十条第一项、第二项规定的情形,续订劳动合同的。\n" +
// "用人单位自用工之日起满一年不与劳动者订立书面劳动合同的,视为用人单位与劳动者已订立无固定期限劳动合同。\n" +
// "第十五条 以完成一定工作任务为期限的劳动合同,是指用人单位与劳动者约定以某项工作的完成为合同期限的劳动合同。\n" +
// "用人单位与劳动者协商一致,可以订立以完成一定工作任务为期限的劳动合同。\n" +
// "第十六条 劳动合同由用人单位与劳动者协商一致,并经用人单位与劳动者在劳动合同文本上签字或者盖章生效。\n" +
// "劳动合同文本由用人单位和劳动者各执一份。\n" +
// "第十七条 劳动合同应当具备以下条款:\n" +
// "(一)用人单位的名称、住所和法定代表人或者主要负责人;\n" +
// "(二)劳动者的姓名、住址和居民身份证或者其他有效身份证件号码;\n" +
// "(三)劳动合同期限;\n" +
// "(四)工作内容和工作地点;\n" +
// "(五)工作时间和休息休假;\n" +
// "(六)劳动报酬;\n" +
// "(七)社会保险;\n" +
// "(八)劳动保护、劳动条件和职业危害防护;\n" +
// "(九)法律、法规规定应当纳入劳动合同的其他事项。\n" +
// "劳动合同除前款规定的必备条款外,用人单位与劳动者可以约定试用期、培训、保守秘密、补充保险和福利待遇等其他事项。\n" +
// "第十八条 劳动合同对劳动报酬和劳动条件等标准约定不明确,引发争议的,用人单位与劳动者可以重新协商;协商不成的,适用集体合同规定;没有集体合同或者集体合同未规定劳动报酬的,实行同工同酬;没有集体合同或者集体合同未规定劳动条件等标准的,适用国家有关规定。\n" +
// "第十九条 劳动合同期限三个月以上不满一年的,试用期不得超过一个月;劳动合同期限一年以上不满三年的,试用期不得超过二个月;三年以上固定期限和无固定期限的劳动合同,试用期不得超过六个月。\n" +
// "同一用人单位与同一劳动者只能约定一次试用期。\n" +
// "以完成一定工作任务为期限的劳动合同或者劳动合同期限不满三个月的,不得约定试用期。\n" +
// "试用期包含在劳动合同期限内。劳动合同仅约定试用期的,试用期不成立,该期限为劳动合同期限。\n" +
// "第二十条 劳动者在试用期的工资不得低于本单位相同岗位最低档工资或者劳动合同约定工资的百分之八十,并不得低于用人单位所在地的最低工资标准。\n" +
// "第二十一条 在试用期中,除劳动者有本法第三十九条和第四十条第一项、第二项规定的情形外,用人单位不得解除劳动合同。用人单位在试用期解除劳动合同的,应当向劳动者说明理由。\n" +
// "第二十二条 用人单位为劳动者提供专项培训费用,对其进行专业技术培训的,可以与该劳动者订立协议,约定服务期。\n" +
// "劳动者违反服务期约定的,应当按照约定向用人单位支付违约金。违约金的数额不得超过用人单位提供的培训费用。用人单位要求劳动者支付的违约金不得超过服务期尚未履行部分所应分摊的培训费用。\n" +
// "用人单位与劳动者约定服务期的,不影响按照正常的工资调整机制提高劳动者在服务期期间的劳动报酬。\n" +
// "第二十三条 用人单位与劳动者可以在劳动合同中约定保守用人单位的商业秘密和与知识产权相关的保密事项。\n" +
// "对负有保密义务的劳动者,用人单位可以在劳动合同或者保密协议中与劳动者约定竞业限制条款,并约定在解除或者终止劳动合同后,在竞业限制期限内按月给予劳动者经济补偿。劳动者违反竞业限制约定的,应当按照约定向用人单位支付违约金。\n" +
// "第二十四条 竞业限制的人员限于用人单位的高级管理人员、高级技术人员和其他负有保密义务的人员。竞业限制的范围、地域、期限由用人单位与劳动者约定,竞业限制的约定不得违反法律、法规的规定。\n" +
// "在解除或者终止劳动合同后,前款规定的人员到与本单位生产或者经营同类产品、从事同类业务的有竞争关系的其他用人单位,或者自己开业生产或者经营同类产品、从事同类业务的竞业限制期限,不得超过二年。\n" +
// "第二十五条 除本法第二十二条和第二十三条规定的情形外,用人单位不得与劳动者约定由劳动者承担违约金。\n" +
// "第二十六条 下列劳动合同无效或者部分无效:\n" +
// "(一)以欺诈、胁迫的手段或者乘人之危,使对方在违背真实意思的情况下订立或者变更劳动合同的;\n" +
// "(二)用人单位免除自己的法定责任、排除劳动者权利的;\n" +
// "(三)违反法律、行政法规强制性规定的。\n" +
// "对劳动合同的无效或者部分无效有争议的,由劳动争议仲裁机构或者人民法院确认。\n" +
// "第二十七条 劳动合同部分无效,不影响其他部分效力的,其他部分仍然有效。\n" +
// "第二十八条 劳动合同被确认无效,劳动者已付出劳动的,用人单位应当向劳动者支付劳动报酬。劳动报酬的数额,参照本单位相同或者相近岗位劳动者的劳动报酬确定。\n" +
// "第三章 劳动合同的履行和变更\n" +
// "第二十九条 用人单位与劳动者应当按照劳动合同的约定,全面履行各自的义务。\n" +
// "第三十条 用人单位应当按照劳动合同约定和国家规定,向劳动者及时足额支付劳动报酬。\n" +
// "用人单位拖欠或者未足额支付劳动报酬的,劳动者可以依法向当地人民法院申请支付令,人民法院应当依法发出支付令。\n" +
// "第三十一条 用人单位应当严格执行劳动定额标准,不得强迫或者变相强迫劳动者加班。用人单位安排加班的,应当按照国家有关规定向劳动者支付加班费。\n" +
// "第三十二条 劳动者拒绝用人单位管理人员违章指挥、强令冒险作业的,不视为违反劳动合同。\n" +
// "劳动者对危害生命安全和身体健康的劳动条件,有权对用人单位提出批评、检举和控告。\n" +
// "第三十三条 用人单位变更名称、法定代表人、主要负责人或者投资人等事项,不影响劳动合同的履行。\n" +
// "第三十四条 用人单位发生合并或者分立等情况,原劳动合同继续有效,劳动合同由承继其权利和义务的用人单位继续履行。\n" +
// "第三十五条 用人单位与劳动者协商一致,可以变更劳动合同约定的内容。变更劳动合同,应当采用书面形式。\n" +
// "变更后的劳动合同文本由用人单位和劳动者各执一份。\n" +
// "第四章 劳动合同的解除和终止\n" +
// "第三十六条 用人单位与劳动者协商一致,可以解除劳动合同。\n" +
// "第三十七条 劳动者提前三十日以书面形式通知用人单位,可以解除劳动合同。劳动者在试用期内提前三日通知用人单位,可以解除劳动合同。\n" +
// "第三十八条 用人单位有下列情形之一的,劳动者可以解除劳动合同:\n" +
// "(一)未按照劳动合同约定提供劳动保护或者劳动条件的;\n" +
// "(二)未及时足额支付劳动报酬的;\n" +
// "(三)未依法为劳动者缴纳社会保险费的;\n" +
// "(四)用人单位的规章制度违反法律、法规的规定,损害劳动者权益的;\n" +
// "(五)因本法第二十六条第一款规定的情形致使劳动合同无效的;\n" +
// "(六)法律、行政法规规定劳动者可以解除劳动合同的其他情形。\n" +
// "用人单位以暴力、威胁或者非法限制人身自由的手段强迫劳动者劳动的,或者用人单位违章指挥、强令冒险作业危及劳动者人身安全的,劳动者可以立即解除劳动合同,不需事先告知用人单位。\n" +
// "第三十九条 劳动者有下列情形之一的,用人单位可以解除劳动合同:\n" +
// "(一)在试用期间被证明不符合录用条件的;\n" +
// "(二)严重违反用人单位的规章制度的;\n" +
// "(三)严重失职,营私舞弊,给用人单位造成重大损害的;\n" +
// "(四)劳动者同时与其他用人单位建立劳动关系,对完成本单位的工作任务造成严重影响,或者经用人单位提出,拒不改正的;\n" +
// "(五)因本法第二十六条第一款第一项规定的情形致使劳动合同无效的;\n" +
// "(六)被依法追究刑事责任的。\n" +
// "第四十条 有下列情形之一的,用人单位提前三十日以书面形式通知劳动者本人或者额外支付劳动者一个月工资后,可以解除劳动合同:\n" +
// "(一)劳动者患病或者非因工负伤,在规定的医疗期满后不能从事原工作,也不能从事由用人单位另行安排的工作的;\n" +
// "(二)劳动者不能胜任工作,经过培训或者调整工作岗位,仍不能胜任工作的;\n" +
// "(三)劳动合同订立时所依据的客观情况发生重大变化,致使劳动合同无法履行,经用人单位与劳动者协商,未能就变更劳动合同内容达成协议的。\n" +
// "第四十一条 有下列情形之一,需要裁减人员二十人以上或者裁减不足二十人但占企业职工总数百分之十以上的,用人单位提前三十日向工会或者全体职工说明情况,听取工会或者职工的意见后,裁减人员方案经向劳动行政部门报告,可以裁减人员:\n" +
// "(一)依照企业破产法规定进行重整的;\n" +
// "(二)生产经营发生严重困难的;\n" +
// "(三)企业转产、重大技术革新或者经营方式调整,经变更劳动合同后,仍需裁减人员的;\n" +
// "(四)其他因劳动合同订立时所依据的客观经济情况发生重大变化,致使劳动合同无法履行的。\n" +
// "裁减人员时,应当优先留用下列人员:\n" +
// "(一)与本单位订立较长期限的固定期限劳动合同的;\n" +
// "(二)与本单位订立无固定期限劳动合同的;\n" +
// "(三)家庭无其他就业人员,有需要扶养的老人或者未成年人的。\n" +
// "用人单位依照本条第一款规定裁减人员,在六个月内重新招用人员的,应当通知被裁减的人员,并在同等条件下优先招用被裁减的人员。\n" +
// "第四十二条 劳动者有下列情形之一的,用人单位不得依照本法第四十条、第四十一条的规定解除劳动合同:\n" +
// "(一)从事接触职业病危害作业的劳动者未进行离岗前职业健康检查,或者疑似职业病病人在诊断或者医学观察期间的;\n" +
// "(二)在本单位患职业病或者因工负伤并被确认丧失或者部分丧失劳动能力的;\n" +
// "(三)患病或者非因工负伤,在规定的医疗期内的;\n" +
// "(四)女职工在孕期、产期、哺乳期的;\n" +
// "(五)在本单位连续工作满十五年,且距法定退休年龄不足五年的;\n" +
// "(六)法律、行政法规规定的其他情形。\n" +
// "第四十三条 用人单位单方解除劳动合同,应当事先将理由通知工会。用人单位违反法律、行政法规规定或者劳动合同约定的,工会有权要求用人单位纠正。用人单位应当研究工会的意见,并将处理结果书面通知工会。\n" +
// "第四十四条 有下列情形之一的,劳动合同终止:\n" +
// "(一)劳动合同期满的;\n" +
// "(二)劳动者开始依法享受基本养老保险待遇的;\n" +
// "(三)劳动者死亡,或者被人民法院宣告死亡或者宣告失踪的;\n" +
// "(四)用人单位被依法宣告破产的;\n" +
// "(五)用人单位被吊销营业执照、责令关闭、撤销或者用人单位决定提前解散的;\n" +
// "(六)法律、行政法规规定的其他情形。\n" +
// "第四十五条 劳动合同期满,有本法第四十二条规定情形之一的,劳动合同应当续延至相应的情形消失时终止。但是,本法第四十二条第二项规定丧失或者部分丧失劳动能力劳动者的劳动合同的终止,按照国家有关工伤保险的规定执行。\n" +
// "第四十六条 有下列情形之一的,用人单位应当向劳动者支付经济补偿:\n" +
// "(一)劳动者依照本法第三十八条规定解除劳动合同的;\n" +
// "(二)用人单位依照本法第三十六条规定向劳动者提出解除劳动合同并与劳动者协商一致解除劳动合同的;\n" +
// "(三)用人单位依照本法第四十条规定解除劳动合同的;\n" +
// "(四)用人单位依照本法第四十一条第一款规定解除劳动合同的;\n" +
// "(五)除用人单位维持或者提高劳动合同约定条件续订劳动合同,劳动者不同意续订的情形外,依照本法第四十四条第一项规定终止固定期限劳动合同的;\n" +
// "(六)依照本法第四十四条第四项、第五项规定终止劳动合同的;\n" +
// "(七)法律、行政法规规定的其他情形。\n" +
// "第四十七条 经济补偿按劳动者在本单位工作的年限,每满一年支付一个月工资的标准向劳动者支付。六个月以上不满一年的,按一年计算;不满六个月的,向劳动者支付半个月工资的经济补偿。\n" +
// "劳动者月工资高于用人单位所在直辖市、设区的市级人民政府公布的本地区上年度职工月平均工资三倍的,向其支付经济补偿的标准按职工月平均工资三倍的数额支付,向其支付经济补偿的年限最高不超过十二年。\n" +
// "本条所称月工资是指劳动者在劳动合同解除或者终止前十二个月的平均工资。\n" +
// "第四十八条 用人单位违反本法规定解除或者终止劳动合同,劳动者要求继续履行劳动合同的,用人单位应当继续履行;劳动者不要求继续履行劳动合同或者劳动合同已经不能继续履行的,用人单位应当依照本法第八十七条规定支付赔偿金。\n" +
// "第四十九条 国家采取措施,建立健全劳动者社会保险关系跨地区转移接续制度。\n" +
// "第五十条 用人单位应当在解除或者终止劳动合同时出具解除或者终止劳动合同的证明,并在十五日内为劳动者办理档案和社会保险关系转移手续。\n" +
// "劳动者应当按照双方约定,办理工作交接。用人单位依照本法有关规定应当向劳动者支付经济补偿的,在办结工作交接时支付。\n" +
// "用人单位对已经解除或者终止的劳动合同的文本,至少保存二年备查。\n" +
// "第五章 特别规定\n" +
// "第一节 集体合同\n" +
// "第五十一条 企业职工一方与用人单位通过平等协商,可以就劳动报酬、工作时间、休息休假、劳动安全卫生、保险福利等事项订立集体合同。集体合同草案应当提交职工代表大会或者全体职工讨论通过。\n" +
// "集体合同由工会代表企业职工一方与用人单位订立;尚未建立工会的用人单位,由上级工会指导劳动者推举的代表与用人单位订立。\n" +
// "第五十二条 企业职工一方与用人单位可以订立劳动安全卫生、女职工权益保护、工资调整机制等专项集体合同。\n" +
// "第五十三条 在县级以下区域内,建筑业、采矿业、餐饮服务业等行业可以由工会与企业方面代表订立行业性集体合同,或者订立区域性集体合同。\n" +
// "第五十四条 集体合同订立后,应当报送劳动行政部门;劳动行政部门自收到集体合同文本之日起十五日内未提出异议的,集体合同即行生效。\n" +
// "依法订立的集体合同对用人单位和劳动者具有约束力。行业性、区域性集体合同对当地本行业、本区域的用人单位和劳动者具有约束力。\n" +
// "第五十五条 集体合同中劳动报酬和劳动条件等标准不得低于当地人民政府规定的最低标准;用人单位与劳动者订立的劳动合同中劳动报酬和劳动条件等标准不得低于集体合同规定的标准。\n" +
// "第五十六条 用人单位违反集体合同,侵犯职工劳动权益的,工会可以依法要求用人单位承担责任;因履行集体合同发生争议,经协商解决不成的,工会可以依法申请仲裁、提起诉讼。\n" +
// "第二节 劳务派遣\n" +
// "第五十七条 经营劳务派遣业务应当具备下列条件:\n" +
// " (一)注册资本不得少于人民币二百万元;\n" +
// " (二)有与开展业务相适应的固定的经营场所和设施;\n" +
// " (三)有符合法律、行政法规规定的劳务派遣管理制度;\n" +
// " (四)法律、行政法规规定的其他条件。\n" +
// " 经营劳务派遣业务,应当向劳动行政部门依法申请行政许可;经许可的,依法办理相应的公司登记。未经许可,任何单位和个人不得经营劳务派遣业务。\n" +
// "第五十八条 劳务派遣单位是本法所称用人单位,应当履行用人单位对劳动者的义务。劳务派遣单位与被派遣劳动者订立的劳动合同,除应当载明本法第十七条规定的事项外,还应当载明被派遣劳动者的用工单位以及派遣期限、工作岗位等情况。\n" +
// "劳务派遣单位应当与被派遣劳动者订立二年以上的固定期限劳动合同,按月支付劳动报酬;被派遣劳动者在无工作期间,劳务派遣单位应当按照所在地人民政府规定的最低工资标准,向其按月支付报酬。\n" +
// "第五十九条 劳务派遣单位派遣劳动者应当与接受以劳务派遣形式用工的单位(以下称用工单位)订立劳务派遣协议。劳务派遣协议应当约定派遣岗位和人员数量、派遣期限、劳动报酬和社会保险费的数额与支付方式以及违反协议的责任。\n" +
// "用工单位应当根据工作岗位的实际需要与劳务派遣单位确定派遣期限,不得将连续用工期限分割订立数个短期劳务派遣协议。\n" +
// "第六十条 劳务派遣单位应当将劳务派遣协议的内容告知被派遣劳动者。\n" +
// "劳务派遣单位不得克扣用工单位按照劳务派遣协议支付给被派遣劳动者的劳动报酬。\n" +
// "劳务派遣单位和用工单位不得向被派遣劳动者收取费用。\n" +
// "第六十一条 劳务派遣单位跨地区派遣劳动者的,被派遣劳动者享有的劳动报酬和劳动条件,按照用工单位所在地的标准执行。\n" +
// "第六十二条 用工单位应当履行下列义务:\n" +
// "(一)执行国家劳动标准,提供相应的劳动条件和劳动保护;\n" +
// "(二)告知被派遣劳动者的工作要求和劳动报酬;\n" +
// "(三)支付加班费、绩效奖金,提供与工作岗位相关的福利待遇;\n" +
// "(四)对在岗被派遣劳动者进行工作岗位所必需的培训;\n" +
// "(五)连续用工的,实行正常的工资调整机制。\n" +
// "用工单位不得将被派遣劳动者再派遣到其他用人单位。\n" +
// "第六十三条 被派遣劳动者享有与用工单位的劳动者同工同酬的权利。用工单位应当按照同工同酬原则,对被派遣劳动者与本单位同类岗位的劳动者实行相同的劳动报酬分配办法。用工单位无同类岗位劳动者的,参照用工单位所在地相同或者相近岗位劳动者的劳动报酬确定。\n" +
// "劳务派遣单位与被派遣劳动者订立的劳动合同和与用工单位订立的劳务派遣协议,载明或者约定的向被派遣劳动者支付的劳动报酬应当符合前款规定。\n" +
// "第六十四条 被派遣劳动者有权在劳务派遣单位或者用工单位依法参加或者组织工会,维护自身的合法权益。\n" +
// "第六十五条 被派遣劳动者可以依照本法第三十六条、第三十八条的规定与劳务派遣单位解除劳动合同。\n" +
// "被派遣劳动者有本法第三十九条和第四十条第一项、第二项规定情形的,用工单位可以将劳动者退回劳务派遣单位,劳务派遣单位依照本法有关规定,可以与劳动者解除劳动合同。\n" +
// "第六十六条 劳动合同用工是我国的企业基本用工形式。劳务派遣用工是补充形式,只能在临时性、辅助性或者替代性的工作岗位上实施。\n" +
// " 前款规定的临时性工作岗位是指存续时间不超过六个月的岗位;辅助性工作岗位是指为主营业务岗位提供服务的非主营业务岗位;替代性工作岗位是指用工单位的劳动者因脱产学习、休假等原因无法工作的一定期间内,可以由其他劳动者替代工作的岗位。\n" +
// " 用工单位应当严格控制劳务派遣用工数量,不得超过其用工总量的一定比例,具体比例由国务院劳动行政部门规定。\n" +
// "第六十七条 用人单位不得设立劳务派遣单位向本单位或者所属单位派遣劳动者。\n" +
// "第三节 非全日制用工\n" +
// "第六十八条 非全日制用工,是指以小时计酬为主,劳动者在同一用人单位一般平均每日工作时间不超过四小时,每周工作时间累计不超过二十四小时的用工形式。\n" +
// "第六十九条 非全日制用工双方当事人可以订立口头协议。\n" +
// "从事非全日制用工的劳动者可以与一个或者一个以上用人单位订立劳动合同;但是,后订立的劳动合同不得影响先订立的劳动合同的履行。\n" +
// "第七十条 非全日制用工双方当事人不得约定试用期。\n" +
// "第七十一条 非全日制用工双方当事人任何一方都可以随时通知对方终止用工。终止用工,用人单位不向劳动者支付经济补偿。\n" +
// "第七十二条 非全日制用工小时计酬标准不得低于用人单位所在地人民政府规定的最低小时工资标准。\n" +
// "非全日制用工劳动报酬结算支付周期最长不得超过十五日。\n" +
// "第六章 监督检查\n" +
// "第七十三条 国务院劳动行政部门负责全国劳动合同制度实施的监督管理。\n" +
// "县级以上地方人民政府劳动行政部门负责本行政区域内劳动合同制度实施的监督管理。\n" +
// "县级以上各级人民政府劳动行政部门在劳动合同制度实施的监督管理工作中,应当听取工会、企业方面代表以及有关行业主管部门的意见。\n" +
// "第七十四条 县级以上地方人民政府劳动行政部门依法对下列实施劳动合同制度的情况进行监督检查:\n" +
// "(一)用人单位制定直接涉及劳动者切身利益的规章制度及其执行的情况;\n" +
// "(二)用人单位与劳动者订立和解除劳动合同的情况;\n" +
// "(三)劳务派遣单位和用工单位遵守劳务派遣有关规定的情况;\n" +
// "(四)用人单位遵守国家关于劳动者工作时间和休息休假规定的情况;\n" +
// "(五)用人单位支付劳动合同约定的劳动报酬和执行最低工资标准的情况;\n" +
// "(六)用人单位参加各项社会保险和缴纳社会保险费的情况;\n" +
// "(七)法律、法规规定的其他劳动监察事项。\n" +
// "第七十五条 县级以上地方人民政府劳动行政部门实施监督检查时,有权查阅与劳动合同、集体合同有关的材料,有权对劳动场所进行实地检查,用人单位和劳动者都应当如实提供有关情况和材料。\n" +
// "劳动行政部门的工作人员进行监督检查,应当出示证件,依法行使职权,文明执法。\n" +
// "第七十六条 县级以上人民政府建设、卫生、安全生产监督管理等有关主管部门在各自职责范围内,对用人单位执行劳动合同制度的情况进行监督管理。\n" +
// "第七十七条 劳动者合法权益受到侵害的,有权要求有关部门依法处理,或者依法申请仲裁、提起诉讼。\n" +
// "第七十八条 工会依法维护劳动者的合法权益,对用人单位履行劳动合同、集体合同的情况进行监督。用人单位违反劳动法律、法规和劳动合同、集体合同的,工会有权提出意见或者要求纠正;劳动者申请仲裁、提起诉讼的,工会依法给予支持和帮助。\n" +
// "第七十九条 任何组织或者个人对违反本法的行为都有权举报,县级以上人民政府劳动行政部门应当及时核实、处理,并对举报有功人员给予奖励。\n" +
// "第七章 法律责任\n" +
// "第八十条 用人单位直接涉及劳动者切身利益的规章制度违反法律、法规规定的,由劳动行政部门责令改正,给予警告;给劳动者造成损害的,应当承担赔偿责任。\n" +
// "第八十一条 用人单位提供的劳动合同文本未载明本法规定的劳动合同必备条款或者用人单位未将劳动合同文本交付劳动者的,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。\n" +
// "第八十二条 用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。\n" +
// "用人单位违反本法规定不与劳动者订立无固定期限劳动合同的,自应当订立无固定期限劳动合同之日起向劳动者每月支付二倍的工资。\n" +
// "第八十三条 用人单位违反本法规定与劳动者约定试用期的,由劳动行政部门责令改正;违法约定的试用期已经履行的,由用人单位以劳动者试用期满月工资为标准,按已经履行的超过法定试用期的期间向劳动者支付赔偿金。\n" +
// "第八十四条 用人单位违反本法规定,扣押劳动者居民身份证等证件的,由劳动行政部门责令限期退还劳动者本人,并依照有关法律规定给予处罚。\n" +
// "用人单位违反本法规定,以担保或者其他名义向劳动者收取财物的,由劳动行政部门责令限期退还劳动者本人,并以每人五百元以上二千元以下的标准处以罚款;给劳动者造成损害的,应当承担赔偿责任。\n" +
// "劳动者依法解除或者终止劳动合同,用人单位扣押劳动者档案或者其他物品的,依照前款规定处罚。\n" +
// "第八十五条 用人单位有下列情形之一的,由劳动行政部门责令限期支付劳动报酬、加班费或者经济补偿;劳动报酬低于当地最低工资标准的,应当支付其差额部分;逾期不支付的,责令用人单位按应付金额百分之五十以上百分之一百以下的标准向劳动者加付赔偿金:\n" +
// "(一)未按照劳动合同的约定或者国家规定及时足额支付劳动者劳动报酬的;\n" +
// "(二)低于当地最低工资标准支付劳动者工资的;\n" +
// "(三)安排加班不支付加班费的;\n" +
// "(四)解除或者终止劳动合同,未依照本法规定向劳动者支付经济补偿的。\n" +
// "第八十六条 劳动合同依照本法第二十六条规定被确认无效,给对方造成损害的,有过错的一方应当承担赔偿责任。\n" +
// "第八十七条 用人单位违反本法规定解除或者终止劳动合同的,应当依照本法第四十七条规定的经济补偿标准的二倍向劳动者支付赔偿金。\n" +
// "第八十八条 用人单位有下列情形之一的,依法给予行政处罚;构成犯罪的,依法追究刑事责任;给劳动者造成损害的,应当承担赔偿责任:\n" +
// "(一)以暴力、威胁或者非法限制人身自由的手段强迫劳动的;\n" +
// "(二)违章指挥或者强令冒险作业危及劳动者人身安全的;\n" +
// "(三)侮辱、体罚、殴打、非法搜查或者拘禁劳动者的;\n" +
// "(四)劳动条件恶劣、环境污染严重,给劳动者身心健康造成严重损害的。\n" +
// "第八十九条 用人单位违反本法规定未向劳动者出具解除或者终止劳动合同的书面证明,由劳动行政部门责令改正;给劳动者造成损害的,应当承担赔偿责任。\n" +
// "第九十条 劳动者违反本法规定解除劳动合同,或者违反劳动合同中约定的保密义务或者竞业限制,给用人单位造成损失的,应当承担赔偿责任。\n" +
// "第九十一条 用人单位招用与其他用人单位尚未解除或者终止劳动合同的劳动者,给其他用人单位造成损失的,应当承担连带赔偿责任。\n" +
// "第九十二条 违反本法规定,未经许可,擅自经营劳务派遣业务的,由劳动行政部门责令停止违法行为,没收违法所得,并处违法所得一倍以上五倍以下的罚款;没有违法所得的,可以处五万元以下的罚款。\n" +
// " 劳务派遣单位、用工单位违反本法有关劳务派遣规定的,由劳动行政部门责令限期改正;逾期不改正的,以每人五千元以上一万元以下的标准处以罚款,对劳务派遣单位,吊销其劳务派遣业务经营许可证。用工单位给被派遣劳动者造成损害的,劳务派遣单位与用工单位承担连带赔偿责任。\n" +
// "第九十三条 对不具备合法经营资格的用人单位的违法犯罪行为,依法追究法律责任;劳动者已经付出劳动的,该单位或者其出资人应当依照本法有关规定向劳动者支付劳动报酬、经济补偿、赔偿金;给劳动者造成损害的,应当承担赔偿责任。\n" +
// "第九十四条 个人承包经营违反本法规定招用劳动者,给劳动者造成损害的,发包的组织与个人承包经营者承担连带赔偿责任。\n" +
// "第九十五条 劳动行政部门和其他有关主管部门及其工作人员玩忽职守、不履行法定职责,或者违法行使职权,给劳动者或者用人单位造成损害的,应当承担赔偿责任;对直接负责的主管人员和其他直接责任人员,依法给予行政处分;构成犯罪的,依法追究刑事责任。\n" +
// "第八章 附则\n" +
// "第九十六条 事业单位与实行聘用制的工作人员订立、履行、变更、解除或者终止劳动合同,法律、行政法规或者国务院另有规定的,依照其规定;未作规定的,依照本法有关规定执行。\n" +
// "第九十七条 本法施行前已依法订立且在本法施行之日存续的劳动合同,继续履行;本法第十四条第二款第三项规定连续订立固定期限劳动合同的次数,自本法施行后续订固定期限劳动合同时开始计算。\n" +
// "本法施行前已建立劳动关系,尚未订立书面劳动合同的,应当自本法施行之日起一个月内订立。\n" +
// "本法施行之日存续的劳动合同在本法施行后解除或者终止,依照本法第四十六条规定应当支付经济补偿的,经济补偿年限自本法施行之日起计算;本法施行前按照当时有关规定,用人单位应当向劳动者支付经济补偿的,按照当时有关规定执行。\n" +
// "第九十八条 本法自2008年1月1日起施行。\n" +
// "\n";
//
//
//}

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