Compare commits

...

315 Commits

Author SHA1 Message Date
899264250c 请求中附带非法或过期 Token 时,返回重复的 401 请求 #9107 2025-11-17 11:02:43 +08:00
0be7d00eb2 集成vite-plugin-pwa实现渐进式Web应用,首屏很快,同时异步加载了系统的资源,点击菜单更快 2025-11-17 10:24:38 +08:00
7152ae9e49 jeecg-boot-starter项目说明 2025-11-12 23:14:04 +08:00
58b41db786 Online报表(带参数)预览后台报错 #9000 2025-11-11 15:09:15 +08:00
d715c7a0ac rollup版本号固定4.52.5,rollup-plugin-visualizer固定5.14.0,导致打包失败 2025-11-10 15:54:36 +08:00
aca407e1ce AI实战编程教程:JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战 2025-10-31 11:31:10 +08:00
cfea79a187 修复功能菜单“系统管理”-“部门管理”,用户列表中,编辑用户界面-用户账号为空 #9032 2025-10-30 22:26:55 +08:00
7848d1fb33 lock版本更新 2025-10-28 22:59:47 +08:00
91fa645878 3.8.3-master分支:租户用户 菜单下 新增用户报错 #9039 2025-10-28 13:40:27 +08:00
c9fc948658 更新jimureport和jimubi的版本号至2.1.5 2025-10-21 17:44:28 +08:00
b97d041e7f 更新springboot3版本号 2025-10-17 19:31:53 +08:00
6492f2c99a 更新README.md,修正Sa-Token下载链接格式 2025-10-16 19:13:47 +08:00
bf32385a06 更新README.md,调整下载链接顺序 2025-10-16 19:12:11 +08:00
6ef637c46f 提供SpringBoot3.3 + Sa-Token版本 2025-10-16 19:05:59 +08:00
bc6f336745 issues/8972 通义千问的多模态模型保存激活报错 #55 2025-10-14 22:46:46 +08:00
0d86df8e9e 1 2025-10-14 18:03:51 +08:00
3db673b67d issue格式 2025-10-14 18:03:01 +08:00
3ba5395d33 优化gateway启动报警告 2025-10-14 16:48:17 +08:00
e7eed37470 升级shardingsphere-jdbc版本到5.5.0,需要手工配置ShardingSphere数据源到spring.datasource.dynamic.datasource中,用法更明确 2025-10-14 16:47:05 +08:00
30ac3f7c72 升级shardingsphere-jdbc版本到5.5.0,需要手工配置ShardingSphere数据源到spring.datasource.dynamic.datasource中,用法更明确 2025-10-14 16:02:15 +08:00
03e6c97d80 重构JeecgBizToolsProvider.java,使用JsonObjectSchema替代JsonSchemaProperty,优化参数定义 2025-10-13 14:10:52 +08:00
b9f6f6dc53 升级langchain4j到1.3.0,解决很多模型不支持问题和MCP支持 2025-10-13 11:27:09 +08:00
107e13c8af [issues/8859]online表单java增强失效-- 2025-10-11 11:35:36 +08:00
0512b41b2b 更新README.md,增加对Node.js版本要求的说明,强调不再支持EOL的Node.js 18 2025-10-10 17:34:00 +08:00
d6d880f887 更新说明 2025-10-10 10:20:09 +08:00
b0e974a418 更新README.md,优化平台介绍和技术架构信息,增强AI应用平台描述 2025-10-09 11:15:45 +08:00
388fa9b8c2 v3.8.3大版本发布,全面迈向 SpringBoot3 2025-10-09 11:06:32 +08:00
bc04bd1433 --author:scott--date:20250930--for:使用@PostConstruct注解初始化PrometheusMeterRegistry配置,避免启动后配置延迟 2025-09-30 15:43:24 +08:00
35aba0784d Path Traversal Vulnerability /sys/comment/addFile /sys/upload/uploadMinio endpoint (notice the uploadlocal function is different from the /sys/common/upload ) #8827 2025-09-29 18:24:33 +08:00
c3822ab702 3.8.3版本能正常连接sqlserver数据库,但是无法解析查询代码 #8900 2025-09-29 11:55:37 +08:00
d4487356f0 更新 JeecgSystemApplication.java,排除 MongoAutoConfiguration 以避免未集成 mongo 的报错 2025-09-28 22:34:10 +08:00
ae4363dc72 仪表盘大屏分享,提示需要token错误 2025-09-28 18:12:03 +08:00
3e6c7651ee 提供v3.8.3版本数据库脚本 2025-09-28 14:29:52 +08:00
c0ffd14b7a 更新 pom.xml,修改 jimureport 依赖的 artifactId 2025-09-26 16:26:35 +08:00
914875d6a1 更新 pom.xml,升级 jimubi-spring-boot-starter 和 jimureport-nosql-starter 版本 2025-09-26 15:26:30 +08:00
2298ee3eed 更新 docker-compose-cloud.yml,添加 jeecg-boot 网络配置到 sentinel 和 xxljob 服务 2025-09-25 13:47:33 +08:00
2a8853b353 docker微服务启动 docker-compose 增加xxljob和sentinel配置 2025-09-25 12:48:25 +08:00
b920c5b794 Oracle11g数据库 多租户管理>>添加租户 报错 #8897 2025-09-25 12:42:53 +08:00
d3fa38a9e6 更新 application-docker.yml,修改 Redis 和 XXL-JOB 配置为使用服务名称 2025-09-25 11:17:53 +08:00
b0df78b06c 配置文件升级到v3.8.3 切换tomcat 2025-09-25 10:23:51 +08:00
80749098bd 添加 JustAuth 自动配置支持,更新 pom.xml 以包含 jeecg-boot-starter-job 依赖,并修改 application.yml 中的 nacos 服务器地址配置 2025-09-24 16:06:54 +08:00
19b7f2cb29 springboot3 支持 jdk17、jdk21、jdk24 2025-09-24 15:18:32 +08:00
39f5c3a5be 支持jdk21 2025-09-24 13:18:52 +08:00
9ee3a36fbb 版本发布日期更新 2025-09-24 13:17:21 +08:00
8c5cf3a0d9 【v3.8.3开源版本发布】更改groupId从org.jeecgframework.boot为org.jeecgframework.boot3,与springboot2区分 2025-09-24 12:19:01 +08:00
053552c123 【v3.8.3开源版本发布】
Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	README.md
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/FileDownloadUtils.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserAgentController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/ISysDepartService.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/ISysUserService.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/SysAnnouncementServiceImpl.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/pom.xml
#	jeecg-boot/pom.xml
2025-09-24 12:01:11 +08:00
fc44deca83 Merge remote-tracking branch 'origin/springboot3' into springboot3 2025-09-24 11:18:36 +08:00
5d5d9fc53d Merge branch 'master' of https://github.com/zhangdaiscott/jeecg-boot 2025-09-24 11:16:50 +08:00
002bfe25f8 【v3.8.3】升级数据库,暂时先删除 2025-09-24 11:06:54 +08:00
7cb2dc4fde Update @vitejs/plugin-vue-jsx version
Updated version of @vitejs/plugin-vue-jsx to 4.1.1.
自动下载的依赖 @vitejs/plugin-vue-jsx@4.2.0 是 CJS 格式 , 1.0.0-beta.38 版本的 @rolldown/pluginutils 是 ESM 格式模块不兼容
2025-09-18 17:32:11 +08:00
9c244bd266 Update @vitejs/plugin-vue-jsx version
Updated version of @vitejs/plugin-vue-jsx to 4.1.1.

 自动下载的依赖 @vitejs/plugin-vue-jsx@4.2.0 是 CJS 格式 , 1.0.0-beta.38 版本的 @rolldown/pluginutils 是 ESM 格式
模块不兼容
2025-09-18 17:31:14 +08:00
ab151879b3 XXL-JOB版本号错误,修改至v2.4.0 2025-09-14 16:03:45 +08:00
000ae1db30 升级前端pnpm lock 2025-09-14 15:52:27 +08:00
63e066180d 【v3.8.3】更新pom.xml,升级Kotlin和Liteflow版本 2025-09-14 12:57:48 +08:00
44c1079f87 【v3.8.3】优化顶部导航风格菜单的样式,支持外部链接打开及菜单重定向 2025-09-14 11:59:09 +08:00
e825e0f912 【v3.8.3】升级数据库 2025-09-14 11:58:05 +08:00
132e89b0e1 【v3.8.3】底层core的一些功能修改 2025-09-14 11:57:47 +08:00
881a637285 【v3.8.3】AI助手调用系统业务扩展接口,支持添加用户、查询用户、查询角色及授予角色功能 2025-09-14 11:57:37 +08:00
02e9f8984f 【v3.8.3】更新EnumDict.java,添加使用说明和配置要求 2025-09-14 11:55:45 +08:00
a4343fc2cb 【v3.8.3】底层core的一些功能修改 2025-09-14 11:55:31 +08:00
152e8c7aaa 【v3.8.3】优化枚举字典数据加载,支持多包路径扫描,提升初始化性能 2025-09-14 11:54:01 +08:00
d7dc81455d 【v3.8.3】用户组织机构大改造(新增主岗位、主岗位、用户签名) 2025-09-14 11:53:36 +08:00
aefdcd6315 【v3.8.3】大数据导出共通类 2025-09-14 11:50:56 +08:00
1cf4054e76 【v3.8.3】租户大改造 2025-09-14 11:50:37 +08:00
7829cf18d7 解决升级mybatisPlus后SqlServer分页使用OFFSET 2025-09-14 11:49:55 +08:00
69c3a9da9a 【v3.8.3】升级版本号至3.8.3,更新依赖项和排除项 2025-09-14 11:49:42 +08:00
4d34150479 升级kingbase8驱动和切换回tomcat 2025-09-14 11:48:00 +08:00
29687c8908 【v3.8.3】升级版本号3.8.3和docker配置 2025-09-14 10:48:13 +08:00
2e93a92dde 【v3.8.3】修改配置文件,删除undertow 2025-09-14 10:45:32 +08:00
d383f7458d 【v3.8.3】企业微信通知采用卡片 2025-09-14 10:44:08 +08:00
700318e1c1 【v3.8.3】首页配置功能改造 2025-09-14 10:43:21 +08:00
d728d6b090 【v3.8.3】升级代码生成器模板 2025-09-14 10:43:00 +08:00
7abc2e4c9c 【v3.8.3】大数据导出示例 2025-09-14 10:42:45 +08:00
da2b0cc354 【v3.8.3】升级aiflow 2025-09-14 10:41:53 +08:00
a6751c22be 【v3.8.3】功能性能优化 2025-09-14 10:41:25 +08:00
f087525a75 【v3.8.3】undertow不稳定切换回tomcat 2025-09-14 10:40:21 +08:00
4f46213df6 【v3.8.3】前端小改动汇总集合 2025-09-14 10:39:48 +08:00
d76842ae07 【v3.8.3】airag优化体验升级 2025-09-14 10:38:41 +08:00
8c64db46e5 【v3.8.3】默认首页改造 2025-09-14 10:37:29 +08:00
81fb2ac3b2 【v3.8.3】切换回Tomcat信息 2025-09-14 10:37:14 +08:00
fa98817aeb 【v3.8.3】部门大改造关联修改 2025-09-14 10:37:05 +08:00
1158977826 更新版本号至3.8.3,修改README.md以反映最新框架和工具版本 2025-09-14 10:27:07 +08:00
8b6def0ee3 导出excel总提示格式不匹配和日志大数据导出示例 2025-09-14 10:26:52 +08:00
39c0d5b3f5 【v3.8.3】组织机构部门大改造(支持子公司、岗位与不能功能划分更清晰,岗位可以设置上下级,岗位可以设置职级,支持回报关系) 2025-09-14 10:26:24 +08:00
adeebee840 【v3.8.3】用户大改造,取消原职位换成职位字典、原职位改成岗位职级、新增主岗位和兼职岗位 2025-09-14 10:24:02 +08:00
862aaa8632 【v3.8.3】我的租户大改造 2025-09-14 10:22:26 +08:00
6a11ff8a64 【v3.8.3】升级版本号 2025-09-14 10:16:56 +08:00
73059b8a53 优化AIRG 2025-09-13 16:15:23 +08:00
e377bf6990 升级版本号 2025-09-05 18:49:20 +08:00
434b42e9ed 部分配置丢失,修复 2025-09-04 10:47:50 +08:00
f1ceb08e16 Merge remote-tracking branch 'origin/master' into springboot3
# Conflicts:
#	README.md
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/pom.xml
#	jeecg-boot/pom.xml
#	jeecgboot-vue3/src/components/jeecg/JVxeTable/src/hooks/useFinallyProps.ts
2025-09-04 09:40:57 +08:00
d2eedacc85 优化项目介绍 2025-09-03 19:58:13 +08:00
8791384791 优化项目介绍 2025-09-03 19:21:56 +08:00
fd60e49f5b 升级积木报表和积木BI到2.1.3\升级minidao解决SqlServer兼容问题 2025-09-03 17:44:01 +08:00
5f1dc06067 升级积木报表和积木BI到2.1.3\升级minidao解决SqlServer兼容问题 2025-09-03 17:43:57 +08:00
b67770ff14 升级miniao到1.10.14,解决SqlServer分页兼容问题 2025-09-03 17:36:32 +08:00
1dae808cf1 升级积木报表和积木BI到最新版 v2.1.3 2025-09-03 16:34:26 +08:00
70d8353219 升级springBoot 3.5.5 2025-09-03 15:53:53 +08:00
208d9990ae 主干默认springboot3版本 2025-09-03 15:41:30 +08:00
3e208de18e 默认主干切换springboot3分支 2025-09-03 15:37:26 +08:00
4f3c71af5b 主干默认springboot3版本 2025-09-03 15:30:19 +08:00
70bd639206 【issues/8738】componentProps是函数时获取不到valueType 2025-08-26 13:29:16 +08:00
d245ef3037 【JVXETable】修复首屏加载速度 2025-08-26 13:29:05 +08:00
7af8346b79 【#8695】修复JVxeTable卡顿问题 2025-08-22 17:51:42 +08:00
1b8a31f0d3 代码生成器还原到 1.5.1 2025-08-22 14:44:57 +08:00
294ad5a6c9 代码生成,把角色授权sql菜单的,也生成出来 2025-08-20 19:34:01 +08:00
065b255d90 设置代码生成,FreeMarker空值处理不报错 2025-08-20 15:12:59 +08:00
56976e68b4 升级spring-boot到3.5.4、升级spring-cloud到2025.0.0、升级spring-cloud-alibaba到2023.0.3.3 2025-08-20 10:42:43 +08:00
db1ff0268b Squashed commit of the following:
commit b7519d7199
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 15:18:52 2025 +0800

    中文乱码

commit 81ba07c853
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 15:09:21 2025 +0800

    增加代码生成用法文档

commit 92ed296e63
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:04:22 2025 +0800

    【issues/8709】LayoutContent样式多出1px

commit c2aff84914
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:04:09 2025 +0800

    【issues/8683】DatePicker组件的componentProps使用函数形式时初始值获取不对

commit e002cd3bf3
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:03:51 2025 +0800

    【issues/8680】editComponentProps 可接受一个函数传入record

commit 1de07ff3ff
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:03:32 2025 +0800

    -- author:liaozhiyang---date:20250813--for:【issues/8690】BasicTable的rowSelection新增onSelect方法 ---

commit 35852d41f1
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:03:04 2025 +0800

    jvxeTable表格切换disabled属性时,相邻的两个枚举下拉,如果值是一样的,但是label不一样,会把第二个下拉的显示值渲染到第一个下拉中 #8593

commit a2cb1d9f25
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 14:00:01 2025 +0800

    【issues/8529】setColumns将原本隐藏的列展示后,列配置里却没有勾选该列

commit 2002af54d0
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 13:59:42 2025 +0800

    JVxeTypes.image组件action字段只能定义第1张图片的上传接口,后面图片的接口还是使用公共上传接口 #8628

commit 89747403a2
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 13:59:28 2025 +0800

    JVxeTable组件全选翻页后会被取消选中 #8630

commit 3db0995c3f
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 19 11:23:19 2025 +0800

    [代码生成]前端代码支持直接生成到前端项目、菜单sql会自动生成到start项目的flyway目录

commit 950621dd88
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 18 23:04:59 2025 +0800

    升级代码生成器,一键生成代码,vue3代码会生成到前端项目、菜单升级sql自动迁移到flyway目录重启自动执行(不需要手工迁移前端代码和手工执行升级sql)

commit 033cf51d69
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 18 23:04:52 2025 +0800

    升级代码生成器,一键生成代码,vue3代码会生成到前端项目、菜单升级sql自动迁移到flyway目录重启自动执行(不需要手工迁移前端代码和手工执行升级sql)

commit fb9f367517
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 18 23:02:53 2025 +0800

    代码生成,online自定义按钮无排序报错

commit b2da45d803
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 18 16:23:22 2025 +0800

    演示地址

commit 2840f0d325
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 18 15:37:10 2025 +0800

    默认账号密码

commit 6ace7eae8a
Author: JEECG <445654970@qq.com>
Date:   Sun Aug 17 15:11:45 2025 +0800

    开发环境关闭日志生成,项目启动快;生产环境请按需打开注释

commit 3d88147c59
Author: JEECG <445654970@qq.com>
Date:   Thu Aug 14 23:03:46 2025 +0800

    调整微服务启动文档

commit ba0052d452
Author: JEECG <445654970@qq.com>
Date:   Wed Aug 13 13:55:13 2025 +0800

    支持lazy-initialization启动,项目大了后启动会更快

commit 69fca254f0
Author: JEECG <445654970@qq.com>
Date:   Wed Aug 13 11:47:27 2025 +0800

    补充注释

commit b3de596199
Author: JEECG <445654970@qq.com>
Date:   Wed Aug 13 11:47:16 2025 +0800

    彻底关闭 prettier 校验规则

commit f46273d15e
Author: JEECG <445654970@qq.com>
Date:   Wed Aug 13 10:26:23 2025 +0800

    设置ESLint 的 vue/html-self-closing 自闭合标签警告配置

commit 0fe258dbc2
Author: JEECG <445654970@qq.com>
Date:   Wed Aug 13 09:26:16 2025 +0800

    修复 onExportXls defSort 不生效问题 #7570

commit de7f23c555
Merge: d97e56b2 444c7140
Author: JEECG <zhangdaiscott@163.com>
Date:   Wed Aug 13 09:20:31 2025 +0800

    Merge pull request #8496 from lileiAimee/developer

    解决TableAction中自定义图标颜色不起作用的问题

commit d97e56b2f0
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 19:08:43 2025 +0800

    多租户模式下系统系统会给租户默认增加上测试的角色菜单,但是后台获取菜单时异常,无法打开相关页面 #8667

commit c868496b78
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 19:02:49 2025 +0800

    映射警告

commit c5150baa69
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 18:18:34 2025 +0800

    支持通过用户账号邀请加入租户

commit 3d9f59c69b
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 18:17:55 2025 +0800

    邀请用户加入租户,支持通过用户账号

commit 420d6db3fb
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 18:06:20 2025 +0800

    登录用户没有部门,不提示警告

commit 473a626039
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 14:10:02 2025 +0800

    增加JPopup组件带参数示例

commit 0308b0597c
Author: JEECG <445654970@qq.com>
Date:   Tue Aug 12 14:08:18 2025 +0800

    【issues/8426】解决JPopup组件传参不能接收

commit 2191f5d48c
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 11 22:43:07 2025 +0800

    调整位置

commit 1158b0b6e7
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 11 22:30:39 2025 +0800

    升级seata到1.7.0;升级dynamic-datasource-spring-boot-starter到3.5.2

commit ead2cef1f4
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 11 18:47:48 2025 +0800

    支持多字段默认排序defSort数组、解决多列排序无效 #8659

commit 83bb0a0a6a
Author: JEECG <445654970@qq.com>
Date:   Mon Aug 11 18:47:43 2025 +0800

    支持多字段默认排序defSort数组

commit b474e9e5a5
Author: JEECG <445654970@qq.com>
Date:   Sun Aug 10 17:06:01 2025 +0800

    开发环境安装

commit 422373e300
Author: JEECG <445654970@qq.com>
Date:   Sun Aug 10 16:30:13 2025 +0800

    提供JeecgBoot 运行环境python检查脚本

commit 1cf11a4c2a
Author: JEECG <445654970@qq.com>
Date:   Sat Aug 9 09:41:57 2025 +0800

    提供jeecgboot-oracle11g.dmp

commit 925f163784
Author: JEECG <445654970@qq.com>
Date:   Fri Aug 8 22:07:24 2025 +0800

    引入jeecg-boot-starter-job依赖启动报错 #8694

commit d01c1d7d47
Author: JEECG <445654970@qq.com>
Date:   Thu Aug 7 15:41:21 2025 +0800

    支持lazy-initialization

commit 3576b54945
Author: JEECG <445654970@qq.com>
Date:   Thu Aug 7 15:36:51 2025 +0800

    升级积木报表和积木BI到最新版v2.1.2

commit 444c7140f6
Author: lileiAimee <345697385@qq.com>
Date:   Wed Jun 25 09:55:07 2025 +0800

    解决TableAction中自定义图标颜色不起作用的问题

# Conflicts:
#	README-EN.md
#	README.md
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-test/jeecg-cloud-test-seata/pom.xml
#	jeecg-boot/pom.xml
2025-08-19 22:58:12 +08:00
b7519d7199 中文乱码 2025-08-19 15:18:52 +08:00
81ba07c853 增加代码生成用法文档 2025-08-19 15:09:21 +08:00
92ed296e63 【issues/8709】LayoutContent样式多出1px 2025-08-19 14:04:22 +08:00
c2aff84914 【issues/8683】DatePicker组件的componentProps使用函数形式时初始值获取不对 2025-08-19 14:04:09 +08:00
e002cd3bf3 【issues/8680】editComponentProps 可接受一个函数传入record 2025-08-19 14:03:51 +08:00
1de07ff3ff -- author:liaozhiyang---date:20250813--for:【issues/8690】BasicTable的rowSelection新增onSelect方法 --- 2025-08-19 14:03:32 +08:00
35852d41f1 jvxeTable表格切换disabled属性时,相邻的两个枚举下拉,如果值是一样的,但是label不一样,会把第二个下拉的显示值渲染到第一个下拉中 #8593 2025-08-19 14:03:04 +08:00
a2cb1d9f25 【issues/8529】setColumns将原本隐藏的列展示后,列配置里却没有勾选该列 2025-08-19 14:00:01 +08:00
2002af54d0 JVxeTypes.image组件action字段只能定义第1张图片的上传接口,后面图片的接口还是使用公共上传接口 #8628 2025-08-19 13:59:42 +08:00
89747403a2 JVxeTable组件全选翻页后会被取消选中 #8630 2025-08-19 13:59:28 +08:00
3db0995c3f [代码生成]前端代码支持直接生成到前端项目、菜单sql会自动生成到start项目的flyway目录 2025-08-19 11:23:19 +08:00
950621dd88 升级代码生成器,一键生成代码,vue3代码会生成到前端项目、菜单升级sql自动迁移到flyway目录重启自动执行(不需要手工迁移前端代码和手工执行升级sql) 2025-08-18 23:04:59 +08:00
033cf51d69 升级代码生成器,一键生成代码,vue3代码会生成到前端项目、菜单升级sql自动迁移到flyway目录重启自动执行(不需要手工迁移前端代码和手工执行升级sql) 2025-08-18 23:04:52 +08:00
fb9f367517 代码生成,online自定义按钮无排序报错 2025-08-18 23:02:53 +08:00
b2da45d803 演示地址 2025-08-18 16:23:22 +08:00
2840f0d325 默认账号密码 2025-08-18 15:37:10 +08:00
6ace7eae8a 开发环境关闭日志生成,项目启动快;生产环境请按需打开注释 2025-08-17 15:11:45 +08:00
3d88147c59 调整微服务启动文档 2025-08-14 23:03:46 +08:00
08f245bdf9 修改遗漏 swagger上选择的接口和实际接口不对应#8705 2025-08-13 18:15:19 +08:00
8cc033b86f swagger上选择的接口和实际接口不对应 #8705 2025-08-13 17:22:42 +08:00
6b7542620b swagger上选择的接口和实际接口不对应 #8705 2025-08-13 16:44:16 +08:00
ba0052d452 支持lazy-initialization启动,项目大了后启动会更快 2025-08-13 13:55:13 +08:00
69fca254f0 补充注释 2025-08-13 11:47:27 +08:00
b3de596199 彻底关闭 prettier 校验规则 2025-08-13 11:47:16 +08:00
f46273d15e 设置ESLint 的 vue/html-self-closing 自闭合标签警告配置 2025-08-13 10:26:23 +08:00
0fe258dbc2 修复 onExportXls defSort 不生效问题 #7570 2025-08-13 09:26:16 +08:00
de7f23c555 Merge pull request #8496 from lileiAimee/developer
解决TableAction中自定义图标颜色不起作用的问题
2025-08-13 09:20:31 +08:00
67d9865861 Merge pull request #8550 from TsuGit/fix/xxljob-startup-error
fix(xxljob): 修复因 factoryBeanObjectType 导致的启动失败
2025-08-13 00:02:30 +08:00
d97e56b2f0 多租户模式下系统系统会给租户默认增加上测试的角色菜单,但是后台获取菜单时异常,无法打开相关页面 #8667 2025-08-12 19:08:43 +08:00
c868496b78 映射警告 2025-08-12 19:02:49 +08:00
c5150baa69 支持通过用户账号邀请加入租户 2025-08-12 18:18:34 +08:00
3d9f59c69b 邀请用户加入租户,支持通过用户账号 2025-08-12 18:17:55 +08:00
420d6db3fb 登录用户没有部门,不提示警告 2025-08-12 18:06:20 +08:00
473a626039 增加JPopup组件带参数示例 2025-08-12 14:10:02 +08:00
0308b0597c 【issues/8426】解决JPopup组件传参不能接收 2025-08-12 14:08:18 +08:00
cd809a6573 Squashed commit of the following:
升级seata到1.7.0;升级dynamic-datasource-spring-boot-starter到3.5.2
    支持多字段默认排序defSort数组、解决多列排序无效 #8659
    支持多字段默认排序defSort数组
    提供JeecgBoot 运行环境python检查脚本
    提供jeecgboot-oracle11g.dmp
2025-08-12 09:25:01 +08:00
2191f5d48c 调整位置 2025-08-11 22:43:07 +08:00
1158b0b6e7 升级seata到1.7.0;升级dynamic-datasource-spring-boot-starter到3.5.2 2025-08-11 22:30:39 +08:00
ead2cef1f4 支持多字段默认排序defSort数组、解决多列排序无效 #8659 2025-08-11 18:47:48 +08:00
83bb0a0a6a 支持多字段默认排序defSort数组 2025-08-11 18:47:43 +08:00
b474e9e5a5 开发环境安装 2025-08-10 17:06:01 +08:00
422373e300 提供JeecgBoot 运行环境python检查脚本 2025-08-10 16:30:13 +08:00
1cf11a4c2a 提供jeecgboot-oracle11g.dmp 2025-08-09 09:41:57 +08:00
ac446691c4 Squashed commit of the following:
commit 925f163784
Author: JEECG <445654970@qq.com>
Date:   Fri Aug 8 22:07:24 2025 +0800

    引入jeecg-boot-starter-job依赖启动报错 #8694

commit d01c1d7d47
Author: JEECG <445654970@qq.com>
Date:   Thu Aug 7 15:41:21 2025 +0800

    支持lazy-initialization

commit 3576b54945
Author: JEECG <445654970@qq.com>
Date:   Thu Aug 7 15:36:51 2025 +0800

    升级积木报表和积木BI到最新版v2.1.2

# Conflicts:
#	jeecg-boot/pom.xml
2025-08-08 22:22:30 +08:00
925f163784 引入jeecg-boot-starter-job依赖启动报错 #8694 2025-08-08 22:07:24 +08:00
0feb307e8d Merge remote-tracking branch 'origin/master' into springboot3 2025-08-07 18:11:58 +08:00
781d61e96e swagger请求头部没有X-Access-Token #8676 2025-08-07 18:02:27 +08:00
d01c1d7d47 支持lazy-initialization 2025-08-07 15:41:21 +08:00
3576b54945 升级积木报表和积木BI到最新版v2.1.2 2025-08-07 15:36:51 +08:00
a760f94b94 解决pgvector向量数据库不支持2000维度的问题 2025-08-07 13:51:46 +08:00
e795e03365 【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY,导致online报表报错 2025-08-04 18:39:35 +08:00
342bdd2e38 【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY,无默认排序字段导致online报表报错 2025-08-04 18:14:11 +08:00
59ece16059 修改springboot3的配置yml 2025-08-03 12:51:32 +08:00
91208a4968 Merge remote-tracking branch 'origin/master' into springboot3 2025-08-03 12:49:26 +08:00
419e2bea0b 其他数据库配置 2025-08-03 12:47:42 +08:00
443abc3ede 其他数据库配置 2025-08-03 12:45:01 +08:00
358e46559f 其他数据库配置 2025-08-03 12:44:55 +08:00
128c2c97f6 修改springboot3的配置yml 2025-08-03 10:32:18 +08:00
424dc33bba 修改springboot3的配置yml 2025-08-03 10:13:47 +08:00
1cb48b4f0c 【解决SqlServer兼容问题,提供oracle和SqlServer的数据库脚本】
Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisPlusSaasConfig.java
2025-08-03 10:07:04 +08:00
ea59454f51 oracle初始脚本 2025-08-03 10:04:37 +08:00
615a1bc4ff sqlserver初始脚本 2025-08-03 09:52:13 +08:00
d976f12c8f sqlserver初始脚本 2025-08-03 09:51:49 +08:00
3783765161 sqlserver初始脚本 2025-08-03 09:43:21 +08:00
8cc6810fdd 提交oracle配置yml 2025-08-03 09:43:07 +08:00
a902d9af19 提交SqlServer配置yml 2025-08-03 09:40:00 +08:00
37a116f2fb 提交oracle配置yml 2025-08-03 09:39:20 +08:00
1564831f7e 提交oracle配置yml 2025-08-03 09:37:36 +08:00
18c1cd00c1 提交SqlServer配置yml 2025-08-03 09:37:24 +08:00
a988b05e72 提交oracle配置yml 2025-08-03 09:31:37 +08:00
a9a6fd529d 提交SqlServer配置yml 2025-08-03 09:31:11 +08:00
6586d3a880 【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY,导致online报表报错--- 2025-08-02 19:45:36 +08:00
af354f9f5e 【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY,导致online报表报错--- 2025-08-02 19:10:24 +08:00
79b182819b Merge remote-tracking branch 'origin/master' into springboot3 2025-08-01 17:20:53 +08:00
a638a93b65 一键docker启动脚本 2025-08-01 17:13:28 +08:00
621781d336 JEECG Boot 一键docker启动脚本 2025-08-01 17:04:39 +08:00
f1cad333da 提供一键构建docker镜像的脚本,省掉手工操作步骤 2025-08-01 12:39:28 +08:00
e70844ce61 【合并v3.8.2 docker-compose优化配置】
Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/Dockerfile
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-docker.yml
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/pom.xml
2025-08-01 10:54:33 +08:00
e00ffa2670 升级autopoi到1.4.14最新版 2025-08-01 10:46:07 +08:00
676fffa2c8 docker-compose增加jeecg-boot-pgvector 2025-07-31 22:47:01 +08:00
23cc569a47 docker system日志中文乱码解决 2025-07-31 18:12:57 +08:00
dafacf153b 升级版本号到3.8.2 2025-07-31 18:12:41 +08:00
7a9f357510 更新v3.8.2 功能清单 2025-07-31 17:56:40 +08:00
6c15b45a8c 【合并v3.8.2最新版代码】
Squashed commit of the following:

commit f30a8c658a
Author: JEECG <445654970@qq.com>
Date:   Thu Jul 31 11:35:16 2025 +0800

    数据库缺少openapi微服务网关配置

commit e84d7726d2
Author: JEECG <445654970@qq.com>
Date:   Thu Jul 31 10:20:09 2025 +0800

    后台接口地址修改

commit 0f39802698
Author: JEECG <445654970@qq.com>
Date:   Thu Jul 31 09:56:24 2025 +0800

    docker自动化部署命令

commit a014a3ed0e
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 21:55:16 2025 +0800

    v3.8.2 优化一键docker启动前后端

commit 5720d1a01e
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 19:26:38 2025 +0800

    升级版本号到3.8.2

commit 5eed6ac6d2
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 18:49:29 2025 +0800

    升级版本号到3.8.2

commit 0cfa1e223a
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 18:28:10 2025 +0800

    v3.8.2 系统通知改造支持分类

commit 219869f4c0
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 18:25:58 2025 +0800

    v3.8.2 版本前端代码

commit e6edde963a
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 18:25:46 2025 +0800

    v3.8.2 版本后端代码

commit c44b66128e
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 18:23:09 2025 +0800

    XXL-JOB(2.4.0 及以上)已被移除,分片参数获取方式变更。

commit 9356b04741
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 10:57:52 2025 +0800

    升级online到3.8.2-beta

commit d0a094f9a3
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 10:57:31 2025 +0800

    升级mybatis-plus到3.5.12、升级jsqlparser到4.9

commit 73eb625737
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 09:51:34 2025 +0800

    升级jimureport到v2.1.1

commit 74880705b8
Author: JEECG <445654970@qq.com>
Date:   Wed Jul 30 09:18:46 2025 +0800

    升级online到3.8.2-beta

# Conflicts:
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/pom.xml
2025-07-31 14:23:45 +08:00
f30a8c658a 数据库缺少openapi微服务网关配置 2025-07-31 11:35:16 +08:00
e84d7726d2 后台接口地址修改 2025-07-31 10:20:09 +08:00
0f39802698 docker自动化部署命令 2025-07-31 09:56:24 +08:00
a014a3ed0e v3.8.2 优化一键docker启动前后端 2025-07-30 21:55:16 +08:00
5720d1a01e 升级版本号到3.8.2 2025-07-30 19:26:38 +08:00
5eed6ac6d2 升级版本号到3.8.2 2025-07-30 18:49:29 +08:00
0cfa1e223a v3.8.2 系统通知改造支持分类 2025-07-30 18:28:10 +08:00
219869f4c0 v3.8.2 版本前端代码 2025-07-30 18:25:58 +08:00
e6edde963a v3.8.2 版本后端代码 2025-07-30 18:25:46 +08:00
c44b66128e XXL-JOB(2.4.0 及以上)已被移除,分片参数获取方式变更。 2025-07-30 18:23:09 +08:00
9356b04741 升级online到3.8.2-beta 2025-07-30 10:57:52 +08:00
d0a094f9a3 升级mybatis-plus到3.5.12、升级jsqlparser到4.9 2025-07-30 10:57:31 +08:00
73eb625737 升级jimureport到v2.1.1 2025-07-30 09:51:34 +08:00
74880705b8 升级online到3.8.2-beta 2025-07-30 09:18:46 +08:00
f67cfa1bfb 删除重复依赖 2025-07-29 12:00:05 +08:00
8d91caa4e6 升级积木报表和积木BI到最新版 2025-07-28 18:54:21 +08:00
0d9f9a04cc 升级knife4j-openapi3-jakarta-spring-boot-starter到4.6.0解决knife4j-production不生效问题 2025-07-25 14:08:00 +08:00
90565fcf79 Merge remote-tracking branch 'origin/master' into springboot3
# Conflicts:
#	jeecg-boot/db/tables_nacos.sql
2025-07-25 13:23:47 +08:00
118775cf79 【swagger问题】不带/doc.html访问后台项目swaager,接口测试实际请求后台接口地址少了项目前缀 2025-07-25 13:14:11 +08:00
7b80ae3e68 【swagger文档】application/json变成了application/x-www-form-urlencoded 2025-07-25 13:01:36 +08:00
cf4d888839 TODO 暂时注释掉,for:【issues/8638】springboot3分支,knife4j不能正确显示文档,但是swagger-ui和v3/api-docs正常 #8638 2025-07-24 18:34:25 +08:00
336e7851aa 【online】下拉搜索+带条件表字典,报错400 2025-07-22 10:08:40 +08:00
56b9131675 【online】下拉搜索+带条件表字典,报错400 2025-07-22 09:50:22 +08:00
3c0cc49f0c 【issues/8317】菜单管理页首页国际化报错 --- 2025-07-19 09:34:17 +08:00
69b2e97935 【issues/8552】useScript的isLoading默认值应该是true 2025-07-19 09:30:23 +08:00
1c2a49d371 【issues/8564】basicTale的TableLayout换成auto不生效 2025-07-19 09:29:37 +08:00
967197d224 有数十个字段时只展示2个字段,其余字段为ifShow:false会有滚动条 2025-07-19 09:28:16 +08:00
396718bc5e 解决问题:[issues/8558]批量删除优化 2025-07-17 15:07:33 +08:00
37c62c3962 启动democloud服务时出现循环依赖报错 #8573 2025-07-11 10:44:23 +08:00
f510578cb7 启动democloud服务时出现循环依赖报错 #8573 2025-07-11 10:35:17 +08:00
96b378bb7a AI视频介绍 2025-07-09 09:49:56 +08:00
2d7c51eadc 【合并升级v3.8.1】
Merge remote-tracking branch 'origin/master' into springboot3

# Conflicts:
#	jeecg-boot/README.md
#	jeecg-boot/db/tables_nacos.sql
#	jeecg-boot/jeecg-boot-base-core/pom.xml
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/encryption/AesEncryptUtil.java
#	jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java
#	jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/pom.xml
#	jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/java/org/jeecg/modules/airag/app/controller/AiragAppController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/monitor/actuator/httptrace/CustomInMemoryHttpTraceRepository.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/openapi/service/impl/OpenApiPermissionServiceImpl.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleIndexController.java
#	jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/ISysUserService.java
#	jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml
#	jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
#	jeecg-boot/jeecg-server-cloud/jeecg-system-cloud-start/src/main/java/org/jeecg/JeecgSystemCloudApplication.java
#	jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel/pom.xml
#	jeecg-boot/pom.xml
#	jeecgboot-vue3/pnpm-lock.yaml
2025-07-08 16:33:51 +08:00
9d440a4261 升级积木报表到最新版 v2.1.0 2025-07-08 14:02:50 +08:00
4870c43f39 解决【issues/8527】apiSelect分页加载重复请求 2025-07-08 13:44:47 +08:00
9c21f621c0 开源协议 2025-07-08 11:17:34 +08:00
8d382b76ad 开源协议说明 2025-07-08 11:13:57 +08:00
410f2539e1 更新地址 2025-07-08 10:01:02 +08:00
baee00921f 修复问题:低代码开发Online表单开发主子表ERP显示问题 #8532 2025-07-07 14:51:33 +08:00
216d4b9a1f fix: 解决 xxljob 因 factoryBeanObjectType 导致的启动失败 2025-07-06 03:10:33 +08:00
e00b25af42 增加系统功能架构图 2025-07-02 18:57:50 +08:00
44d6b37873 增加博客地址 2025-07-02 16:53:06 +08:00
271712b050 增加博客地址 2025-07-02 16:51:53 +08:00
3482b3a0db 更新项目介绍 2025-07-02 10:29:22 +08:00
10bf5cf127 V3.8.1_2__openapi.sql数据重复 #8531 2025-07-02 10:13:41 +08:00
6cc74699aa 完善项目介绍 2025-07-02 09:57:15 +08:00
0e8fdc2c5a 完善项目介绍 2025-07-02 09:29:45 +08:00
bd6051d972 完善项目介绍 2025-07-02 09:24:30 +08:00
9cfb9dd2b7 完善项目介绍 2025-07-02 09:22:03 +08:00
87b41ab36f 完善项目介绍 2025-07-01 20:26:43 +08:00
378f42bcb7 完善项目介绍 2025-07-01 20:16:38 +08:00
6838888f5f 完善项目介绍 2025-07-01 20:06:27 +08:00
bb8281107e 完善项目介绍 2025-07-01 19:39:35 +08:00
e9d44bfc80 完善项目介绍 2025-07-01 18:53:40 +08:00
8057172f8c 精简项目介绍 2025-07-01 18:45:13 +08:00
c6dd83a262 精简项目介绍 2025-07-01 18:40:42 +08:00
7469474124 项目版本介绍 2025-07-01 18:29:53 +08:00
dc9f36905d 项目版本介绍 2025-07-01 18:23:33 +08:00
30486c5358 项目版本介绍 2025-07-01 18:20:42 +08:00
5b7a90c8bc 项目版本介绍 2025-07-01 15:30:27 +08:00
ac40596ad6 项目版本介绍 2025-07-01 15:25:32 +08:00
6e0edc093b 项目版本介绍 2025-07-01 15:11:14 +08:00
2831996dd4 项目版本介绍 2025-07-01 13:59:06 +08:00
130793fffe 分支说明 2025-07-01 13:54:14 +08:00
5cabfe1655 分支说明 2025-07-01 13:53:02 +08:00
e99e29a523 分支说明 2025-07-01 13:49:35 +08:00
d64e8b0adb 代码生成器模板小优化:生成的列表默认吸底、代码生成年、月、日存储格式修改 2025-07-01 10:15:32 +08:00
4108ba54c3 代码严谨处理:解决部分用户反馈Cannot invoke "org.jeecg.common.api.CommonAPI.queryUserRolesById(String)" because "this.commonAPI" is null 2025-07-01 09:37:57 +08:00
e8ad887096 代码严谨处理:解决部分用户反馈Cannot invoke "org.jeecg.common.api.CommonAPI.queryUserRolesById(String)" because "this.commonAPI" is null 2025-07-01 09:24:04 +08:00
6c24b3cf30 打包electron:build-all 报错少个文件copyChat.ts 2025-06-30 16:02:29 +08:00
208861f808 新增QQ交流群:964611995 2025-06-30 14:24:28 +08:00
f7fe1dfc58 优化初始化脚本jeecgboot-mysql-5.7.sql大小 2025-06-30 09:38:46 +08:00
e2805050cf v3.8.1发布,代码生成支持关联记录组件 2025-06-26 11:50:22 +08:00
b34bb84fc9 v3.8.1发布,更新数据库 2025-06-26 11:30:27 +08:00
11beca16da v3.8.1发布,更新前端依赖 2025-06-26 10:56:18 +08:00
b0c9a2fd9e v3.8.1发布,升级Aiflow后端解决打包过大的问题 2025-06-26 10:56:02 +08:00
6b4e8695ae v3.8.1发布,暂时注释掉报错代码 2025-06-26 10:55:41 +08:00
e8ca74f2ab v3.8.1发布,升级Aiflow前端依赖版本号 2025-06-26 10:55:25 +08:00
eabf98533a v3.8.1发布,上传前端缺少文件 2025-06-26 09:53:12 +08:00
7848881dcb v3.8.1发布,微服务启动system提示mogodb提示错误 2025-06-25 17:51:51 +08:00
553bee57f1 v3.8.1发布,修改版本号 2025-06-25 17:37:33 +08:00
b751570789 v3.8.1发布,上传新版数据库脚本 2025-06-25 16:55:31 +08:00
e45d29c4ff v3.8.1发布,上传前端代码(暂时注释仪表盘缓存刷新功能) 2025-06-25 16:44:00 +08:00
8c8a448a71 v3.8.1发布,上传后台java代码 2025-06-25 16:34:26 +08:00
0148f45979 v3.8.1发布,上传前端代码 2025-06-25 16:04:02 +08:00
3d414aaec8 v3.8.1发布,上传代码 2025-06-25 15:16:00 +08:00
7a4c66af57 v3.8.1发布,上传代码 2025-06-25 13:34:13 +08:00
444c7140f6 解决TableAction中自定义图标颜色不起作用的问题 2025-06-25 09:55:07 +08:00
0fda30b5d2 修改接口地址,验证码404 2025-06-16 15:00:39 +08:00
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
5972c74b43 解决积木报表springboot3 找不到类base64utils #3834和SqlServer兼容问题 2025-06-05 15:51:05 +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
715 changed files with 138829 additions and 9397 deletions

View File

@ -10,6 +10,9 @@ assignees: getActivity
##### 版本号:
##### 分支:
##### 问题描述:

View File

@ -6,10 +6,12 @@ assignees: getActivity
---
##### 版本号:
##### 分支:
##### 问题描述:

2
.gitignore vendored
View File

@ -13,3 +13,5 @@ os_del.cmd
os_del_doc.cmd
.svn
derby.log
.cursor
.history

View File

@ -16,6 +16,19 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单字段建议表单设计器
#### Dify `VS` JEECG AI
@ -44,10 +57,10 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
### 安装向量库 pgvector
- https://help.jeecg.com/aigc/config
### 技术文档
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [安装向量库 pgvector](https://help.jeecg.com/aigc/config)
@ -73,7 +86,7 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
## 技术交流
- 开发文档https://help.jeecg.com/aigc
- QQ群716488839
- QQ群964611995、716488839(满)
## 功能列表

View File

@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.8.0 (Release date: 2025-04-18)
Current version: 3.8.3 (Release date: 2025-10-09)
[![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.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.2-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)
@ -69,9 +69,13 @@ Jeecg-Boot AI low code platform can be applied in the development of any J2EE pr
Starts the project
-----------------------------------
- [IDEA Quick start](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start](https://help.jeecg.com/java/docker/quick)
> Default account password admin/123456
- [Development Environment setup](https://help.jeecg.com/java/setup/tools)
- [IDEA Quick start(single model)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker Quick start(single model)](https://help.jeecg.com/java/docker/quick)
- [IDEA Quick start(microservices model)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker Quick start(microservices model)](https://help.jeecg.com/java/docker/quickcloud)
Technical documentation
@ -81,7 +85,7 @@ Technical documentation
- Demo [OnlineDemo](http://boot3.jeecg.com) | [APP](http://jeecg.com/appIndex)
- Doc [DocumentCenter](http://help.jeecg.com) | [AI Config](https://help.jeecg.com/java/ai/aichat)
- Newbie guide [Quick start](http://www.jeecg.com/doc/quickstart) | [Q&A ](http://www.jeecg.com/doc/qa) | [1 minute experience](https://my.oschina.net/jeecg/blog/3083313)
- QQ group ⑩716488839、⑨808791225
- QQ group 964611995、⑩716488839(满)、⑨808791225(满)

124
README-Enterprise.md Normal file
View File

@ -0,0 +1,124 @@
JeecgBoot低代码平台(商业版介绍)
===============
项目介绍
-----------------------------------
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot是一款集成AI应用的基于BPM流程的低代码平台旨在帮助企业快速实现低代码开发和构建个性化AI应用前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro。强大的代码生成器让前后端代码一键生成无需写任何代码 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作让开发更多关注业务提高效率、节省成本同时又不失灵活性低代码能力Online表单、表单设计、流程设计、Online报表、大屏/仪表盘设计、报表设计; AI应用平台功能AI知识库问答、AI模型管理、AI流程编排、AI聊天等支持含ChatGPT、DeepSeek、Ollama等多种AI大模型
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
#### JeecgBoot商业版与同类产品区别
-----------------------------------
- 灵活性jeecgboot基于开源技术栈设计初考虑到可插拔性和集成灵活性确保平台的智能性与灵活性避免因平台过于庞大而导致的扩展困难。
- 流程管理:支持一个表单挂接多个流程,同时一个流程可以连接多个表单,增强了流程的灵活性和复杂性管理。
- 符合中国国情的流程针对中国市场的特定需求jeecgboot能够实现各种符合中国国情的业务流程。
- 强大的表单设计器jeecgboot的表单设计器与敲敲云共享具备高质量和智能化的特点能够满足零代码应用的需求业内同类产品中不多见。
- 报表功能:自主研发的报表工具,拥有独立知识产权,功能上比业内老牌产品如帆软更智能,操作简便。
- BI产品整合提供大屏、仪表盘、门户等功能完美解决这些需求并支持移动面板的设计与渲染。
- 自主研发的模块jeecgboot的所有模块均为自主研发具有独立的知识产权。
- 颗粒度和功能细致在功能细致度和颗粒度上jeecgboot远超同类产品尤其在零代码能力方面表现突出。
- 零代码应用管理最新版支持与敲敲云的零代码应用管理能力的集成使得jeecgboot既具备低代码又具备零代码的应用能力业内独一无二。
- 强大的代码生成器作为开源代码生成器的先锋jeecgboot在代码生成的智能化和在线低代码与代码生成的结合方面优势明显。
- 精细化权限管理提供行级和列级的数据权限控制满足企业在ERP和OA领域对权限管理的严格需求。
- 多平台支持的APP目前采用uniapp3实现支持小程序、H5、App及鸿蒙、鸿蒙Next、Electron桌面应用等多种终端。
> 综上所述jeecgboot不仅在功能上具备丰富性和灵活性还在技术架构、权限管理和用户体验等方面展现出明显的优势是一个综合性能强大的低代码平台。
商业版演示
-----------------------------------
JeecgBoot vs 敲敲云
> - JeecgBoot是低代码产品拥有系列低代码能力比如流程设计、表单设计、大屏设计代码生成器适合半开发模式开发+低代码结合),也可以集成零代码应用管理模块.
> - 敲敲云是零代码产品完全不写代码通过配置搭建业务系统其在jeecgboot基础上研发而成删除了online、代码生成、OA等需要编码功能只保留应用管理功能和聊天、日程、文件三个OA组件.
- JeecgBoot低代码 https://boot3.jeecg.com
- 敲敲云零代码https://app.qiaoqiaoyun.com
- APP演示(多端): http://jeecg.com/appIndex
### 流程视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
### 商业版功能简述
> 详细的功能介绍,[请联系官方](https://jeecg.com/vip)
```
│─更多商业功能
│ ├─流程设计器
│ ├─简流设计器(类钉钉版)
│ ├─门户设计NEW
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─零代码应用管理(无需编码,在线搭建应用系统)
│ ├─积木报表企业版含jimureport、jimubi
│ ├─AI流程设计器源码
│ ├─Online全模块功能和源码
│ ├─AI写文章CMS
│ ├─AI表单字段建议表单设计器
│ ├─OA办公协同组件
│ ├─在线聊天功能
│ ├─设计表单移动适配
│ ├─设计表单支持外部填报
│ ├─设计表单AI字段建议
│ ├─设计表单视图功能(支持多种类型含日历、表格、看板、甘特图)
│ └─。。。
```
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

454
README.md
View File

@ -2,12 +2,13 @@
JeecgBoot AI低代码平台
===============
当前最新版本: 3.8.0发布日期2025-04-18
当前最新版本: 3.8.3发布日期2025-10-09
[![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.8.0-brightgreen.svg)](https://github.com/jeecgboot/JeecgBoot)
[![](https://img.shields.io/badge/Author-北京国炬软件-orange.svg)](https://jeecg.com)
[![](https://img.shields.io/badge/blog-技术博客-orange.svg)](https://jeecg.blog.csdn.net)
[![](https://img.shields.io/badge/version-3.8.3-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,177 +17,176 @@ JeecgBoot AI低代码平台
项目介绍
-----------------------------------
<h3 align="center">Java AI Low Code Platform</h3>
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot是一款基于AIGC和低代码引擎的AI低代码平台旨在帮助开发者快速实现低代码开发和构建、部署个性化的 AI 应用。
前后端分离架构Ant Design&Vue3SpringBootSpringCloud AlibabaMybatis-plusShiro,强大代码生成器前后端代码一键生成,无需写任何代码!
成套AI大模型功能: AI模型管理、AI应用、知识库、AI流程编排、AI对话助手等
引领AI低代码开发模式: AIGC生成->OnlineCoding-> 代码生成-> 手工MERGE 帮助Java项目解决80%的重复工作,让开发更多关注业务,快速提高效率 节省成本,同时又不失灵活性!
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus,强大代码生成器实现前后端一键生成,无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在线开发Online表单开发、Online报表、复杂报表设计、打印设计、在线图表设计、仪表盘设计、大屏设计、移动图表能力、表单设计器、在线设计流程、流程自动化配置、插件能力可插拔
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`AI赋能低代码:` 目前提供了AI应用、AI模型管理、AI流程编排、AI对话助手AI建表、AI写文章、AI知识库问答、AI字段建议等功能;支持各种AI大模型ChatGPT、DeepSeek、Ollama、智普、千问等.
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`JEECG宗旨是:` 简单功能由OnlineCoding配置实现做到`零代码开发`复杂功能由代码生成器生成进行手工Merge 实现`低代码开发`,既保证了`智能`又兼顾`灵活`;实现了低代码开发的同时又支持灵活编码,解决了当前低代码产品普遍不灵活的弊端!
`JEECG业务流程:` 采用工作流来实现、扩展出任务接口,供开发编写业务逻辑,表单提供多种解决方案: 表单设计器、online配置表单、编码表单。同时实现了流程与表单的分离设计松耦合、并支持任务节点灵活配置既保证了公司流程的保密性又减少了开发人员的工作量。
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
### 视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/flow_video.png)](https://www.bilibili.com/video/BV1Nk4y1o7Qc)
适用项目
-----------------------------------
JeecgBoot AI低代码平台,可以应用在任何J2EE项目开发,支持信创国产化。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM等,其半智能手工Merge开发式,可显著提高开发效率70%以上,极大降低开发成本
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化,特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM及AI知识库等场景。其半智能手工Merge开发式,可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。
信创国产化
-----------------------------------
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
**信创兼容说明**
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
- 数据库达梦、人大金仓、TiDB
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
通过这些适配JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
项目说明
版本说明
-----------------------------------
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+ts最新技术栈 |
| `JeecgUniapp` | [配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5 |
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|------|----------------|----------------------------|-------------------|--------------------------------------------|
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`master`](https://github.com/jeecgboot/JeecgBoot) 分支|
| Gitee | [`springboot3`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3/) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba支持单体和微服务切换.
- `jeecgboot-vue3` 是前端VUE3源码项目vue3+vite6+ts最新技术栈.
- `JeecgUniapp` 是[配套APP框架](https://github.com/jeecgboot/JeecgUniapp) 适配多个终端支持APP、小程序、H5、鸿蒙、鸿蒙Next.
- `jeecg-boot-starter` 是[jeecg-boot对应的底层封装starter](https://github.com/jeecgboot/jeecg-boot-starter) 微服务启动、xxljob、分布式锁starter、rabbitmq、分布式事务、分库分表shardingsphere等.
- 参考 [文档](https://help.jeecg.com/ui/2dev/mini) 可以删除不需要的demo制作一个精简版本
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
> 默认账号密码: admin/123456
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
AIGC应用平台介绍
技术文档
-----------------------------------
JeecgBoot 平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
- [AIGC专题介绍页](README-AI.md)
- [AIGC开发文档](https://help.jeecg.com/aigc)
- [配置向量库PGVector](https://help.jeecg.com/aigc/config)
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- AI编程实战视频 [JEECG低代码与Cursor+GitHub Copilot实现AI高效编程实战](https://www.bilibili.com/video/BV11XyaBVEoH)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
##### AI视频介绍
AI 应用平台介绍
-----------------------------------
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
- [详细专题介绍,请点击查看](README-AI.md)
- AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### 在线体验
- JeecgBoot演示 https://boot3.jeecg.com
- 敲敲云在线搭建AI知识库https://app.qiaoqiaoyun.com
##### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
> - Markdown文档库导入
> JEECG AI允许用户直接导入整个Markdown文档库这不仅保留markdown格式还支持图片的导入确保文档内容的完整性和可视化效果
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这个功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
##### 功能大模块
- AI应用开发平台
- AI知识库系统
- AI大模型管理
- AI流程编排
- AI对话支持图片
- AI对话助手(智能问答)
- AI建表Online表单
- AI写文章CMS
- AI表单字段建议表单设计器
##### AI大模型支持
| AI大模型 | 支持 |
| --- | --- |
| DeepSeek | √ |
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| Ollama本地模型 | √ |
| 等等。。 | √ |
为什么选择JeecgBoot?
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
- 7.低代码能力在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码在线配置零代码开发、所见即所得支持23种类控件。
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)
- 9.Online支持在线增强开发提供在线代码编辑器支持代码高亮、代码提示等功能支持多种语言Java、SQL、JavaScript等
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
- 11.前端UI提供丰富的组件库支持各种常用组件如表格、树形控件、下拉框、日期选择器等满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
- 12.提供APP配套框架一份多代码多终端适配一份代码多终端适配小程序、H5、安卓、iOS、鸿蒙Next。
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件。
- 14.提供了一套成熟的AI应用平台功能从AI模型、知识库到AI应用搭建助力企业快速落地AI服务加速智能化升级。
- 15.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
- 16.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能。
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
- 18.常用共通封装各种工具类定时任务、短信接口、邮件发送、Excel导入导出等基本满足80%项目需求。
- 19.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能。
- 20.集成智能报表工具报表打印、图像报表和数据导出非常方便可极其方便地生成PDF、Excel、Word等报表。
- 21.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件、编辑器等。
- 22.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询)。
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
- 24.接口安全机制可细化控制接口授权非常简便实现不同客户端只看自己数据等控制也提供了基于AK和SK认证鉴权的OpenAPI功能。
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
- 26.权限控制采用RBACRole-Based Access Control基于角色的访问控制
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
- 28.支持SaaS服务模式提供SaaS多租户架构方案。
- 29.分布式文件服务集成MinIO、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
- 30.主流数据库兼容一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
- 31.集成工作流Flowable并实现了只需在页面配置流程转向可极大简化BPM工作流的开发用BPM的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的Java代码。
- 32.低代码能力在线流程设计采用开源Flowable流程引擎实现在线画流程、自定义表单、表单挂靠、业务流转。
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
- 34.提供单点登录CAS集成方案项目中已经提供完善的对接代码。
- 35.低代码能力表单设计器支持用户自定义表单布局支持单表、一对多表单支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
- 36.专业接口对接机制统一采用RESTful接口方式集成Swagger-UI在线接口文档JWT token安全验证方便客户端对接。
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
- 38.提供各种系统监控实时跟踪系统运行情况监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控
- 39.消息中心支持短信、邮件、微信推送等集成WebSocket消息通知机制。
- 40.支持多语言,提供国际化方案。
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
- 42.提供简单易用的打印插件支持谷歌、火狐、IE11+等各种浏览器。
- 43.后端采用Maven分模块开发方式前端支持菜单动态路由。
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
技术架构:
-----------------------------------
#### 前端
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
` ( Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 默认jdk17(支持jdk8、jdk21)
- 语言Java 默认jdk17(jdk21、jdk24)
- 依赖管理Maven
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.6.2
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.9.5
- 安全框架Apache Shiro 1.13.0Jwt 4.5.0
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.1.24
- AI大模型支持 `ChatGPT` `DeepSeek`切换
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
- 默认数据库脚本:MySQL5.7+
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
- 默认提供MySQL5.7+数据库脚本
#### 前端
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 依赖管理node、npm、pnpm
#### 前端环境要求
* 本地环境安装 `Node.js 、npm 、pnpm`
* pnpm 要求`9+` 版本以上
* Node.js 版本建议`v20.15.0`,要求`Node 20+` 版本以上
` ( 因为Vite6 需要 Node.js 18 / 20+ )`
#### 平台支持数据库
#### 数据库支持
> jeecgboot平台支持以下数据库默认我们只提供mysql脚本其他数据库可以参考[转库文档](https://my.oschina.net/jeecg/blog/4905722)自己转。
@ -201,12 +201,12 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
| 人大金仓 | √ |
| TiDB | √ |
| kingbase8 | √ |
## 微服务解决方案
- 1、服务注册和发现 Nacos √
- 2、统一配置中心 Nacos √
- 3、路由网关 gateway(三种加载方式) √
@ -224,62 +224,23 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
- 15、CAS 单点登录 √
- 16、路由限流 √
#### 微服务方式启动
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动微服务前后端](https://help.jeecg.com/java/docker/quickcloud)
#### 微服务架构图
![微服务架构图](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecgboot_springcloud2022.png "在这里输入图片标题")
为什么选择JeecgBoot?
开源版与企业版区别?
-----------------------------------
* 1.采用最新主流前后分离框架Springboot+Mybatis+antd+vue3容易上手; 代码生成器依赖性低,灵活的扩展能力,可快速实现二次开发;
* 2.支持微服务SpringCloud Alibaba(Nacos、Gateway、Sentinel、Skywalking),提供切换机制支持单体和微服务自由切换
* 3.开发效率高,采用代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用引入AI能力支持自动建表等功能
* 4.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)
* 5.代码生成器非常智能在线业务建模、在线配置、所见即所得支持23种类控件一键生成前后端代码大幅度提升开发效率不再为重复工作发愁。
* 6.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI建表、AI报表等功能。
* 6.低代码能力Online在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码
* 7.低代码能力Online在线报表、Online在线图表无需编码通过在线配置方式实现数据报表和图形报表可以快速抽取数据减轻开发压力实现人人皆可编码
* 9.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能
* 10.常用共通封装,各种工具类(定时任务,短信接口,邮件发送,Excel导入导出等),基本满足80%项目需求
* 11.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能
* 12.集成简易报表工具图像报表和数据导出非常方便可极其方便的生成图形报表、pdf、excel、word等报表
* 13.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件编辑器等等
* 14.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询);
* 15.数据权限(精细化数据权限控制,控制到行级,列表级,表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段
* 16.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等);
* 17.支持SAAS服务模式提供SaaS多租户架构方案。
* 18.分布式文件服务集成minio、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
* 19.主流数据库兼容一套代码完全兼容Mysql、Postgresql、Oracle、Sqlserver、MariaDB、达梦、人大金仓等主流数据库。
* 20.集成工作流flowable并实现了只需在页面配置流程转向可极大的简化bpm工作流的开发用bpm的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的java代码
* 21.低代码能力在线流程设计采用开源flowable流程引擎实现在线画流程,自定义表单,表单挂靠,业务流转
* 22.多数据源:及其简易的使用方式,在线配置数据源配置,便捷的从其他数据抓取数据;
* 23.提供单点登录CAS集成方案项目中已经提供完善的对接代码
* 24.低代码能力表单设计器支持用户自定义表单布局支持单表一对多表单、支持select、radio、checkbox、textarea、date、popup、列表、宏等控件
* 25.专业接口对接机制统一采用restful接口方式集成swagger-ui在线接口文档Jwt token安全验证方便客户端对接
* 26.接口安全机制,可细化控制接口授权,非常简便实现不同客户端只看自己数据等控制
* 27.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史
* 28.提供各种系统监控,实时跟踪系统运行情况(监控 Redis、Tomcat、jvm、服务器信息、请求追踪、SQL监控
* 29.消息中心(支持短信、邮件、微信推送等等)
* 30.集成Websocket消息通知机制
* 31.移动自适应效果优秀提供APP发布方案
* 32.支持多语言,提供国际化方案;
* 33.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化
* 34.平台UI强大实现了移动自适应
* 35.平台首页风格,提供多种组合模式,支持自定义风格
* 36.提供简单易用的打印插件支持谷歌、火狐、IE11+ 等各种浏览器
* 37.示例代码丰富,提供很多学习案例参考
* 38.采用maven分模块开发方式
* 39.支持菜单动态路由
* 40.权限控制采用 RBACRole-Based Access Control基于角色的访问控制
* 41.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能
* 42.提供仪表盘设计器,类大屏设计支持移动端,免费的数据可视化设计工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作图表和门户设计;目前支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等等;
- JeecgBoot开源版采用 [Apache-2.0 license](LICENSE) 协议附加补充条款:允许商用使用,不会造成侵权行为,允许基于本平台软件开展业务系统开发(但在任何情况下,您不得使用本软件开发可能被认为与本软件竞争的软件).
- 商业版与开源版主要区别在于商业版提供了技术支持 和 更多的企业级功能(例如Online图表、流程监控、流程设计、流程审批、表单设计器、表单视图、积木报表企业版、OA办公、商业APP、零代码应用、Online模块源码等功能). [更多商业功能介绍,点击查看](README-Enterprise.md)
- JeecgBoot未来发展方向是零代码平台的建设也就是团队的另外一款产品 [敲敲云零代码](https://www.qiaoqiaoyun.com) 无需编码即可通过拖拽快速搭建企业级应用与JeecgBoot低代码平台形成互补满足从简单业务到复杂系统的全场景开发需求目前已经开源[欢迎下载](https://qiaoqiaoyun.com/downloadCode)
### Jeecg Boot 产品功能蓝图
@ -287,47 +248,19 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
#### 系统功能架构图
### 分支说明
![](https://oscimg.oschina.net/oscnet/up-1569487b95a07dbc3599fb1349a2e3aaae1.png)
> 主干master更稳定如果你对最新技术栈无要求建议采用主干
#### springboot3分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3
- 架构说明升级Spring Boot3 & JDK 17 + Undertow + springdoc + fastjson2
#### springboot3_sas分支
- 源码地址https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas
- 架构说明在springboot3分支基础上采用SpringAuthorizationServer替换Shiro
### 功能模块
### 开源版功能清单
```
├─AI开发
│ ├─支持AI大模型ChatGPT和DeepSeek
│ ├─AI对话助手
│ ├─AI建表
│ ├─AI写文章
│ ├─AI流程编排
│ ├─AI知识库问答系统
│ ├─AI应用开发平台
│ ├─AI聊天窗口支持嵌入第三方
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
├─积木报表设计器
│ ├─打印设计器
│ ├─数据报表设计
│ ├─图形报表设计支持echart
├─系统管理
│ ├─用户管理
│ ├─角色管理
│ ├─菜单管理
│ ├─首页配置
│ ├─权限设置(支持按钮权限、数据权限)
│ ├─表单权限(控制字段禁用、隐藏)
│ ├─部门管理
@ -338,7 +271,36 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
│ └─职务管理
│ └─通讯录
│ ├─多数据源管理
└─多租户管理(租户管理、租户角色、我的租户)
├─白名单管理
│ ├─第三方配置(对接钉钉和企业微信)
│ └─多租户管理(租户管理、租户角色、我的租户、租户默认套餐管理)
├─Online在线开发(低代码)
│ ├─Online在线表单
│ ├─Online代码生成器
│ ├─Online在线报表
│ ├─仪表盘设计器
│ ├─系统编码规则
│ ├─系统校验规则
│ ├─APP版本管理
├─AI应用平台
│ ├─AI知识库问答系统
│ ├─AI大模型管理
│ ├─AI流程编排
│ ├─AI流程设计器
│ ├─AI对话支持图片
│ ├─AI对话助手(智能问答)
│ ├─AI建表Online表单
│ ├─AI聊天窗口支持嵌入第三方
│ ├─AI聊天窗口支持移动端
│ ├─支持常见大模型ChatGPT和DeepSeek、ollama等等
│ ├─AI OCR示例
├─数据可视化
│ ├─报表设计器(支持打印设计)
│ ├─大屏设和仪表盘设计
├─OpenAPI基于AK和SK认证鉴权
│ ├─接口管理
│ ├─接口授权
│ ├─接口文档
├─消息中心
│ ├─消息管理
│ ├─模板管理
@ -350,8 +312,12 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
│ ├─高级查询器(弹窗自动组合查询条件)
│ ├─Excel导入导出工具集成支持单表一对多 导入导出)
│ ├─平台移动自适应支持
│ ├─提供新版uniapp3的代码生成器模板
├─系统监控
│ ├─Gateway路由网关
│ ├─基于AK和SK认证鉴权OpenAPI功能
│ ├─定时任务
│ ├─数据源管理
│ ├─性能扫描监控
│ │ ├─监控 Redis
│ │ ├─Tomcat
@ -359,13 +325,11 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
│ │ ├─服务器信息
│ │ ├─请求追踪
│ │ ├─磁盘监控
│ ├─定时任务
│ ├─系统日志
│ ├─消息中心(支持短信、邮件、微信推送等等)
│ ├─数据日志(记录数据快照,可对比快照,查看数据变更情况)
│ ├─系统通知
│ ├─SQL监控
│ ├─swagger-ui(在线接口文档)
│ ├─在线用户
│─报表示例
│ ├─曲线图
│ └─饼状图
@ -430,46 +394,16 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
│ ├─提供单点登录CAS集成方案
│ ├─提供APP发布方案
│ ├─集成Websocket消息通知机制
─更多商业功能
│ ├─流程设计器
│ ├─表单设计器
│ ├─大屏设计器
│ └─我的任务
│ └─历史流程
│ └─历史流程
│ └─流程实例管理
│ └─流程监听管理
│ └─流程表达式
│ └─我发起的流程
│ └─我的抄送
│ └─流程委派、抄送、跳转
│ └─OA办公组件
│ └─。。。
├─支持electron桌面应用打包(支持windows、linux、macOS三大平台)
│ ├─docker容器支持
│ ├─提供移动APP框架及源码Uniapp3版本支持H5、小程序、APP、鸿蒙Next
│ ├─提供移动APP低代码设计(Online表单、仪表盘)
```
### 系统效果
##### 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)
@ -489,6 +423,22 @@ AI写文章
![](https://oscimg.oschina.net/oscnet/up-16c07e000278329b69b228ae3189814b8e9.png)
##### 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)
##### 仪表盘设计器
@ -555,28 +505,6 @@ AI写文章
![](https://oscimg.oschina.net/oscnet/up-6b81781b43086819049c4421206810667c5.png)
##### 流程设计
![](https://oscimg.oschina.net/oscnet/up-981ce174e4fbb48c8a2ce4ccfd7372e2994.png)
![](https://oscimg.oschina.net/oscnet/up-1dc0d052149ec675f3e4fad632b82b48add.png)
![](https://oscimg.oschina.net/oscnet/up-de31bc2f9d9b8332c554b0954cc73d79593.png)
![输入图片说明](https://static.oschina.net/uploads/img/201907/05165142_yyQ7.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160917_9Ftz.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201904/14160633_u59G.png "在这里输入图片标题")
##### 表单设计器
![](https://oscimg.oschina.net/oscnet/up-5f8cb657615714b02190b355e59f60c5937.png)
![](https://oscimg.oschina.net/oscnet/up-d9659b2f324e33218476ec98c9b400e6508.png)
![](https://oscimg.oschina.net/oscnet/up-4868615395272d3206dbb960ade02dbc291.png)

216
check_jeecgenv.py Normal file
View File

@ -0,0 +1,216 @@
import os
import subprocess
import re
import sys
from typing import Tuple, Optional
def run_command(cmd: str) -> Tuple[int, str]:
"""执行命令并返回退出码和输出"""
try:
result = subprocess.run(cmd, shell=True, check=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True)
return result.returncode, result.stdout.strip()
except Exception as e:
return -1, str(e)
def check_java() -> bool:
"""检查JDK 17+是否安装"""
print("\n检查JDK 17+...")
rc, output = run_command("java -version 2>&1")
if rc != 0:
print("❌ 未检测到Java请安装JDK 17+")
return False
version_pattern = r'"(\d+)(?:\.\d+)*(?:_\d+)?'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Java版本")
return False
version = int(match.group(1))
if version >= 17:
print(f"✅ JDK版本 {version} (满足17+要求)")
return True
else:
print(f"❌ JDK版本 {version} (需要17+)")
return False
def check_maven() -> bool:
"""检查Maven是否安装"""
print("\n检查Maven...")
rc, output = run_command("mvn -v")
if rc == 0:
print("✅ Maven已安装")
return True
else:
print("❌ Maven未安装")
return False
def check_node() -> bool:
"""检查Node.js 20+是否安装"""
print("\n检查Node.js 20+...")
rc, output = run_command("node -v")
if rc != 0:
print("❌ Node.js未安装")
return False
version_pattern = r'v(\d+)\.\d+\.\d+'
match = re.search(version_pattern, output)
if not match:
print("❌ 无法解析Node.js版本")
return False
version = int(match.group(1))
if version >= 20:
print(f"✅ Node.js版本 {version} (满足20+要求)")
return True
else:
print(f"❌ Node.js版本 {version} (需要20+)")
return False
def check_pnpm() -> bool:
"""检查PNPM 9+是否安装"""
print("\n检查PNPM 9+...")
rc, output = run_command("pnpm -v")
if rc != 0:
print("❌ PNPM未安装")
return False
try:
# 处理可能的版本号格式v9.0.0 或 9.0.0 或 9
version_str = output.strip().lstrip('v').split('.')[0]
version = int(version_str)
if version >= 9:
print(f"✅ PNPM版本 {output.strip()} (满足9+要求)")
return True
else:
print(f"❌ PNPM版本 {output.strip()} (需要9+)")
return False
except (ValueError, IndexError):
print(f"❌ 无法解析PNPM版本: {output.strip()}")
return False
def check_redis_connection() -> bool:
"""检查Redis连接"""
print("\n检查Redis连接...")
print("⚠️ 请确保已配置Redis连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def check_mysql_connection() -> bool:
"""检查MySQL连接"""
print("\n检查MySQL连接...")
print("⚠️ 请确保已配置MySQL连接信息并在jeecg-boot项目中正确配置")
print("⚠️ 此检查需要根据实际项目配置进行验证")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
return True
def print_mysql_config():
"""打印MySQL配置并提示需要修改的位置"""
print("\nMySQL配置参考 (请检查以下配置是否正确):")
print("""
spring.datasource.dynamic.datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root # ← 可能需要修改
password: root # ← 可能需要修改
driver-class-name: com.mysql.cj.jdbc.Driver
""")
def check_ai_vector_db() -> bool:
"""检查AI向量库(pgvector)配置"""
print("\n检查AI知识库向量库配置...")
print("⚠️ 如果需要使用AI知识库功能请配置pgvector向量库")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg.ai-rag:
embed-store:
host: 127.0.0.1 # ← 可能需要修改
port: 5432 # ← 可能需要修改
database: postgres # ← 可能需要修改
user: postgres # ← 可能需要修改
password: postgres # ← 可能需要修改
table: embeddings # ← 可能需要修改
""")
print("⚠️ 注意: 请确保已安装PostgreSQL并添加pgvector扩展docker安装参考https://help.jeecg.com/aigc/config")
return True
def check_ai_config() -> bool:
"""检查AI账号配置"""
print("\n检查AI功能配置...")
print("⚠️ 如果需要使用AI聊天功能请配置AI账号信息")
print("⚠️ 配置文件位置: jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml")
print("\n配置参考:")
print("""
jeecg:
# AI集成
ai-chat:
enabled: true # ← 启用AI功能
model: deepseek-chat # ← 模型名称
apiKey: ?? # ← 必须修改为您的API Key
apiHost: https://api.deepseek.com/v1 # ← API地址
timeout: 60 # ← 超时时间(秒)
""")
print("⚠️ 注意: 请确保已获取有效的API Key并正确配置AI账号注册获取参考 https://help.jeecg.com/java/deepSeekSupport")
return True
def print_redis_config():
"""打印Redis配置并提示需要修改的位置"""
print("\nRedis配置参考 (请检查以下配置是否正确):")
print("""
spring.redis:
database: 0
host: 127.0.0.1 # ← 可能需要修改
port: 6379 # ← 可能需要修改
password: '' # ← 如果需要密码请修改
""")
def main():
print("="*50)
print("JeecgBoot 运行环境检查脚本")
print("="*50)
all_checks_passed = True
# 检查各项依赖
if not check_java():
all_checks_passed = False
if not check_maven():
all_checks_passed = False
if not check_node():
all_checks_passed = False
if not check_pnpm():
all_checks_passed = False
# 数据库提示
print("="*50)
check_redis_connection()
print_redis_config()
print("="*50)
check_mysql_connection()
print_mysql_config()
print("="*50)
check_ai_config()
print("="*50)
check_ai_vector_db()
print("\n" + "="*50)
if all_checks_passed:
print("✅ 所有基础环境检查通过")
print("⚠️ 注意: 请确保Redis和MySQL、AI账号、向量库pgvector 已正确配置并连接成功")
else:
print("❌ 部分环境检查未通过,请根据上述提示解决问题")
print("="*50)
if __name__ == "__main__":
main()
input("\n按回车键退出...") # 等待用户输入

View File

@ -18,20 +18,33 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 3306:3306
- 13306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
ports:
- 6379:6379
# ports:
# - 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-nacos:
restart: always
build:
@ -96,31 +109,37 @@ services:
# environment:
# RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest
# jeecg-boot-sentinel:
# restart: on-failure
# build:
# context: ./jeecg-visual/jeecg-cloud-sentinel
# ports:
# - 9000:9000
# depends_on:
# - jeecg-boot-nacos
# - jeecg-boot-demo
# - jeecg-boot-system
# - jeecg-boot-gateway
# container_name: jeecg-boot-sentinel
# hostname: jeecg-boot-sentinel
#
# jeecg-boot-xxljob:
# build:
# context: ./jeecg-visual/jeecg-cloud-xxljob
# ports:
# - 9080:9080
# container_name: jeecg-boot-xxljob
# hostname: jeecg-boot-xxljob
jeecg-boot-sentinel:
restart: on-failure
build:
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-sentinel
ports:
- 9000:9000
depends_on:
- jeecg-boot-nacos
- jeecg-boot-demo
- jeecg-boot-system
- jeecg-boot-gateway
container_name: jeecg-boot-sentinel
hostname: jeecg-boot-sentinel
networks:
- jeecg-boot
jeecg-boot-xxljob:
build:
context: ./jeecg-boot/jeecg-server-cloud/jeecg-visual/jeecg-cloud-xxljob
ports:
- 9080:9080
container_name: jeecg-boot-xxljob
hostname: jeecg-boot-xxljob
networks:
- jeecg-boot
jeecg-vue:
build:
context: ./jeecgboot-vue3
dockerfile: Dockerfile.cloud
container_name: jeecgboot-vue3-nginx
image: jeecgboot-vue3
depends_on:

View File

@ -18,20 +18,33 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 3306:3306
- 13306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
ports:
- 6379:6379
# ports:
# - 3792:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
ports:
- 5432:5432
restart: always
networks:
- jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-boot/jeecg-module-system/jeecg-system-start

View File

@ -2,12 +2,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.8.0发布日期2025-05-16
当前最新版本: 3.8.3发布日期2025-10-09
[![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.8.0-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.8.3-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)
@ -16,43 +16,127 @@ JeecgBoot 低代码开发平台
项目介绍
-----------------------------------
<h3 align="center">Java Low Code Platform for Enterprise web applications</h3>
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款基于BPM流程和代码生成的AI低代码平台助力企业快速实现低代码开发和构建AI应用。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
`JEECG业务流程:` JEECG业务流程采用BPM工作流引擎实现业务审批扩展任务接口供开发人员编写业务逻辑表单提供表单设计器、在线配置表单和编码表单等多种解决方案。通过流程与表单的分离设计松耦合及任务节点的灵活配置既保障了企业流程的安全性与保密性又大幅降低了开发人员的工作量。
适用项目
-----------------------------------
JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化特别适用于SAAS、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM及AI知识库等场景。其半智能手工Merge开发模式可显著提升70%以上的开发效率极大降低开发成本。同时JeecgBoot还是一款全栈式AI开发平台助力企业快速构建和部署个性化AI应用。。
**信创兼容说明**
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端分离架构 SpringBoot2.x和3.xSpringCloudAnt Design Vue3Mybatis-plusShiroJWT支持微服务。强大的代码生成器让前后端代码一键生成实现低代码开发! JeecgBoot 引领新的低代码开发模式(OnlineCoding-> 代码生成器-> 手工MERGE) 帮助解决Java项目70%的重复工作,让开发更多关注业务。既能快速提高效率,节省研发成本,同时又不失灵活性!
#### 项目说明
| 项目名 | 说明 |
|--------------------|------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot微服务架构 |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite5+ts最新技术栈 |
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart)
- QQ交流群 ⑩716488839、⑨808791225、其他(满)
- 在线演示 [在线演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex)
> 演示系统的登录账号密码,请点击 [获取账号密码](http://jeecg.com/doc/demo) 获取
| 项目名 | 说明 |
|--------------------|------------------------------------|
| `jeecg-boot` | 后端源码JAVASpringBoot3微服务架构) |
| `jeecgboot-vue3` | 前端源码VUE3vue3+vite6+antd4+ts最新技术栈 |
启动项目
-----------------------------------
- [IDEA启动前后端项目](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动前后端](https://help.jeecg.com/java/docker/quick)
> 默认账号密码: admin/123456
- [开发环境搭建](https://help.jeecg.com/java/setup/tools)
- [IDEA启动前后端(单体模式)](https://help.jeecg.com/java/setup/idea/startup)
- [Docker一键启动(单体模式)](https://help.jeecg.com/java/docker/quick)
- [IDEA启动前后端(微服务方式)](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker一键启动(微服务方式)](https://help.jeecg.com/java/docker/quickcloud)
微服务启动
技术文档
-----------------------------------
- [单体快速切换微服务](https://help.jeecg.com/java/springcloud/switchcloud/monomer)
- [Docker启动微服务后台](https://help.jeecg.com/java/docker/springcloud)
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示: [平台演示](https://boot3.jeecg.com) | [APP演示](https://jeecg.com/appIndex)
- 入门指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [代码生成使用](https://help.jeecg.com/java/codegen/online) | [开发文档](https://help.jeecg.com) | [AI应用手册](https://help.jeecg.com/aigc) | [视频教程](http://jeecg.com/doc/video)
- 技术支持: [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md) | [低代码体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- QQ交流群 964611995、⑩716488839(满)、⑨808791225(满)、其他(满)
AI 应用平台介绍
-----------------------------------
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
- [详细专题介绍,请点击查看](README-AI.md)
- AI视频介绍
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
为什么选择JeecgBoot?
-----------------------------------
- 1.采用最新主流前后分离框架Spring Boot3 + MyBatis + Shiro/SpringAuthorizationServer + Ant Design4 + Vue3容易上手代码生成器依赖性低灵活的扩展能力可快速实现二次开发。
- 2.前端大版本换代,最新版采用 Vue3.0 + TypeScript + Vite6 + Ant Design Vue4 等新技术方案。
- 3.支持微服务Spring Cloud AlibabaNacos、Gateway、Sentinel、Skywalking提供简易机制支持单体和微服务自由切换这样可以满足各类项目需求
- 4.开发效率高支持在线建表和AI建表提供强大代码生成器单表、树列表、一对多、一对一等数据模型增删改查功能一键生成菜单配置直接使用。
- 5.代码生成器提供强大模板机制,支持自定义模板,目前提供四套风格模板(单表两套、树模型一套、一对多三套)。
- 6.提供强大的报表和大屏可视化工具,支持丰富的数据源连接,能够通过拖拉拽方式快速制作报表、大屏和门户设计;支持多种图表类型:柱形图、折线图、散点图、饼图、环形图、面积图、漏斗图、进度图、仪表盘、雷达图、地图等。
- 7.低代码能力在线表单无需编码通过在线配置表单实现表单的增删改查支持单表、树、一对多、一对一等模型实现人人皆可编码在线配置零代码开发、所见即所得支持23种类控件。
- 8.低代码能力:在线报表、在线图表(无需编码,通过在线配置方式,实现数据报表和图形报表,可以快速抽取数据,减轻开发压力,实现人人皆可编码)。
- 9.Online支持在线增强开发提供在线代码编辑器支持代码高亮、代码提示等功能支持多种语言Java、SQL、JavaScript等
- 10.封装完善的用户、角色、菜单、组织机构、数据字典、在线定时任务等基础功能,支持访问授权、按钮权限、数据权限等功能。
- 11.前端UI提供丰富的组件库支持各种常用组件如表格、树形控件、下拉框、日期选择器等满足各种复杂的业务需求 [UI组件库文档](https://help.jeecg.com/category/ui%E7%BB%84%E4%BB%B6%E5%BA%93)。
- 12.提供APP配套框架一份多代码多终端适配一份代码多终端适配小程序、H5、安卓、iOS、鸿蒙Next。
- 13.新版APP框架采用Uniapp、Vue3.0、Vite、Wot-design-uni、TypeScript等最新技术栈包括二次封装组件、路由拦截、请求拦截等功能。实现了与JeecgBoot完美对接目前已经实现登录、用户信息、通讯录、公告、移动首页、九宫格、聊天、Online表单、仪表盘等功能提供了丰富的组件。
- 14.提供了一套成熟的AI应用平台功能从AI模型、知识库到AI应用搭建助力企业快速落地AI服务加速智能化升级。
- 15.AI能力目前JeecgBoot支持AI大模型chatgpt和deepseek现在最新版默认使用deepseek速度更快质量更高。目前提供了AI对话助手、AI知识库、AI应用、AI建表、AI报表等功能。
- 16.提供新行编辑表格JVXETable轻松满足各种复杂ERP布局拥有更高的性能、更灵活的扩展、更强大的功能。
- 17.平台首页风格,提供多种组合模式,支持自定义风格;支持门户设计,支持自定义首页。
- 18.常用共通封装各种工具类定时任务、短信接口、邮件发送、Excel导入导出等基本满足80%项目需求。
- 19.简易Excel导入导出支持单表导出和一对多表模式导出生成的代码自带导入导出功能。
- 20.集成智能报表工具报表打印、图像报表和数据导出非常方便可极其方便地生成PDF、Excel、Word等报表。
- 21.采用前后分离技术页面UI风格精美针对常用组件做了封装时间、行表格控件、截取显示控件、报表组件、编辑器等。
- 22.查询过滤器查询功能自动生成后台动态拼SQL追加查询条件支持多种匹配方式全匹配/模糊查询/包含查询/不匹配查询)。
- 23.数据权限(精细化数据权限控制,控制到行级、列表级、表单字段级,实现不同人看不同数据,不同人对同一个页面操作不同字段)。
- 24.接口安全机制可细化控制接口授权非常简便实现不同客户端只看自己数据等控制也提供了基于AK和SK认证鉴权的OpenAPI功能。
- 25.活跃的社区支持;近年来,随着网络威胁的日益增加,团队在安全和漏洞管理方面积累了丰富的经验,能够为企业提供全面的安全解决方案。
- 26.权限控制采用RBACRole-Based Access Control基于角色的访问控制
- 27.页面校验自动生成(必须输入、数字校验、金额校验、时间空间等)。
- 28.支持SaaS服务模式提供SaaS多租户架构方案。
- 29.分布式文件服务集成MinIO、阿里OSS等优秀的第三方提供便捷的文件上传与管理同时也支持本地存储。
- 30.主流数据库兼容一套代码完全兼容MySQL、PostgreSQL、Oracle、SQL Server、MariaDB、达梦、人大金仓等主流数据库。
- 31.集成工作流Flowable并实现了只需在页面配置流程转向可极大简化BPM工作流的开发用BPM的流程设计器画出了流程走向一个工作流基本就完成了只需写很少量的Java代码。
- 32.低代码能力在线流程设计采用开源Flowable流程引擎实现在线画流程、自定义表单、表单挂靠、业务流转。
- 33.多数据源:极其简易的使用方式,在线配置数据源配置,便捷地从其他数据抓取数据。
- 34.提供单点登录CAS集成方案项目中已经提供完善的对接代码。
- 35.低代码能力表单设计器支持用户自定义表单布局支持单表、一对多表单支持select、radio、checkbox、textarea、date、popup、列表、宏等控件。
- 36.专业接口对接机制统一采用RESTful接口方式集成Swagger-UI在线接口文档JWT token安全验证方便客户端对接。
- 37.高级组合查询功能,在线配置支持主子表关联查询,可保存查询历史。
- 38.提供各种系统监控实时跟踪系统运行情况监控Redis、Tomcat、JVM、服务器信息、请求追踪、SQL监控
- 39.消息中心支持短信、邮件、微信推送等集成WebSocket消息通知机制。
- 40.支持多语言,提供国际化方案。
- 41.数据变更记录日志,可记录数据每次变更内容,通过版本对比功能查看历史变化。
- 42.提供简单易用的打印插件支持谷歌、火狐、IE11+等各种浏览器。
- 43.后端采用Maven分模块开发方式前端支持菜单动态路由。
- 44.提供丰富的示例代码,涵盖了常用的业务场景,便于学习和参考。
技术架构:
@ -61,28 +145,33 @@ JeecgBoot 是一款基于代码生成器的`低代码开发平台`!前后端
#### 后端
- IDE建议 IDEA (必须安装lombok插件 )
- 语言Java 8+ (支持17)
- 语言Java 默认jdk17(jdk21、jdk24)
- 依赖管理Maven
- 基础框架Spring Boot 2.7.18
- 微服务框架: Spring Cloud Alibaba 2021.0.1.0
- 持久层框架MybatisPlus 3.5.3.2
- 报表工具: JimuReport 1.9.4
- 安全框架Apache Shiro 1.12.0Jwt 3.11.0
- 基础框架Spring Boot 3.5.5
- 微服务框架: Spring Cloud Alibaba 2023.0.3.3
- 持久层框架MybatisPlus 3.5.12
- 报表工具: JimuReport 2.1.3
- 安全框架Apache Shiro 2.0.4Jwt 4.5.0
- 微服务技术栈Spring Cloud Alibaba、Nacos、Gateway、Sentinel、Skywalking
- 数据库连接池阿里巴巴Druid 1.1.24
- 数据库连接池阿里巴巴Druid 1.2.24
- AI大模型支持 `ChatGPT` `DeepSeek` `千问`等各种常规模式
- 日志打印logback
- 缓存Redis
- 其他autopoi, fastjsonpoiSwagger-uiquartz, lombok简化代码等。
- 默认数据库脚本:MySQL5.7+
- 默认提供MySQL5.7+数据库脚本
- [其他数据库,需要自己转](https://my.oschina.net/jeecg/blog/4905722)
#### 前端
- 前端IDE建议WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite+Ant-Design-Vue等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite5 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6
- 前端环境要求Node.js要求`Node 20+` 版本以上、pnpm 要求`9+` 版本以上
` ( Vite 不再支持已结束生命周期EOL的 Node.js 18。现在需要使用 Node.js 20.19+ 或 22.12+)`
- 依赖管理node、npm、pnpm
- 前端IDE建议IDEA、WebStorm、Vscode
- 采用 Vue3.0+TypeScript+Vite6+Ant-Design-Vue4等新技术方案包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能
- 最新技术栈Vue3.0 + TypeScript + Vite6 + ant-design-vue4 + pinia + echarts + unocss + vxe-table + qiankun + es6

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
#
# XXL-JOB v2.2.0
# XXL-JOB v2.4.0
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_general_ci;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
oracle导出编码 export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK
导出用户: jeecgbootos
导入命令: imp scott/tiger@orcl file=jeecgboot-oracle11g.dmp

View File

@ -18,20 +18,47 @@ services:
--max_allowed_packet=128M
--default-authentication-plugin=caching_sha2_password
ports:
- 3306:3306
- 13306:3306
networks:
- jeecg-boot
jeecg-boot-redis:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/redis:5.0
ports:
- 6379:6379
# ports:
# - 6379:6379
restart: always
hostname: jeecg-boot-redis
container_name: jeecg-boot-redis
networks:
- jeecg-boot
jeecg-boot-pgvector:
image: registry.cn-hangzhou.aliyuncs.com/jeecgdocker/pgvector
container_name: jeecg-boot-pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: vector_db
# ports:
# - 5432:5432
restart: always
networks:
- jeecg-boot
# jeecg-boot-rabbitmq:
# image: rabbitmq:3.7.7-management
## ports:
## - 5672:5672
## - 15672:15672
# restart: always
# container_name: jeecg-boot-rabbitmq
# hostname: jeecg-boot-rabbitmq
# environment:
# RABBITMQ_DEFAULT_USER: guest
# RABBITMQ_DEFAULT_PASS: guest
# networks:
# - jeecg-boot
jeecg-boot-system:
build:
context: ./jeecg-module-system/jeecg-system-start
@ -46,6 +73,8 @@ services:
- 8080:8080
networks:
- jeecg-boot
volumes:
- ./config:/jeecg-boot/config
networks:
jeecg-boot:

View File

@ -2,9 +2,9 @@
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">
<parent>
<groupId>org.jeecgframework.boot</groupId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.8.1</version>
<version>3.8.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -42,23 +42,13 @@
<dependencies>
<!--jeecg-tools-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-common3</artifactId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-common</artifactId>
</dependency>
<!--集成springmvc框架并实现自动配置 -->
<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>
@ -117,11 +107,10 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- minidao -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
</dependency>
<!-- druid -->
<dependency>
@ -148,7 +137,7 @@
<!-- sqlserver-->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<artifactId>mssql-jdbc</artifactId>
<version>${sqljdbc4.version}</version>
<scope>runtime</scope>
</dependency>
@ -170,13 +159,13 @@
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>kingbase8</artifactId>
<version>9.0.0</version>
<version>${kingbase8.version}</version>
<scope>runtime</scope>
</dependency>
<!--达梦数据库驱动 版本号1-3-26-2023.07.26-197096-20046-ENT -->
<dependency>
<groupId>com.dameng</groupId>
<artifactId>Dm8JdbcDriver18</artifactId>
<artifactId>DmJdbcDriver18</artifactId>
<version>${dm8.version}</version>
</dependency>
<dependency>
@ -264,8 +253,8 @@
</exclusions>
</dependency>
<!-- <dependency>
<!-- <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
@ -304,17 +293,6 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>autopoi-web</artifactId>
<version>${autopoi-web.version}</version>
<exclusions>
<exclusion>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</exclusion>
<exclusion>
<artifactId>xercesImpl</artifactId>
<groupId>xerces</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>xerces</groupId>
@ -385,14 +363,9 @@
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- chatgpt -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter3-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>minidao-spring-boot-starter-jsqlparser-4.9</artifactId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,5 +1,6 @@
package org.jeecg.common.api;
import org.jeecg.common.api.dto.AiragFlowDTO;
import org.jeecg.common.system.vo.*;
import java.util.List;
@ -144,4 +145,15 @@ public interface CommonAPI {
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 16 运行AIRag流程
* for [QQYUN-13634]在baseapi里面封装方法方便其他模块调用
*
* @param airagFlowDTO
* @return 流程执行结果,可能是String或者Map
* @author chenrui
* @date 2025/9/2 11:43
*/
Object runAiragFlow(AiragFlowDTO airagFlowDTO);
}

View File

@ -0,0 +1,36 @@
package org.jeecg.common.api.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* 调用AI流程入参
* for [QQYUN-13634]在baseapi里面封装方法方便其他模块调用
* @author chenrui
* @date 2025/9/2 14:11
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class AiragFlowDTO implements Serializable {
private static final long serialVersionUID = 7431775881170684867L;
/**
* 流程id
*/
private String flowId;
/**
* 输入参数
*/
private Map<String, Object> inputParams;
}

View File

@ -91,6 +91,12 @@ public class MessageDTO implements Serializable {
private Boolean isTimeJob = false;
//---【邮件相关参数】-------------------------------------------------------------
/**
* 枚举org.jeecg.common.constant.enums.NoticeTypeEnum
* 通知类型(system:系统消息、file:知识库、flow:流程、plan:日程计划、meeting:会议)
*/
private String noticeType;
public MessageDTO(){
}

View File

@ -1,6 +1,7 @@
package org.jeecg.common.api.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.jeecg.common.constant.CommonConstant;

View File

@ -303,6 +303,11 @@ public interface CommonConstant {
*/
String SYS_USER_ID_MAPPING_CACHE = "sys:cache:user:id_mapping";
/**
* 系统角色管理员编码
*/
String SYS_ROLE_ADMIN = "admin";
/**
* 考勤补卡业务状态 1同意 2不同意
*/
@ -428,6 +433,11 @@ public interface CommonConstant {
*/
String NOTICE_MSG_BUS_TYPE = "NOTICE_MSG_BUS_TYPE";
/**
* 通知类型,用于区分来源 file 知识 flow 流程 plan 日程 system 系统消息
*/
String NOTICE_TYPE = "noticeType";
/**
* 邮箱消息中地址登录时地址后携带的token,需要替换成真实的token值
*/
@ -629,4 +639,74 @@ public interface CommonConstant {
* 修改手机号验证码请求次数超出
*/
Integer PHONE_SMS_FAIL_CODE = 40002;
/**
* 自定义首页关联关系(ROLE:表示角色 USER:表示用户 DEFAULT:默认首页)
*
*/
String HOME_RELATION_ROLE = "ROLE";
String HOME_RELATION_USER = "USER";
String HOME_RELATION_DEFAULT = "DEFAULT";
/**
* 是否置顶(0否 1是)
*/
Integer IZ_TOP_1 = 1;
Integer IZ_TOP_0 = 0;
//关注流程缓存前缀
String FLOW_FOCUS_NOTICE_PREFIX = "flow:runtimeData:focus:notice:";
//任务缓办时间缓存前缀
String FLOW_TASK_DELAY_PREFIX = "flow:runtimeData:task:delay:";
/**
* 用户代理类型离职quit 代理agent
*/
String USER_AGENT_TYPE_QUIT = "quit";
String USER_AGENT_TYPE_AGENT = "agent";
/**
* 督办流程首节点任务taskKey
*/
String SUPERVISE_FIRST_TASK_KEY = "Task_1bhxpt0";
/**
* wps模板预览数据缓存前缀
*/
String EOA_WPS_TEMPLATE_VIEW_DATA ="eoa:wps:templateViewData:";
/**
* wps模板预览版本号缓存前缀
*/
String EOA_WPS_TEMPLATE_VIEW_VERSION ="eoa:wps:templateViewVersion:";
/**
* 表单设计器oa新增字段
* x_oa_timeout_date:逾期时间
* x_oa_archive_status:归档状态
*/
String X_OA_TIMEOUT_DATE ="x_oa_timeout_date";
String X_OA_ARCHIVE_STATUS ="x_oa_archive_status";
/**
* 流程状态
* 待提交: 1
* 处理中: 2
* 已完成: 3
* 已作废: 4
* 已挂起: 5
*/
String BPM_STATUS_1 ="1";
String BPM_STATUS_2 ="2";
String BPM_STATUS_3 ="3";
String BPM_STATUS_4 ="4";
String BPM_STATUS_5 ="5";
/**
* 默认租户产品包
*/
String TENANT_PACK_DEFAULT = "default";
/**
* 部门名称redisKey(全路径)
*/
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
}

View File

@ -4,6 +4,20 @@ package org.jeecg.common.constant;
* @author: jeecg-boot
*/
public interface DataBaseConstant {
/**
* 内置的系统变量键列表
*/
public static final String[] SYSTEM_KEYS = {
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
DataBaseConstant.SYS_BASE_PATH
};
//*********数据库类型****************************************
/**MYSQL数据库*/

View File

@ -0,0 +1,15 @@
package org.jeecg.common.constant;
/**
* @Description: 密码常量类
*
* @author: wangshuai
* @date: 2025/8/27 20:10
*/
public interface PasswordConstant {
/**
* 导入用户默认密码
*/
String DEFAULT_PASSWORD = "123456";
}

View File

@ -121,7 +121,7 @@ public class ProvinceCityArea {
public void getAreaByCode(String code,List<String> ls){
for(Area area: areaList){
if(area.getId().equals(code)){
if(null != area && area.getId().equals(code)){
String pid = area.getPid();
ls.add(0,area.getText());
getAreaByCode(pid,ls);

View File

@ -0,0 +1,97 @@
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* @Description: 部门类型枚举类
*
* @author: wangshuai
* @date: 2025/8/19 21:37
*/
public enum DepartCategoryEnum {
DEPART_CATEGORY_COMPANY("部门类型:公司","公司","1"),
DEPART_CATEGORY_DEPART("部门类型:部门","部门","2"),
DEPART_CATEGORY_POST("部门类型:岗位","岗位","3"),
DEPART_CATEGORY_SUB_COMPANY("部门类型:子公司","子公司","4");
DepartCategoryEnum(String described, String name, String value) {
this.value = value;
this.name = name;
this.described = described;
}
/**
* 描述
*/
private String described;
/**
* 值
*/
private String value;
/**
* 名称
*/
private String name;
public String getDescribed() {
return described;
}
public void setDescribed(String described) {
this.described = described;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 根据值获取名称
*
* @param value
* @return
*/
public static String getNameByValue(String value){
if (oConvertUtils.isEmpty(value)) {
return null;
}
for (DepartCategoryEnum val : values()) {
if (val.getValue().equals(value)) {
return val.getName();
}
}
return value;
}
/**
* 根据名称获取值
*
* @param name
* @return
*/
public static String getValueByName(String name){
if (oConvertUtils.isEmpty(name)) {
return null;
}
for (DepartCategoryEnum val : values()) {
if (val.getName().equals(name)) {
return val.getValue();
}
}
return name;
}
}

View File

@ -9,14 +9,14 @@ import org.apache.commons.lang3.StringUtils;
public enum DySmsEnum {
/**登录短信模板编码*/
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
LOGIN_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**忘记密码短信模板编码*/
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
FORGET_PASSWORD_TEMPLATE_CODE("SMS_175435174","敲敲云","code"),
/**修改密码短信模板编码*/
CHANGE_PASSWORD_TEMPLATE_CODE("SMS_465391221","敲敲云","code"),
/**注册账号短信模板编码*/
REGISTER_TEMPLATE_CODE("SMS_175430166","敲敲云","code");
/**
* 短信模板编码
*/

View File

@ -13,6 +13,10 @@ public enum EmailTemplateEnum {
* 流程催办
*/
BPM_CUIBAN_EMAIL("bpm_cuiban_email", "/templates/email/bpm_cuiban_email.ftl"),
/**
* 流程抄送
*/
BPM_CC_EMAIL("bpm_cc_email", "/templates/email/bpm_cc_email.ftl"),
/**
* 流程新任务
*/

View File

@ -8,21 +8,30 @@ import java.util.List;
/**
* 消息类型
*
* @author: jeecg-boot
*/
@EnumDict("messageType")
public enum MessageTypeEnum {
/** 系统消息 */
XT("system", "系统消息"),
/** 邮件消息 */
YJ("email", "邮件消息"),
/** 钉钉消息 */
/**
* 系统消息
*/
XT("system", "系统消息"),
/**
* 邮件消息
*/
YJ("email", "邮件消息"),
/**
* 钉钉消息
*/
DD("dingtalk", "钉钉消息"),
/** 企业微信 */
/**
* 企业微信
*/
QYWX("wechat_enterprise", "企业微信");
MessageTypeEnum(String type, String note){
MessageTypeEnum(String type, String note) {
this.type = type;
this.note = note;
}
@ -56,12 +65,13 @@ public enum MessageTypeEnum {
/**
* 获取字典数据
*
* @return
*/
public static List<DictModel> getDictList(){
public static List<DictModel> getDictList() {
List<DictModel> list = new ArrayList<>();
DictModel dictModel = null;
for(MessageTypeEnum e: MessageTypeEnum.values()){
for (MessageTypeEnum e : MessageTypeEnum.values()) {
dictModel = new DictModel();
dictModel.setValue(e.getType());
dictModel.setText(e.getNote());

View File

@ -0,0 +1,84 @@
package org.jeecg.common.constant.enums;
/**
* @Description: 文件类型枚举类
*
* @author: wangshuai
* @date: 2025/6/26 17:29
*/
public enum NoticeTypeEnum {
//VUE3专用
NOTICE_TYPE_FILE("知识库消息","file"),
NOTICE_TYPE_FLOW("工作流消息","flow"),
NOTICE_TYPE_PLAN("日程消息","plan"),
//暂时没用到
NOTICE_TYPE_MEETING("会议消息","meeting"),
NOTICE_TYPE_SYSTEM("系统消息","system"),
/**
* 协同工作
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
NOTICE_TYPE_COLLABORATION("协同工作", "collab"),
/**
* 督办
*/
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
/**
* 文件类型名称
*/
private String name;
/**
* 文件类型值
*/
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
NoticeTypeEnum(String name, String value) {
this.name = name;
this.value = value;
}
/**
* 获取聊天通知类型
*
* @param value
* @return
*/
public static String getChatNoticeType(String value){
return value + "Notice";
}
/**
* 获取通知名称
*
* @param value
* @return
*/
public static String getNoticeNameByValue(String value){
value = value.replace("Notice","");
for (NoticeTypeEnum e : NoticeTypeEnum.values()) {
if (e.getValue().equals(value)) {
return e.getName();
}
}
return "系统消息";
}
}

View File

@ -0,0 +1,180 @@
package org.jeecg.common.constant.enums;
import java.util.Arrays;
import java.util.List;
/**
* 职级枚举类
*
* 注意此枚举仅适用于天津临港控股OA项目,职级的名称和等级均为写死(需要与数据库配置一致)
* @date 2025-08-26
* @author scott
*/
public enum PositionLevelEnum {
// 领导层级等级1-3
CHAIRMAN("董事长", 1, PositionType.LEADER),
GENERAL_MANAGER("总经理", 2, PositionType.LEADER),
VICE_GENERAL_MANAGER("副总经理", 3, PositionType.LEADER),
// 职员层级等级4-6
MINISTER("部长", 4, PositionType.STAFF),
VICE_MINISTER("副部长", 5, PositionType.STAFF),
STAFF("职员", 6, PositionType.STAFF);
private final String name;
private final int level;
private final PositionType type;
PositionLevelEnum(String name, int level, PositionType type) {
this.name = name;
this.level = level;
this.type = type;
}
public String getName() {
return name;
}
public int getLevel() {
return level;
}
public PositionType getType() {
return type;
}
/**
* 职级类型枚举
*/
public enum PositionType {
STAFF("职员层级"),
LEADER("领导层级");
private final String desc;
PositionType(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
/**
* 根据职级名称获取枚举
* @param name 职级名称
* @return 职级枚举
*/
public static PositionLevelEnum getByName(String name) {
for (PositionLevelEnum position : values()) {
if (position.getName().equals(name)) {
return position;
}
}
return null;
}
/**
* 根据职级等级获取枚举
* @param level 职级等级
* @return 职级枚举
*/
public static PositionLevelEnum getByLevel(int level) {
for (PositionLevelEnum position : values()) {
if (position.getLevel() == level) {
return position;
}
}
return null;
}
/**
* 根据职级名称判断是否为职员层级
* @param name 职级名称
* @return true-职员层级false-非职员层级
*/
public static boolean isStaffLevel(String name) {
PositionLevelEnum position = getByName(name);
return position != null && position.getType() == PositionType.STAFF;
}
/**
* 根据职级名称判断是否为领导层级
* @param name 职级名称
* @return true-领导层级false-非领导层级
*/
public static boolean isLeaderLevel(String name) {
PositionLevelEnum position = getByName(name);
return position != null && position.getType() == PositionType.LEADER;
}
/**
* 比较两个职级的等级高低
* @param name1 职级名称1
* @param name2 职级名称2
* @return 正数表示name1等级更高负数表示name2等级更高0表示等级相同
*/
public static int compareLevel(String name1, String name2) {
PositionLevelEnum pos1 = getByName(name1);
PositionLevelEnum pos2 = getByName(name2);
if (pos1 == null || pos2 == null) {
return 0;
}
// 等级数字越小代表职级越高
return pos2.getLevel() - pos1.getLevel();
}
/**
* 判断是否为更高等级
* @param currentName 当前职级名称
* @param targetName 目标职级名称
* @return true-目标职级更高false-目标职级不高于当前职级
*/
public static boolean isHigherLevel(String currentName, String targetName) {
return compareLevel(targetName, currentName) > 0;
}
/**
* 获取所有职员层级名称
* @return 职员层级名称列表
*/
public static List<String> getStaffLevelNames() {
return Arrays.asList(MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName());
}
/**
* 获取所有领导层级名称
* @return 领导层级名称列表
*/
public static List<String> getLeaderLevelNames() {
return Arrays.asList(CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName());
}
/**
* 获取所有职级名称(按等级排序)
* @return 所有职级名称列表
*/
public static List<String> getAllPositionNames() {
return Arrays.asList(
CHAIRMAN.getName(), GENERAL_MANAGER.getName(), VICE_GENERAL_MANAGER.getName(),
MINISTER.getName(), VICE_MINISTER.getName(), STAFF.getName()
);
}
/**
* 获取指定等级范围的职级
* @param minLevel 最小等级
* @param maxLevel 最大等级
* @return 职级名称列表
*/
public static List<String> getPositionsByLevelRange(int minLevel, int maxLevel) {
return Arrays.stream(values())
.filter(p -> p.getLevel() >= minLevel && p.getLevel() <= maxLevel)
.map(PositionLevelEnum::getName)
.collect(java.util.stream.Collectors.toList());
}
}

View File

@ -23,7 +23,25 @@ public enum SysAnnmentTypeEnum {
/**
* 邀请用户跳转到个人设置
*/
TENANT_INVITE("tenant_invite", "url", "/system/usersetting");
TENANT_INVITE("tenant_invite", "url", "/system/usersetting"),
/**
* 协同工作-待办通知
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
EOA_CO_NOTIFY("eoa_co_notify", "url", "/collaboration/pending"),
/**
* 协同工作-催办通知
* for [JHHB-136]【vue3】协同工作系统消息需要添加一个类型
*/
EOA_CO_REMIND("eoa_co_remind", "url", "/collaboration/pending"),
/**
* 督办管理-催办
*/
EOA_SUP_REMIND("eoa_sup_remind", "url", "/superivse/list"),
/**
* 督办管理-通知
*/
EOA_SUP_NOTIFY("eoa_sup_notify", "url", "/superivse/list");
/**
* 业务类型(email:邮件 bpm:流程)

View File

@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Map;
@ -56,7 +57,7 @@ public class JeecgBootExceptionHandler {
addSysLog(e);
return Result.error("校验失败!" + e.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(",")));
}
/**
* 处理自定义异常
*/
@ -166,6 +167,27 @@ public class JeecgBootExceptionHandler {
return Result.error("文件大小超出10MB限制, 请压缩或降低文件质量! ");
}
/**
* 处理文件过大异常.
* jdk17中的MultipartException异常类已经被拆分成了MultipartException和MaxUploadSizeExceededException
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 16:13
*/
@ExceptionHandler(MultipartException.class)
public Result<?> handleMaxUploadSizeExceededException(MultipartException e) {
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException) {
log.error("文件大小超出限制: {}", cause.getMessage(), e);
addSysLog(e);
return Result.error("文件大小超出限制, 请压缩或降低文件质量!");
} else {
return handleException(e);
}
}
@ExceptionHandler(DataIntegrityViolationException.class)
public Result<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
log.error(e.getMessage(), e);
@ -221,11 +243,16 @@ public class JeecgBootExceptionHandler {
} catch (NullPointerException | BeansException ignored) {
}
if (null != request) {
//update-begin---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
//请求的参数
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)){
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
Map<String, String[]> parameterMap = request.getParameterMap();
if(!CollectionUtils.isEmpty(parameterMap)) {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
}
//update-end---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
// 请求地址
log.setRequestUrl(request.getRequestURI());
//设置IP地址
@ -251,4 +278,26 @@ public class JeecgBootExceptionHandler {
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 是否文件过大异常
* for [QQYUN-11716]上传大图片失败没有精确提示
* @param e
* @return
* @author chenrui
* @date 2025/4/8 20:21
*/
private static boolean isTooBigException(Throwable e) {
boolean isTooBigException = false;
if(e instanceof MultipartException){
Throwable cause = e.getCause();
if (cause instanceof IllegalStateException){
isTooBigException = true;
}
}
if(e instanceof MaxUploadSizeExceededException){
isTooBigException = true;
}
return isTooBigException;
}
}

View File

@ -4,6 +4,11 @@ import java.lang.annotation.*;
/**
* 将枚举类转化成字典数据
*
* <<使用说明>>
* 1. 枚举类需以 `Enum` 结尾,并且在类上添加 `@EnumDict` 注解。
* 2. 需要手动将枚举类所在包路径** 添加到 `org.jeecg.common.system.util.ResourceUtil.BASE_SCAN_PACKAGES` 配置数组中。
*
* @Author taoYan
* @Date 2022/7/8 10:34
**/

View File

@ -18,6 +18,7 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecgframework.poi.handler.inter.IExcelExportServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
@ -127,6 +128,53 @@ public class JeecgController<T, S extends IService<T>> {
return mv;
}
/**
* 大数据导出
* @param request
* @param object
* @param clazz
* @param title
* @param pageSize 每次查询的数据量
* @return
* @author chenrui
* @date 2025/8/11 16:11
*/
protected ModelAndView exportXlsForBigData(HttpServletRequest request, T object, Class<T> clazz, String title,Integer pageSize) {
// 组装查询条件
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(object, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
// 计算分页数
double total = service.count();
int count = (int) Math.ceil(total / pageSize);
// 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id", selectionList);
}
// 定义IExcelExportServer
IExcelExportServer excelExportServer = (queryParams, pageNum) -> {
if (pageNum > count) {
return null;
}
Page<T> page = new Page<T>(pageNum, pageSize);
IPage<T> pageList = service.page(page, (QueryWrapper<T>) queryParams);
return new ArrayList<>(pageList.getRecords());
};
// AutoPoi 导出Excel
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, jeecgBaseConfig.getPath().getUpload());
mv.addObject(NormalExcelConstants.PARAMS, exportParams);
mv.addObject(NormalExcelConstants.EXPORT_SERVER, excelExportServer);
mv.addObject(NormalExcelConstants.QUERY_PARAMS, queryWrapper);
return mv;
}
/**
* 根据权限导出excel传入导出字段参数

View File

@ -2,7 +2,6 @@ package org.jeecg.common.system.base.entity;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
@ -10,6 +9,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

View File

@ -11,6 +11,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import org.apache.commons.beanutils.PropertyUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
@ -257,8 +258,69 @@ public class QueryGenerator {
if(parameterMap!=null&& parameterMap.containsKey(ORDER_TYPE)) {
order = parameterMap.get(ORDER_TYPE)[0];
}
log.debug("排序规则>>列:" + column + ",排序方式:" + order);
if(oConvertUtils.isNotEmpty(column)){
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
}
// 1. 列表多字段排序优先
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
// 多字段排序
String sortInfoString = parameterMap.get("sortInfoString")[0];
log.info("多字段排序规则>> sortInfoString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
// 2. 列表单字段默认排序
if(oConvertUtils.isEmpty(column) && parameterMap!=null&& parameterMap.containsKey("defSortString")) {
// 多字段排序
String sortInfoString = parameterMap.get("defSortString")[0];
log.info("默认多字段排序规则>> defSortString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
String columnName = item.getColumn();
// 1.字典字段,去掉字典翻译文本后缀
if(columnName.endsWith(CommonConstant.DICT_TEXT_SUFFIX)) {
columnName = columnName.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
// 2.实体驼峰字段转为数据库字段
columnName = SqlInjectionUtil.getSqlInjectSortField(columnName);
// 二、设置字段排序规则
if (item.isAsc()) {
queryWrapper.orderByAsc(columnName);
} else {
queryWrapper.orderByDesc(columnName);
}
}
}
return;
}
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
@ -352,9 +414,11 @@ public class QueryGenerator {
}
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
List<QueryCondition> filterConditions = conditions.stream().filter(
rule -> oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())
rule -> (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
).collect(Collectors.toList());
if (filterConditions.size() == 0) {
return;
@ -365,9 +429,12 @@ public class QueryGenerator {
queryWrapper.and(andWrapper -> {
for (int i = 0; i < filterConditions.size(); i++) {
QueryCondition rule = filterConditions.get(i);
if (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
&& oConvertUtils.isNotEmpty(rule.getVal())) {
if (
(
oConvertUtils.isNotEmpty(rule.getField()) && oConvertUtils.isNotEmpty(rule.getRule()) && oConvertUtils.isNotEmpty(rule.getVal())
)
|| "empty".equals(rule.getRule())
) {
log.debug("SuperQuery ==> " + rule.toString());
@ -654,7 +721,11 @@ public class QueryGenerator {
* @param value 查询条件值
*/
public static void addEasyQuery(QueryWrapper<?> queryWrapper, String name, QueryRuleEnum rule, Object value) {
if (name==null || value == null || rule == null || oConvertUtils.isEmpty(value)) {
if (
(
name==null || value == null || rule == null || oConvertUtils.isEmpty(value)
)
&& !QueryRuleEnum.EMPTY.equals(rule)) {
return;
}
name = oConvertUtils.camelToUnderline(name);
@ -666,6 +737,9 @@ public class QueryGenerator {
case GE:
queryWrapper.ge(name, value);
break;
case EMPTY:
queryWrapper.isNull(name);
break;
case LT:
queryWrapper.lt(name, value);
break;

View File

@ -51,24 +51,24 @@ public class JwtUtil {
* @param code
* @param errorMsg
*/
public static void responseError(ServletResponse response, Integer code, String errorMsg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// issues/I4YH95浏览器显示乱码问题
httpServletResponse.setHeader("Content-type", "text/html;charset=UTF-8");
Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false);
OutputStream os = null;
try {
os = httpServletResponse.getOutputStream();
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setStatus(code);
os.write(new ObjectMapper().writeValueAsString(jsonResult).getBytes("UTF-8"));
os.flush();
os.close();
} catch (IOException e) {
public static void responseError(HttpServletResponse response, Integer code, String errorMsg) {
try {
Result jsonResult = new Result(code, errorMsg);
jsonResult.setSuccess(false);
// 设置响应头和内容类型
response.setStatus(code);
response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setContentType("application/json;charset=UTF-8");
// 使用 ObjectMapper 序列化为 JSON 字符串
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(jsonResult);
response.getWriter().write(json);
response.getWriter().flush();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
/**
* 校验token是否正确
@ -101,7 +101,7 @@ public class JwtUtil {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.warn(e.getMessage(), e);
log.error(e.getMessage(), e);
return null;
}
}

View File

@ -13,31 +13,33 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.lang.reflect.Method;
import java.util.*;
/**
* 资源加载工具类
* 枚举字典数据 资源加载工具类
*
* @Author taoYan
* @Date 2022/7/8 10:40
**/
@Slf4j
public class ResourceUtil {
/**
* 多个包扫描根路径
*
* 之所以让用户手工配置扫描路径,是为了避免不必要的类加载开销,提升启动性能。
* 请务必将所有枚举类所在包路径添加到此配置中。
*/
private final static String[] BASE_SCAN_PACKAGES = {
"org.jeecg.common.constant.enums",
"org.jeecg.modules.message.enums"
};
/**
* 枚举字典数据
*/
private final static Map<String, List<DictModel>> enumDictData = new HashMap<>(5);
/**
* 所有java类
*/
private final static String CLASS_PATTERN="/**/*.class";
/**
* 所有枚举java类
*/
@ -45,9 +47,9 @@ public class ResourceUtil {
private final static String CLASS_ENUM_PATTERN="/**/*Enum.class";
/**
* 包路径 org.jeecg
* 初始化状态标识
*/
private final static String BASE_PACKAGE = "org.jeecg";
private static volatile boolean initialized = false;
/**
* 枚举类中获取字典数据的方法名
@ -55,59 +57,135 @@ public class ResourceUtil {
private final static String METHOD_NAME = "getDictList";
/**
* 获取枚举字典数据
* 获取枚举类对应的字典数据 SysDictServiceImpl#queryAllDictItems()
* @return
*
* @return 枚举字典数据
*/
public static Map<String, List<DictModel>> getEnumDictData(){
if(enumDictData.keySet().size()>0){
return enumDictData;
}
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(BASE_PACKAGE) + CLASS_ENUM_PATTERN;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
for (Resource resource : resources) {
MetadataReader reader = readerFactory.getMetadataReader(resource);
String classname = reader.getClassMetadata().getClassName();
Class<?> clazz = Class.forName(classname);
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
if (enumDict != null) {
EnumDict annotation = clazz.getAnnotation(EnumDict.class);
String key = annotation.value();
if(oConvertUtils.isNotEmpty(key)){
List<DictModel> list = (List<DictModel>) clazz.getDeclaredMethod(METHOD_NAME).invoke(null);
enumDictData.put(key, list);
}
public static Map<String, List<DictModel>> getEnumDictData() {
if (!initialized) {
synchronized (ResourceUtil.class) {
if (!initialized) {
long startTime = System.currentTimeMillis();
log.info("【枚举字典加载】开始初始化枚举字典数据...");
initEnumDictData();
initialized = true;
long endTime = System.currentTimeMillis();
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
}
}
}catch (Exception e){
log.error("获取枚举类字典数据异常", e.getMessage());
// e.printStackTrace();
}
return enumDictData;
}
/**
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
* @param dictCodeList
* @param keys
* @return
* 使用多包路径扫描方式初始化枚举字典数据
*/
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys){
if(enumDictData.keySet().size()==0){
getEnumDictData();
private static void initEnumDictData() {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
long scanStartTime = System.currentTimeMillis();
List<Resource> allResources = new ArrayList<>();
// 扫描多个包路径
for (String basePackage : BASE_SCAN_PACKAGES) {
String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(basePackage) + CLASS_ENUM_PATTERN;
try {
Resource[] resources = resourcePatternResolver.getResources(pattern);
allResources.addAll(Arrays.asList(resources));
log.debug("【枚举字典加载】扫描包 {} 找到 {} 个枚举类文件", basePackage, resources.length);
} catch (Exception e) {
log.warn("【枚举字典加载】扫描包 {} 时出现异常: {}", basePackage, e.getMessage());
}
}
long scanEndTime = System.currentTimeMillis();
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
long processStartTime = System.currentTimeMillis();
int processedCount = 0;
for (Resource resource : allResources) {
try {
MetadataReader reader = readerFactory.getMetadataReader(resource);
String classname = reader.getClassMetadata().getClassName();
// 提前检查是否有@EnumDict注解避免不必要的Class.forName
if (hasEnumDictAnnotation(reader)) {
processEnumClass(classname);
processedCount++;
}
} catch (Exception e) {
log.debug("处理资源异常: {} - {}", resource.getFilename(), e.getMessage());
}
}
long processEndTime = System.currentTimeMillis();
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
}
/**
* 检查类是否有EnumDict注解通过元数据避免类加载
*/
private static boolean hasEnumDictAnnotation(MetadataReader reader) {
try {
return reader.getAnnotationMetadata().hasAnnotation(EnumDict.class.getName());
} catch (Exception e) {
return false;
}
}
/**
* 处理单个枚举类
*/
private static void processEnumClass(String classname) {
try {
Class<?> clazz = Class.forName(classname);
EnumDict enumDict = clazz.getAnnotation(EnumDict.class);
if (enumDict != null) {
String key = enumDict.value();
if (oConvertUtils.isNotEmpty(key)) {
Method method = clazz.getDeclaredMethod(METHOD_NAME);
List<DictModel> list = (List<DictModel>) method.invoke(null);
enumDictData.put(key, list);
log.debug("成功加载枚举字典: {} -> {}", key, classname);
}
}
} catch (Exception e) {
log.debug("处理枚举类异常: {} - {}", classname, e.getMessage());
}
}
/**
* 用于后端字典翻译 SysDictServiceImpl#queryManyDictByKeys(java.util.List, java.util.List)
*
* @param dictCodeList 字典编码列表
* @param keys 键值列表
* @return 字典数据映射
*/
public static Map<String, List<DictModel>> queryManyDictByKeys(List<String> dictCodeList, List<String> keys) {
Map<String, List<DictModel>> enumDict = getEnumDictData();
Map<String, List<DictModel>> map = new HashMap<>();
for (String code : enumDictData.keySet()) {
if(dictCodeList.indexOf(code)>=0){
List<DictModel> dictItemList = enumDictData.get(code);
for(DictModel dm: dictItemList){
// 使用更高效的查找方式
Set<String> dictCodeSet = new HashSet<>(dictCodeList);
Set<String> keySet = new HashSet<>(keys);
for (String code : enumDict.keySet()) {
if (dictCodeSet.contains(code)) {
List<DictModel> dictItemList = enumDict.get(code);
for (DictModel dm : dictItemList) {
String value = dm.getValue();
if(keys.indexOf(value)>=0){
if (keySet.contains(value)) {
List<DictModel> list = new ArrayList<>();
list.add(new DictModel(value, dm.getText()));
map.put(code,list);
map.put(code, list);
break;
}
}
@ -115,22 +193,5 @@ public class ResourceUtil {
}
return map;
}
/**
* 获取实现类
*
* @param classPath
*/
public static Object getImplementationClass(String classPath){
try {
Class<?> aClass = Class.forName(classPath);
return SpringContextUtils.getBean(aClass);
} catch (ClassNotFoundException e) {
log.error("类没有找到",e);
return null;
} catch (NoSuchBeanDefinitionException e){
log.error(classPath + "没有实现",e);
return null;
}
}
}
}

View File

@ -1,13 +1,22 @@
package org.jeecg.common.system.util;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.DataBaseConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.oConvertUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
@ -239,5 +248,47 @@ public class SqlConcatUtil {
private static String getDbType() {
return CommonUtils.getDatabaseType();
}
/**
* 获取前端传过来的 "多字段排序信息: sortInfoString"
* @return
*/
public static List<OrderItem> getQueryConditionOrders(String column, String order, String queryInfoString){
List<OrderItem> list = new ArrayList<>();
if(oConvertUtils.isEmpty(queryInfoString)){
//默认以创建时间倒序查询
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(order)){
list.add(OrderItem.desc(column));
}else{
list.add(OrderItem.asc(column));
}
}else{
// 【TV360X-967】URL解码微服务下需要
if (queryInfoString.contains("%22column%22")) {
log.info("queryInfoString 原生 = {}", queryInfoString);
try {
queryInfoString = URLDecoder.decode(queryInfoString, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new JeecgBootException(e);
}
log.info("queryInfoString 解码 = {}", queryInfoString);
}
JSONArray array = JSONArray.parseArray(queryInfoString);
Iterator it = array.iterator();
while(it.hasNext()){
JSONObject json = (JSONObject)it.next();
String tempColumn = json.getString("column");
if(oConvertUtils.isNotEmpty(tempColumn)){
String tempOrder = json.getString("order");
if(CommonConstant.ORDER_TYPE_DESC.equalsIgnoreCase(tempOrder)){
list.add(OrderItem.desc(tempColumn));
}else{
list.add(OrderItem.asc(tempColumn));
}
}
}
}
return list;
}
}

View File

@ -68,6 +68,12 @@ public class LoginUser {
@SensitiveField
private String avatar;
/**
* 工号
*/
@SensitiveField
private String workNo;
/**
* 生日
*/
@ -138,4 +144,8 @@ public class LoginUser {
/**设备id uniapp推送用*/
private String clientId;
/**
* 主岗位
*/
private String mainDepPostId;
}

View File

@ -25,6 +25,7 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
@ -152,9 +153,9 @@ public class CommonUtils {
*/
public static String uploadLocal(MultipartFile mf,String bizPath,String uploadpath){
try {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(mf);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(mf, bizPath);
String fileName = null;
File file = new File(uploadpath + File.separator + bizPath + File.separator );
if (!file.exists()) {
@ -163,6 +164,10 @@ public class CommonUtils {
}
// 获取文件名
String orgName = mf.getOriginalFilename();
// 无中文情况下进行转码
if (orgName != null && !CommonUtils.ifContainChinese(orgName)) {
orgName = new String(orgName.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
orgName = CommonUtils.getFileName(orgName);
if(orgName.indexOf(SymbolConstant.SPOT)!=-1){
fileName = orgName.substring(0, orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf("."));
@ -242,6 +247,10 @@ public class CommonUtils {
try {
DataSource dataSource = SpringContextUtils.getApplicationContext().getBean(DataSource.class);
dbTypeEnum = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
//【采用SQL_SERVER2005引擎】QQYUN-13298 解决升级mybatisPlus后SqlServer分页使用OFFSET无排序字段报错问题
if (dbTypeEnum == DbType.SQL_SERVER) {
dbTypeEnum = DbType.SQL_SERVER2005;
}
return dbTypeEnum;
} catch (SQLException e) {
log.warn(e.getMessage(), e);

View File

@ -13,6 +13,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -814,4 +816,44 @@ public class DateUtils extends PropertyEditorSupport {
return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR);
}
/**
* 获取两个日期之间的所有日期列表,包含开始和结束日期
*
* @param begin
* @param end
* @return
*/
public static List<Date> getDateRangeList(Date begin, Date end) {
List<Date> dateList = new ArrayList<>();
if (begin == null || end == null) {
return dateList;
}
// 清除时间部分,只比较日期
Calendar beginCal = Calendar.getInstance();
beginCal.setTime(begin);
beginCal.set(Calendar.HOUR_OF_DAY, 0);
beginCal.set(Calendar.MINUTE, 0);
beginCal.set(Calendar.SECOND, 0);
beginCal.set(Calendar.MILLISECOND, 0);
Calendar endCal = Calendar.getInstance();
endCal.setTime(end);
endCal.set(Calendar.HOUR_OF_DAY, 0);
endCal.set(Calendar.MINUTE, 0);
endCal.set(Calendar.SECOND, 0);
endCal.set(Calendar.MILLISECOND, 0);
if (endCal.before(beginCal)) {
return dateList;
}
dateList.add(beginCal.getTime());
while (beginCal.before(endCal)) {
beginCal.add(Calendar.DAY_OF_YEAR, 1);
dateList.add(beginCal.getTime());
}
return dateList;
}
}

View File

@ -1,14 +1,22 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletResponse;
import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.zip.ZipEntry;
@ -203,4 +211,150 @@ public class FileDownloadUtils {
dir.mkdirs();
}
}
/**
* 下载单个文件到ZIP流
* 核心功能获取文件流写入ZIP条目
* @param fileUrl 文件URL可以是HTTP URL或本地路径
* @param fileName ZIP内的文件名
* @param zous ZIP输出流
*/
public static void downLoadSingleFile(String fileUrl, String fileName, String uploadUrl,ZipArchiveOutputStream zous) {
InputStream inputStream = null;
try {
// 创建ZIP条目每个文件在ZIP中都是一个独立条目
ZipArchiveEntry entry = new ZipArchiveEntry(fileName);
zous.putArchiveEntry(entry);
// 获取文件输入流:区分普通文件和快捷方式
if (fileUrl.endsWith(".url")) {
// 处理快捷方式:生成.url文件内容
inputStream = FileDownloadUtils.createInternetShortcut(fileName, fileUrl, "");
} else {
// 普通文件下载从URL或本地路径获取流
inputStream = getDownInputStream(fileUrl,uploadUrl);
}
if (inputStream != null) {
// 将文件流写入ZIP
IOUtils.copy(inputStream, zous);
}
// 关闭当前ZIP条目
zous.closeArchiveEntry();
} catch (IOException e) {
log.error("文件下载失败: {}", e);
} finally {
// 确保输入流关闭
IoUtil.close(inputStream);
}
}
/**
* 获取下载文件输入流
* 功能根据URL类型HTTP或本地获取文件流
* @param fileUrl 文件URL支持HTTP和本地路径
* @return 文件输入流失败返回null
*/
public static InputStream getDownInputStream(String fileUrl, String uploadUrl) {
try {
// 处理HTTP URL通过网络下载
if (oConvertUtils.isNotEmpty(fileUrl) && fileUrl.startsWith(CommonConstant.STR_HTTP)) {
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000); // 连接超时5秒
connection.setReadTimeout(30000); // 读取超时30秒
return connection.getInputStream();
} else {
// 处理本地文件:直接读取文件系统
String downloadFilePath = uploadUrl + File.separator + fileUrl;
// 安全检查:防止下载危险文件类型
SsrfFileTypeFilter.checkDownloadFileType(downloadFilePath);
return new BufferedInputStream(new FileInputStream(downloadFilePath));
}
} catch (IOException e) {
// 异常时返回null上层会处理空流情况
return null;
}
}
/**
* 获取文件扩展名
* 功能:从文件名中提取扩展名
* @param fileName 文件名
* @return 文件扩展名(不含点),如"txt"、"png"
*/
public static String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
}
/**
* 创建快捷方式(.url文件内容
* 功能生成Internet快捷方式文件内容
* @param name 快捷方式名称
* @param url 目标URL地址
* @param icon 图标路径(可选)
* @return 包含.url文件内容的输入流
*/
public static InputStream createInternetShortcut(String name, String url, String icon) {
StringWriter sw = new StringWriter();
try {
// 按照Windows快捷方式格式写入内容
sw.write("[InternetShortcut]\n");
sw.write("URL=" + url + "\n");
if (oConvertUtils.isNotEmpty(icon)) {
sw.write("IconFile=" + icon + "\n");
}
// 将字符串内容转换为输入流
return new ByteArrayInputStream(sw.toString().getBytes(StandardCharsets.UTF_8));
} finally {
IoUtil.close(sw);
}
}
/**
* 从URL中提取文件名
* 功能从HTTP URL或本地路径中提取纯文件名
* @param fileUrl 文件URL
* @return 文件名(不含路径)
*/
public static String getFileNameFromUrl(String fileUrl) {
try {
// 处理HTTP URL从路径部分提取文件名
if (fileUrl.startsWith(CommonConstant.STR_HTTP)) {
URL url = new URL(fileUrl);
String path = url.getPath();
return path.substring(path.lastIndexOf('/') + 1);
}
// 处理本地文件路径:从文件路径提取文件名
return fileUrl.substring(fileUrl.lastIndexOf(File.separator) + 1);
} catch (Exception e) {
// 如果解析失败,使用时间戳作为文件名
return "file_" + System.currentTimeMillis();
}
}
/**
* 生成ZIP中的文件名
* 功能:避免文件名冲突,为多个文件添加序号
* @param fileUrl 文件URL用于提取原始文件名
* @param index 文件序号从0开始
* @param total 文件总数
* @return 处理后的文件名(带序号)
*/
public static String generateFileName(String fileUrl, int index, int total) {
// 从URL中提取原始文件名
String originalFileName = getFileNameFromUrl(fileUrl);
// 如果只有一个文件,直接使用原始文件名
if (total == 1) {
return originalFileName;
}
// 多个文件时,使用序号+原始文件名
String extension = getFileExtension(originalFileName);
String nameWithoutExtension = originalFileName.replace("." + extension, "");
return String.format("%s_%d.%s", nameWithoutExtension, index + 1, extension);
}
}

View File

@ -55,13 +55,11 @@ public class MinioUtil {
*/
public static String upload(MultipartFile file, String bizPath, String customBucket) throws Exception {
String fileUrl = "";
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
// 业务路径过滤,防止攻击
bizPath = StrAttackFilter.filter(bizPath);
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(file, bizPath);
String newBucket = bucketName;
if(oConvertUtils.isNotEmpty(customBucket)){

View File

@ -2,6 +2,7 @@ package org.jeecg.common.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.io.BufferedWriter;
@ -16,6 +17,7 @@ import java.util.List;
*/
@Slf4j
@Component
@Lazy(false)
public class PmsUtil {

View File

@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@ -56,12 +56,22 @@ public class RestUtil {
private final static RestTemplate RT;
static {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
//update-begin---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
//update-end---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
requestFactory.setConnectTimeout(30000);
requestFactory.setReadTimeout(30000);
RT = new RestTemplate(requestFactory);
// 解决乱码问题
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
//update-begin---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8
for (int i = 0; i < RT.getMessageConverters().size(); i++) {
if (RT.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
RT.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
break;
}
}
//update-end---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
}
public static RestTemplate getRestTemplate() {
@ -221,6 +231,72 @@ public class RestUtil {
return RT.exchange(url, method, request, responseType);
}
/**
* 发送请求(支持自定义超时时间)
*
* @param url 请求地址
* @param method 请求方式
* @param headers 请求头 可空
* @param variables 请求url参数 可空
* @param params 请求body参数 可空
* @param responseType 返回类型
* @param timeout 超时时间毫秒如果为0或负数则使用默认超时
* @return ResponseEntity<responseType>
*/
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers,
JSONObject variables, Object params, Class<T> responseType, int timeout) {
log.info(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
}
if (method == null) {
throw new RuntimeException("method 不能为空");
}
if (headers == null) {
headers = new HttpHeaders();
}
// 创建自定义RestTemplate如果需要设置超时
RestTemplate restTemplate = RT;
if (timeout > 0) {
//update-begin---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
//update-end---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
requestFactory.setConnectTimeout(timeout);
requestFactory.setReadTimeout(timeout);
restTemplate = new RestTemplate(requestFactory);
//update-begin---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
// 解决乱码问题(替换 StringHttpMessageConverter 为 UTF-8
for (int i = 0; i < restTemplate.getMessageConverters().size(); i++) {
if (restTemplate.getMessageConverters().get(i) instanceof StringHttpMessageConverter) {
restTemplate.getMessageConverters().set(i, new StringHttpMessageConverter(StandardCharsets.UTF_8));
break;
}
}
//update-end---author:chenrui ---date:20251011 for[issues/8859]online表单java增强失效------------
}
// 请求体
String body = "";
if (params != null) {
if (params instanceof JSONObject) {
body = ((JSONObject) params).toJSONString();
} else {
body = params.toString();
}
}
// 拼接 url 参数
if (variables != null && !variables.isEmpty()) {
url += ("?" + asUrlVariables(variables));
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, method, request, responseType);
}
/**
* 获取JSON请求头
*/

View File

@ -0,0 +1,37 @@
package org.jeecg.common.util;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import java.util.concurrent.*;
/**
* @date 2025-09-04
* @author scott
*
* @Description: 支持shiro的API获取当前登录人方法的线程池
*/
public class ShiroThreadPoolExecutor extends ThreadPoolExecutor {
public ShiroThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable command) {
Subject subject = SecurityUtils.getSubject();
SecurityManager securityManager = SecurityUtils.getSecurityManager();
super.execute(() -> {
try {
ThreadContext.bind(securityManager);
ThreadContext.bind(subject);
command.run();
} finally {
ThreadContext.unbindSubject();
ThreadContext.unbindSecurityManager();
}
});
}
}

View File

@ -8,6 +8,7 @@ import org.jeecg.common.constant.ServiceNameConstants;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@ -16,6 +17,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
* @Description: spring上下文工具类
* @author: jeecg-boot
*/
@Lazy(false)
@Component
public class SpringContextUtils implements ApplicationContextAware {

View File

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

View File

@ -65,6 +65,10 @@ public class TokenUtils {
if (tenantId == null) {
tenantId = oConvertUtils.getString(request.getHeader(CommonConstant.TENANT_ID));
}
if (oConvertUtils.isNotEmpty(tenantId) && "undefined".equals(tenantId)) {
return null;
}
return tenantId;
}

View File

@ -68,6 +68,13 @@ public class DbTypeUtils {
return dbTypeIf(dbType, DbType.ORACLE, DbType.ORACLE_12C, DbType.DM);
}
/**
* 是否是达梦
*/
public static boolean dbTypeIsDm(DbType dbType) {
return dbTypeIf(dbType, DbType.DM);
}
public static boolean dbTypeIsSqlServer(DbType dbType) {
return dbTypeIf(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}

View File

@ -2,6 +2,7 @@ package org.jeecg.common.util.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@ -34,6 +35,7 @@ public class SsrfFileTypeFilter {
FILE_TYPE_WHITE_LIST.add("bmp");
FILE_TYPE_WHITE_LIST.add("svg");
FILE_TYPE_WHITE_LIST.add("ico");
FILE_TYPE_WHITE_LIST.add("heic");
//文本文件
FILE_TYPE_WHITE_LIST.add("txt");
@ -148,29 +150,38 @@ public class SsrfFileTypeFilter {
public static void checkDownloadFileType(String filePath) throws IOException {
//文件后缀
String suffix = getFileTypeBySuffix(filePath);
log.info("suffix:{}", suffix);
log.debug(" 【文件下载校验】文件后缀 suffix: {}", suffix);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new IOException("下载失败,存在非法文件类型:" + suffix);
throw new JeecgBootException("下载失败,存在非法文件类型:" + suffix);
}
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file) throws Exception {
//获取文件真是后缀
String suffix = getFileType(file);
log.info("suffix:{}", suffix);
checkUploadFileType(file, null);
}
/**
* 上传文件类型过滤
*
* @param file
*/
public static void checkUploadFileType(MultipartFile file, String customPath) throws Exception {
//1. 路径安全校验
validatePathSecurity(customPath);
//2. 校验文件后缀和头
String suffix = getFileType(file, customPath);
log.info("【文件上传校验】文件后缀 suffix: {}customPath{}", suffix, customPath);
boolean isAllowExtension = FILE_TYPE_WHITE_LIST.contains(suffix.toLowerCase());
//是否允许下载的文件
if (!isAllowExtension) {
throw new Exception("上传失败,存在非法文件类型:" + suffix);
throw new JeecgBootException("上传失败,存在非法文件类型:" + suffix);
}
}
@ -182,7 +193,7 @@ public class SsrfFileTypeFilter {
* @throws Exception
*/
private static String getFileType(MultipartFile file) throws Exception {
private static String getFileType(MultipartFile file, String customPath) throws Exception {
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
String fileExtendName = null;
InputStream is = null;
@ -202,7 +213,7 @@ public class SsrfFileTypeFilter {
break;
}
}
log.info("-----获取到的指定文件类型------"+fileExtendName);
log.debug("-----获取到的指定文件类型------"+fileExtendName);
// 如果不是上述类型,则判断扩展名
if (StringUtils.isBlank(fileExtendName)) {
String fileName = file.getOriginalFilename();
@ -213,7 +224,6 @@ public class SsrfFileTypeFilter {
// 如果有扩展名,则返回扩展名
return getFileTypeBySuffix(fileName);
}
log.info("-----最終的文件类型------"+fileExtendName);
is.close();
return fileExtendName;
} catch (Exception e) {
@ -248,4 +258,34 @@ public class SsrfFileTypeFilter {
}
return stringBuilder.toString();
}
/**
* 路径安全校验
*/
private static void validatePathSecurity(String customPath) throws JeecgBootException {
if (customPath == null || customPath.trim().isEmpty()) {
return;
}
// 统一分隔符为 /
String normalized = customPath.replace("\\", "/");
// 1. 防止路径遍历攻击
if (normalized.contains("..") || normalized.contains("~")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
// 2. 限制路径深度
int depth = normalized.split("/").length;
if (depth > 5) {
throw new JeecgBootException("上传业务路径深度超出限制!");
}
// 3. 限制字符集(只允许字母、数字、下划线、横线、斜杠)
if (!normalized.matches("^[a-zA-Z0-9/_-]+$")) {
throw new JeecgBootException("上传业务路径包含非法字符!");
}
}
}

View File

@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
@ -473,6 +474,23 @@ public class oConvertUtils {
return true;
}
/**
* 判断字符串是否为JSON格式
* @param str
* @return
*/
public static boolean isJson(String str) {
if (str == null || str.trim().isEmpty()) {
return false;
}
try {
com.alibaba.fastjson.JSON.parse(str);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取Map对象
*/
@ -1131,7 +1149,24 @@ public class oConvertUtils {
* @date 2020/9/12 15:50
*/
public static <T> boolean isIn(T obj, T... objs) {
return isIn(obj, objs);
if (isEmpty(objs)) {
return false;
}
for (T obj1 : objs) {
if (isEqual(obj, obj1)) {
return true;
}
}
return false;
}
/**
* 判断租户ID是否有效
* @param tenantId
* @return
*/
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
}

View File

@ -97,9 +97,8 @@ public class OssBootUtil {
* @return oss 中的相对文件路径
*/
public static String upload(MultipartFile file, String fileDir,String customBucket) throws Exception {
//update-begin-author:liusq date:20210809 for: 过滤上传文件类型
// 文件安全校验,防止上传漏洞文件
SsrfFileTypeFilter.checkUploadFileType(file);
//update-end-author:liusq date:20210809 for: 过滤上传文件类型
String filePath = null;
initOss(endPoint, accessKeyId, accessKeySecret);

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

@ -3,13 +3,14 @@ package org.jeecg.config;
import org.jeecgframework.core.util.ApplicationContextUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* @Author: Scott
* @Date: 2018/2/7
* @description: autopoi 配置类
*/
@Lazy(false)
@Configuration
public class AutoPoiConfig {

View File

@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
* @Version:1.0
*/
@Slf4j
@Lazy(false)
@Service
public class AutoPoiDictConfig implements AutoPoiDictServiceI {
final static String EXCEL_SPLIT_TAG = "_";

View File

@ -74,7 +74,7 @@ public class JeecgBaseConfig {
/**
* 百度开放API配置
*/
private BaiduApi baiduApi;
private BaiduApi baiduApi;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;

View File

@ -0,0 +1,29 @@
package org.jeecg.config;
import org.jeecg.config.vo.GaoDeApi;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* 高德账号配置
*/
@Lazy(false)
@Configuration("jeecgGaodeBaseConfig")
@ConfigurationProperties(prefix = "jeecg.jmreport")
public class JeecgGaodeBaseConfig {
/**
* 高德开放API配置
*/
private GaoDeApi gaoDeApi;
public GaoDeApi getGaoDeApi() {
return gaoDeApi;
}
public void setGaoDeApi(GaoDeApi gaoDeApi) {
this.gaoDeApi = gaoDeApi;
}
}

View File

@ -2,12 +2,14 @@ package org.jeecg.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
* 设置静态参数初始化
* @author: jeecg-boot
*/
@Lazy(false)
@Component
@Data
public class StaticConfig {

View File

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

View File

@ -10,11 +10,13 @@ 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.customizers.OperationCustomizer;
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.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@ -61,42 +63,71 @@ public class Swagger3Config implements WebMvcConfigurer {
}
@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))
);
}
});
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
String path = getFullPath(handlerMethod);
if (!isExcludedPath(path)) {
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
}else{
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
}
return operation;
};
}
private String getFullPath(HandlerMethod handlerMethod) {
StringBuilder fullPath = new StringBuilder();
// 获取类级别的路径
RequestMapping classMapping = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
if (classMapping != null && classMapping.value().length > 0) {
fullPath.append(classMapping.value()[0]);
}
// 获取方法级别的路径
RequestMapping methodMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (methodMapping != null && methodMapping.value().length > 0) {
String methodPath = methodMapping.value()[0];
// 确保路径正确拼接,处理斜杠
if (!fullPath.toString().endsWith("/") && !methodPath.startsWith("/")) {
fullPath.append("/");
}
fullPath.append(methodPath);
}
return fullPath.toString();
}
private boolean isExcludedPath(String path) {
return excludedPaths.stream()
.anyMatch(pattern -> {
if (pattern.endsWith("/**")) {
// 处理通配符匹配
String basePath = pattern.substring(0, pattern.length() - 3);
return path.startsWith(basePath);
}
// 精确匹配
return pattern.equals(path);
});
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.8.0")
.version("3.8.3")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description( "后台API接口")
.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)));
new SecurityScheme()
.name(CommonConstant.X_ACCESS_TOKEN)
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) // 关键:指定为 header
));
}
}

View File

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

View File

@ -11,22 +11,19 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
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.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -50,6 +47,7 @@ import java.util.concurrent.TimeUnit;
* @Author qinfeng
*
*/
@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@ -61,6 +59,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、图片、视频、音频等
*/
@ -83,7 +89,7 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("doc.html");
registry.addViewController("/").setViewName("redirect:/doc.html");
}
@Bean
@ -149,12 +155,18 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 解决metrics端点不显示jvm信息的问题(zyf)
* 在Bean初始化完成后立即配置PrometheusMeterRegistry避免在Meter注册后才配置MeterFilter
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @author chenrui
* @date 2025/5/26 16:46
*/
@Bean
@ConditionalOnBean(name = "meterRegistryPostProcessor")
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
@PostConstruct
public void initPrometheusMeterRegistry() {
// 确保在应用启动早期就配置MeterFilter避免警告
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
log.info("PrometheusMeterRegistry配置完成");
}
}
// /**

View File

@ -8,12 +8,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class LowCodeModeConfiguration implements WebMvcConfigurer {
public LowCodeModeInterceptor payInterceptor() {
return new LowCodeModeInterceptor();
private final LowCodeModeInterceptor lowCodeModeInterceptor;
public LowCodeModeConfiguration(LowCodeModeInterceptor lowCodeModeInterceptor) {
this.lowCodeModeInterceptor = lowCodeModeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(payInterceptor()).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
registry.addInterceptor(lowCodeModeInterceptor).addPathPatterns(LowCodeUrlsEnum.getLowCodeInterceptUrls());
}
}

View File

@ -11,7 +11,7 @@ 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.annotation.Resource;
@ -38,6 +38,7 @@ import java.util.Set;
* @date 20230904
*/
@Slf4j
@Component
public class LowCodeModeInterceptor implements HandlerInterceptor {
/**
* 低代码开发模式
@ -47,18 +48,22 @@ public class LowCodeModeInterceptor implements HandlerInterceptor {
@Resource
private JeecgBaseConfig jeecgBaseConfig;
@Autowired
private CommonAPI commonAPI;
/**
* 在请求处理之前进行调用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
CommonAPI commonAPI = null;
log.info("低代码模式,拦截请求路径:" + request.getRequestURI());
//1、验证是否开启低代码开发模式控制
if (jeecgBaseConfig == null) {
jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
}
if (commonAPI == null) {
commonAPI = SpringContextUtils.getBean(CommonAPI.class);
}
if (jeecgBaseConfig.getFirewall()!=null && LowCodeModeInterceptor.LOW_CODE_MODE_PROD.equals(jeecgBaseConfig.getFirewall().getLowCodeMode())) {
String requestURI = request.getRequestURI().substring(request.getContextPath().length());

View File

@ -1,11 +1,16 @@
package org.jeecg.config.mybatis;
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.toolkit.JdbcUtils;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.log.Log;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.jeecg.common.config.TenantContext;
@ -15,24 +20,27 @@ import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* 单数据源配置jeecg.datasource.open = false时生效
* @Author zhoujf
*
*/
@Slf4j
@Configuration
@MapperScan(value={"org.jeecg.**.mapper*"})
public class MybatisPlusSaasConfig {
@Autowired
private DataSource dataSource;
/**
* 是否开启系统模块的租户隔离
* 控制范围:用户、角色、部门、我的部门、字典、分类字典、多数据源、职务、通知公告
@ -122,7 +130,23 @@ public class MybatisPlusSaasConfig {
//update-begin-author:zyf date:20220425 for:【VUEN-606】注入动态表名适配拦截器解决多表名问题
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
//update-end-author:zyf date:20220425 for:【VUEN-606】注入动态表名适配拦截器解决多表名问题
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
//update-begin---author:scott ---date:2025-08-02 for【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ROWS FETCH NEXT ROWS ONLY导致online报表报错---
DbType dbType = null;
try {
dbType = JdbcUtils.getDbType(dataSource.getConnection().getMetaData().getURL());
log.info("当前数据库类型: {}", dbType);
} catch (SQLException e) {
Log.error(e.getMessage(), e);
}
if (dbType!=null && (dbType == DbType.SQL_SERVER || dbType == DbType.SQL_SERVER2005)) {
// 如果是SQL Server则覆盖为2005分页方式
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER2005));
} else {
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
}
//update-end---author:scott ---date::2025-08-02 for【issues/8666】升级mybatisPlus后SqlServer分页使用OFFSET ROWS FETCH NEXT ROWS ONLY导致online报表报错---
//【jeecg-boot/issues/3847】增加@Version乐观锁支持
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;

View File

@ -9,11 +9,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* Minio文件上传配置文件
* @author: jeecg-boot
*/
@Lazy(false)
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "jeecg.minio", name = "minio_url")

View File

@ -6,11 +6,13 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
/**
* 云存储 配置
* @author: jeecg-boot
*/
@Lazy(false)
@Configuration
@ConditionalOnProperty(prefix = "jeecg.oss", name = "endpoint")
public class OssConfiguration {

View File

@ -109,7 +109,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/getLoginQrcode/**", "anon"); //登录二维码
filterChainDefinitionMap.put("/sys/getQrcodeToken/**", "anon"); //监听扫码
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
//update-begin--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
@ -126,6 +126,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.ttf", "anon");
filterChainDefinitionMap.put("/**/*.woff", "anon");
filterChainDefinitionMap.put("/**/*.woff2", "anon");
filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀
@ -153,7 +154,11 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/drag/share/view/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getAllChartData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalData", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getMapDataByCode", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getTotalDataByCompId", "anon");
filterChainDefinitionMap.put("/drag/mock/json/**", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/getDictByCodes", "anon");
filterChainDefinitionMap.put("/drag/onlDragDatasetHead/queryAllById", "anon");
filterChainDefinitionMap.put("/jimubi/view", "anon");
filterChainDefinitionMap.put("/jimubi/share/view/**", "anon");
@ -169,6 +174,12 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//仪表盘(按钮通信)
filterChainDefinitionMap.put("/dragChannelSocket/**","anon");
//App vue3版本查询版本接口
filterChainDefinitionMap.put("/sys/version/app3version", "anon");
//性能监控——安全隐患泄露TOEKNdurid连接池也有
//filterChainDefinitionMap.put("/actuator/**", "anon");
@ -180,8 +191,6 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -221,6 +230,8 @@ public class ShiroConfig {
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
@ -320,7 +331,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

@ -109,9 +109,9 @@ public class ShiroRealm extends AuthorizingRealm {
try {
loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
e.printStackTrace();
return null;
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
// 重新抛出异常让JwtFilter统一处理避免返回两次错误响应
throw e;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
@ -125,7 +125,7 @@ public class ShiroRealm extends AuthorizingRealm {
// 解密获得username用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
throw new AuthenticationException("Token非法无效!");
}
// 查询用户信息

View File

@ -56,7 +56,7 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
executeLogin(request, response);
return true;
} catch (Exception e) {
JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
return false;
//throw new AuthenticationException("Token失效请重新登录", e);
}

View File

@ -0,0 +1,17 @@
package org.jeecg.config.vo;
import lombok.Data;
/**
* @Description: 高德开放api配置
*
* @author: wangshuai
* @date: 2025/7/17 20:32
*/
@Data
public class GaoDeApi {
/**应用key*/
private String apiKey;
/**应用秘钥*/
private String secretKey;
}

View File

@ -0,0 +1,104 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<div class="box-content">
<div class="info-top">
<img src="https://www.jeecg.com/images/logo.png" style="float: left; margin: 0 10px 0 0; width: 32px;height:32px" /><div style="color:#fff"><strong>【重要】流程抄送的通知</strong></div>
</div>
<div class="info-wrap">
<div class="tips" style="padding:15px;">
<p style="margin: 10px 0;">
您好,您有一个新的流程抄送任务亟待查看,任务内容如下::
</p>
<table style="width: 400px; border-spacing: 0px; border-collapse: collapse; border: none; margin-top: 20px;"><tbody>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
流程名称
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_name}<a style="color: #006eff;" href="${url}" target="_blank" rel="noopener">[立刻查看]</a>
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px;height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送任务
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${bpm_task}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送时间
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${datetime}
</td>
</tr>
<tr style="height: 45px;">
<td style="width: 150px; height: 40px; background: #F6F6F6;border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
抄送内容
</td>
<td style="width: 250px;height: 40px; border: 1px solid #DBDBDB; font-size: 14px; font-weight: normal; text-align: left; padding-left: 14px;">
${remark}
</td>
</tr>
</tbody>
</table>
</div>
<div class="footer">北京国炬平台</div>
</div>
<div style="margin-top: 60px;margin-bottom: 10px;">
<span style="font-size: 13px; font-weight: bold; color: #666;">温馨提醒</span>
<div style="line-height: 24px; margin-top: 10px;">
<div style="font-size: 13px; color: #666;">使用过程中如有任何问题,请联系系统管理员。</div>
</div>
</div>
<div style="width: 600px; margin: 0 auto; margin-top: 50px; font-size: 12px; -webkit-font-smoothing: subpixel-antialiased; text-size-adjust: 100%;">
<p style="text-align: center; line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px !important; color: #7e8890 !important;">
<span class="appleLinks">Copyright © 2023-2024 北京国炬信息技术有限公司. 保留所有权利。</span>
</p>
<p style="text-align: center;line-height: 20.4px; text-size-adjust: 100%; font-family: 'Microsoft YaHei'!important; padding: 0px !important; margin: 0px; color: #7e8890 !important; margin-top: 10px;">
<span class="appleLinks">邮件由系统自动发送,请勿直接回复本邮件!</span>
</p>
</div>
</div>
</body>
<style>
.box-content{
width: 80%;
margin: 20px auto;
max-width: 800px;
min-width: 600px;
}
.info-top{
padding: 15px 25px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background: #4ea3f2;
color: #fff;
overflow: hidden;
line-height: 32px;
}
.info-wrap{
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border:1px solid #ddd;
overflow: hidden;
padding: 15px 15px 20px;
}
.footer{
text-align: right;
color: #999;
padding: 0 15px 15px;
}
</style>
</html>

View File

@ -4,9 +4,9 @@
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>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.8.1</version>
<version>3.8.3</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -31,41 +31,66 @@
</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>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>1.3.0-beta9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- system单体 api-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
<!-- 微服务starter和system微服务 api
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-starter-cloud</artifactId>
</dependency>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-system-cloud-api</artifactId>
</dependency>-->
<!-- aiflow依赖 -->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>1.0.5</version>
<version>3.8.3.1</version>
</dependency>
<!-- beigin 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
<scope>provided</scope>
</dependency>
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
<scope>provided</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
@ -97,17 +122,19 @@
</exclusions>
</dependency>
<!-- aiflow 脚本依赖 -->
<!-- langChain4j model support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-zhipu-ai</artifactId>
<version>${langchain4j.version}</version>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-zhipu-ai</artifactId>
<exclusions>
<exclusion>
<artifactId>checker-qual</artifactId>
@ -121,13 +148,11 @@
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-qianfan</artifactId>
<version>${langchain4j.version}</version>
<artifactId>langchain4j-community-qianfan</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-dashscope</artifactId>
<version>${langchain4j.version}</version>
<artifactId>langchain4j-community-dashscope</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
@ -141,9 +166,9 @@
</dependency>
<!-- langChain4j vextor support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.jeecgframework</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>${langchain4j.version}</version>
<version>1.3.0-beta9</version>
</dependency>
<!-- langChain4j Document Parser -->
<dependency>

View File

@ -16,6 +16,10 @@ public class AiAppConsts {
* 状态:禁用
*/
public static final String STATUS_DISABLE = "disable";
/**
* 状态:发布
*/
public static final String STATUS_RELEASE = "release";
/**

View File

@ -4,10 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -64,6 +67,7 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:app:edit")
public Result<String> edit(@RequestBody AiragApp airagApp) {
AssertUtils.assertNotEmpty("参数异常", airagApp);
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
@ -73,6 +77,28 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return Result.OK("保存完成!", airagApp.getId());
}
/**
* 发布应用
*
* @return
*/
@RequestMapping(value = "/release", method = RequestMethod.POST)
public Result<String> release(@RequestParam(name = "id") String id, @RequestParam(name = "release") Boolean release) {
AssertUtils.assertNotEmpty("id必须填写", id);
if (release == null) {
release = true;
}
AiragApp airagApp = new AiragApp();
airagApp.setId(id);
if (release) {
airagApp.setStatus(AiAppConsts.STATUS_RELEASE);
} else {
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
}
airagAppService.updateById(airagApp);
return Result.OK(release ? "发布成功" : "取消发布成功");
}
/**
* 通过id删除
*
@ -80,23 +106,23 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
* @return
*/
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:app:delete")
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragApp app = airagAppService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == app || !app.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI应用失败不能删除其他租户的AI应用");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagAppService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.airagAppService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*

View File

@ -1,16 +1,24 @@
package org.jeecg.modules.airag.app.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* airag应用-chat
*
@ -25,6 +33,15 @@ public class AiragChatController {
@Autowired
IAiragChatService chatService;
@Value(value = "${jeecg.path.upload}")
private String uploadpath;
/**
* 本地local miniominio 阿里alioss
*/
@Value(value="${jeecg.uploadType}")
private String uploadType;
/**
* 发送消息
@ -59,6 +76,19 @@ public class AiragChatController {
return chatService.send(chatSendParams);
}
/**
* 获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author chenrui
* @date 2025/2/25 11:42
*/
@IgnoreAuth
@GetMapping(value = "/init")
public Result<?> initChat(@RequestParam(name = "id", required = true) String id) {
return chatService.initChat(id);
}
/**
* 获取所有对话
*
@ -126,6 +156,20 @@ public class AiragChatController {
return chatService.clearMessage(conversationId);
}
/**
* 继续接收消息
*
* @param requestId
* @return
* @author chenrui
* @date 2025/8/11 17:49
*/
@IgnoreAuth
@GetMapping(value = "/receive/{requestId}")
public SseEmitter receiveByRequestId(@PathVariable(name = "requestId", required = true) String requestId) {
return chatService.receiveByRequestId(requestId);
}
/**
* 根据请求ID停止某个请求的处理
@ -141,4 +185,36 @@ public class AiragChatController {
return chatService.stop(requestId);
}
/**
* 上传文件
* for [QQYUN-12135]AI聊天上传图片提示非法token
*
* @param request
* @param response
* @return
* @throws Exception
* @author chenrui
* @date 2025/4/25 11:04
*/
@IgnoreAuth
@PostMapping(value = "/upload")
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
String bizPath = "airag";
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
// 获取上传文件对象
MultipartFile file = multipartRequest.getFile("file");
String savePath;
if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) {
savePath = CommonUtils.uploadLocal(file, bizPath, uploadpath);
} else {
savePath = CommonUtils.upload(file, bizPath, uploadType);
}
Result<?> result = new Result<>();
result.setMessage(savePath);
result.setSuccess(true);
return result;
}
}

View File

@ -39,12 +39,13 @@ public class AiragApp implements Serializable {
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
private java.lang.String id;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private java.lang.String createBy;
/**
* 创建日期
*/
@ -56,7 +57,7 @@ public class AiragApp implements Serializable {
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
private java.lang.String updateBy;
/**
* 更新日期
*/
@ -68,95 +69,95 @@ public class AiragApp implements Serializable {
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
private java.lang.String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
private java.lang.String tenantId;
/**
* 应用名称
*/
@Excel(name = "应用名称", width = 15)
@Schema(description = "应用名称")
private String name;
private java.lang.String name;
/**
* 应用描述
*/
@Excel(name = "应用描述", width = 15)
@Schema(description = "应用描述")
private String descr;
private java.lang.String descr;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private String icon;
private java.lang.String icon;
/**
* 应用类型
*/
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
@Dict(dicCode = "ai_app_type")
@Schema(description = "应用类型")
private String type;
private java.lang.String type;
/**
* 开场白
*/
@Excel(name = "开场白", width = 15)
@Schema(description = "开场白")
private String prologue;
private java.lang.String prologue;
/**
* 预设问题
*/
@Excel(name = "预设问题", width = 15)
@Schema(description = "预设问题")
private String presetQuestion;
private java.lang.String presetQuestion;
/**
* 提示词
*/
@Excel(name = "提示词", width = 15)
@Schema(description = "提示词")
private String prompt;
private java.lang.String prompt;
/**
* 模型配置
*/
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
@Schema(description = "模型配置")
private String modelId;
private java.lang.String modelId;
/**
* 历史消息数
*/
@Excel(name = "历史消息数", width = 15)
@Schema(description = "历史消息数")
private Integer msgNum;
private java.lang.Integer msgNum;
/**
* 知识库
*/
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
@Schema(description = "知识库")
private String knowledgeIds;
private java.lang.String knowledgeIds;
/**
* 流程
*/
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
@Schema(description = "流程")
private String flowId;
private java.lang.String flowId;
/**
* 快捷指令
*/
@Excel(name = "快捷指令", width = 15)
@Schema(description = "快捷指令")
private String quickCommand;
private java.lang.String quickCommand;
/**
* 状态
* 状态enable=启用、disable=禁用、release=发布)
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
private java.lang.String status;
/**
@ -164,7 +165,7 @@ public class AiragApp implements Serializable {
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private String metadata;
private java.lang.String metadata;
/**
* 知识库ids

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.app.entity.AiragApp;
@ -11,4 +12,14 @@ import org.jeecg.modules.airag.app.entity.AiragApp;
*/
public interface AiragAppMapper extends BaseMapper<AiragApp> {
/**
* 根据ID查询app信息(忽略租户)
* @param id
* @return
* @author chenrui
* @date 2025/4/21 16:03
*/
@InterceptorIgnore(tenantLine = "true")
AiragApp getByIdIgnoreTenant(String id);
}

View File

@ -2,4 +2,8 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.app.mapper.AiragAppMapper">
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.app.entity.AiragApp">
SELECT * FROM airag_app WHERE id = #{id}
</select>
</mapper>

View File

@ -92,4 +92,23 @@ public interface IAiragChatService {
* @date 2025/3/3 19:49
*/
Result<?> clearMessage(String conversationId);
/**
* 初始化聊天(忽略租户)
* [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
* @param appId
* @return
* @author chenrui
* @date 2025/4/21 14:17
*/
Result<?> initChat(String appId);
/**
* 继续接收消息
* @param requestId
* @return
* @author chenrui
* @date 2025/8/11 17:39
*/
SseEmitter receiveByRequestId(String requestId);
}

View File

@ -71,7 +71,7 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onNext((String resMessage) -> {
tokenStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
@ -99,9 +99,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
throw new RuntimeException(e);
}
})
.onComplete((responseMessage) -> {
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
@ -114,9 +114,6 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
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);

View File

@ -8,12 +8,13 @@ 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.exception.JeecgBootException;
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.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation;
@ -30,6 +31,8 @@ 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.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
@ -40,8 +43,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
@ -63,7 +65,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
RedisTemplate redisTemplate;
@Autowired
IAiragAppService airagAppService;
AiragAppMapper airagAppMapper;
@Autowired
IAiragFlowService airagFlowService;
@ -73,6 +75,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Autowired
private RedisUtil redisUtil;
@Autowired
JeecgToolsProvider jeecgToolsProvider;
/**
* 重新接收消息
*/
private static final ExecutorService SSE_THREAD_POOL = Executors.newFixedThreadPool(10); // 最大10个线程
@Override
public SseEmitter send(ChatSendParams chatSendParams) {
AssertUtils.assertNotEmpty("参数异常", chatSendParams);
@ -85,7 +95,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 获取app信息
AiragApp app = null;
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppService.getById(chatSendParams.getAppId());
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
}
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
// 更新标题
@ -146,13 +156,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
// 发送完成事件
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
log.error("终止会话时发生错误", e);
} catch (Exception e) {
if(!e.getMessage().contains("ResponseBodyEmitter has already completed")){
log.error("终止会话时发生错误", e);
}
try {
// 防止异常冒泡
emitter.completeWithError(e);
} catch (Exception ignore) {}
} finally {
// 从缓存中移除emitter
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
// 关闭emitter
emitter.complete();
try {
emitter.complete();
} catch (Exception ignore) {}
}
}
@ -237,6 +255,102 @@ public class AiragChatServiceImpl implements IAiragChatService {
return Result.ok();
}
@Override
public Result<?> initChat(String appId) {
AiragApp app = airagAppMapper.getByIdIgnoreTenant(appId);
return Result.ok(app);
}
@Override
public SseEmitter receiveByRequestId(String requestId) {
AssertUtils.assertNotEmpty("请选择会话",requestId);
if(AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId) == null){
return null;
}
List<EventData> datas = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId);
if(null == datas){
return null;
}
SseEmitter emitter = createSSE(requestId);
// 120秒
final long timeoutMillis = 120_000L;
// 使用线程池提交任务
SSE_THREAD_POOL.submit(() -> {
int lastIndex = 0;
long lastActiveTime = System.currentTimeMillis();
try {
while (true) {
if(lastIndex < datas.size()) {
try {
EventData eventData = datas.get(lastIndex++);
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]继续接收-接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
// 有新消息,重置计时
lastActiveTime = System.currentTimeMillis();
} catch (IOException e) {
log.error("[AI应用]继续接收-发送消息失败");
}
} else {
// 没有新消息了
if (AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId) == null) {
// 主线程sse已经被移除,退出线程.
log.info("[AI应用]继续接收-SSE消息推送完成: {}", requestId);
break;
} else if (System.currentTimeMillis() - lastActiveTime > timeoutMillis) {
// 主线程未结束,等待超时,
log.warn("[AI应用]继续接收-等待消息更新超时,释放线程: {}", requestId);
break;
} else {
// 主线程未结束, 未超时, 休眠一会再查
log.warn("[AI应用]继续接收-等待消息更新: {}", requestId);
Thread.sleep(500);
}
}
}
} catch (Exception e) {
log.error("SSE消息推送异常", e);
} finally {
try {
// 发送完成事件
emitter.send(SseEmitter.event().data(new EventData(requestId, null, EventData.EVENT_MESSAGE_END)));
} catch (Exception e) {
log.error("终止会话时发生错误", e);
try {
// 防止异常冒泡
emitter.completeWithError(e);
} catch (Exception ignore) {}
} finally {
// 关闭emitter
try {
emitter.complete();
} catch (Exception ignore) {}
}
}
});
return emitter;
}
/**
* 创建SSE
* @param requestId
* @return
* @author chenrui
* @date 2025/8/12 15:30
*/
private static SseEmitter createSSE(String requestId) {
SseEmitter emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, requestId);
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
try {
emitter.complete();
} catch (Exception ignore) {}
});
return emitter;
}
@Override
public Result<?> deleteConversation(String conversationId) {
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
@ -278,7 +392,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private String getConversationCacheKey(String conversationId,HttpServletRequest httpRequest) {
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
if (oConvertUtils.isEmpty(conversationId)) {
return null;
}
@ -376,7 +490,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:27
*/
private void saveChatConversation(ChatConversation chatConversation, boolean temp,HttpServletRequest httpRequest) {
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
if (null == chatConversation) {
return;
}
@ -414,8 +528,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
case AiragConsts.MESSAGE_ROLE_USER:
List<Content> contents = new ArrayList<>();
List<MessageHistory.ImageHistory> images = history.getImages();
if (oConvertUtils.isObjectNotEmpty(images)
&& !images.isEmpty()) {
if (oConvertUtils.isObjectNotEmpty(images) && !images.isEmpty()) {
contents.addAll(images.stream().map(imageHistory -> {
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
return ImageContent.from(imageHistory.getUrl());
@ -452,8 +565,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:05
*/
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation
chatConversation, String topicId) {
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
if (message.type().equals(ChatMessageType.SYSTEM)) {
// 系统消息,放到消息列表最前面,并且不记录历史
@ -467,11 +579,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
histories = new ArrayList<>();
}
// 消息记录
MessageHistory historyMessage = MessageHistory.builder()
.conversationId(chatConversation.getId())
.topicId(topicId)
.datetime(DateUtils.now())
.build();
MessageHistory historyMessage = MessageHistory.builder().conversationId(chatConversation.getId()).topicId(topicId).datetime(DateUtils.now()).build();
if (message.type().equals(ChatMessageType.USER)) {
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_USER);
StringBuilder textContent = new StringBuilder();
@ -515,9 +623,14 @@ public class AiragChatServiceImpl implements IAiragChatService {
AiragApp aiApp = chatConversation.getApp();
// 每次会话都生成一个新的,用来缓存emitter
String requestId = UUIDGenerator.generate();
SseEmitter emitter = new SseEmitter(-0L);
SseEmitter emitter = createSSE(requestId);
// 缓存emitter
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
// 缓存开始发送时间
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
// 初始化历史消息缓存
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
try {
// 组装用户消息
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
@ -541,6 +654,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
// 发消息
sendWithDefault(requestId, chatConversation, topicId, null, messages, null);
}
// 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
} catch (Throwable e) {
log.error(e.getMessage(), e);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
@ -589,36 +706,51 @@ public class AiragChatServiceImpl implements IAiragChatService {
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
flowRunParams.setEventCallback(eventData -> {
if (EventData.EVENT_FLOW_FINISHED.equals(eventData.getEvent())) {
// 打印耗时日志
printChatDuration(requestId, "流程执行完毕");
// 已经执行完了,删除时间缓存
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
EventFlowData data = (EventFlowData) eventData.getData();
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
if(data.isSuccess()) {
Object outputs = data.getOutputs();
if (oConvertUtils.isObjectNotEmpty(outputs)) {
AiMessage aiMessage;
if (outputs instanceof String) {
// 兼容推理模型
String messageText = String.valueOf(outputs);
messageText = messageText.replaceAll("<think>([\\s\\S]*?)</think>", "> $1");
aiMessage = new AiMessage(messageText);
} else {
aiMessage = new AiMessage(JSONObject.toJSONString(outputs));
}
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(aiMessage.text()).build();
msgEventData.setData(messageEventData);
msgEventData.setRequestId(requestId);
sendMessage2Client(emitter, msgEventData);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
}
EventData msgEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder()
.message(aiMessage.text())
.build();
msgEventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(msgEventData);
log.debug("[AI应用]接收FLOW返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}else{
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
// 失败
String message = data.getMessage();
if (message != null && message.contains(FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT)) {
message = "当前用户较多,排队中,请稍后再试!";
EventData errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
errEventData.setData(EventMessageData.builder().message("\n" + message).build());
sendMessage2Client(emitter, errEventData);
errEventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
// 如果是超时,主动关闭SSE,防止流程切面中返回异常消息导致前端不能正常展示上面的{普通消息}.
closeSSE(emitter, errEventData);
}
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
}
});
// 打印流程耗时日志
printChatDuration(requestId, "开始执行流程");
airagFlowService.runFlow(flowRunParams);
}
@ -649,24 +781,26 @@ public class AiragChatServiceImpl implements IAiragChatService {
String metadataStr = aiApp.getMetadata();
if (oConvertUtils.isNotEmpty(metadataStr)) {
JSONObject metadata = JSONObject.parseObject(metadataStr);
if(oConvertUtils.isNotEmpty(metadata)){
if (oConvertUtils.isNotEmpty(metadata)) {
if (metadata.containsKey("temperature")) {
aiChatParams.setTemperature(metadata.getDouble("temperature"));
}
if (metadata.containsKey("topP")) {
aiChatParams.setTopP(metadata.getDouble("temperature"));
aiChatParams.setTopP(metadata.getDouble("topP"));
}
if (metadata.containsKey("presencePenalty")) {
aiChatParams.setPresencePenalty(metadata.getDouble("temperature"));
aiChatParams.setPresencePenalty(metadata.getDouble("presencePenalty"));
}
if (metadata.containsKey("frequencyPenalty")) {
aiChatParams.setFrequencyPenalty(metadata.getDouble("temperature"));
aiChatParams.setFrequencyPenalty(metadata.getDouble("frequencyPenalty"));
}
if (metadata.containsKey("maxTokens")) {
aiChatParams.setMaxTokens(metadata.getInteger("temperature"));
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
}
}
}
// 打印流程耗时日志
printChatDuration(requestId, "构造应用自定义参数完成");
// 发消息
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
}
@ -683,24 +817,42 @@ public class AiragChatServiceImpl implements IAiragChatService {
* @author chenrui
* @date 2025/2/25 19:24
*/
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId,
List<ChatMessage> messages,AIChatParams aiChatParams) {
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
// 调用ai聊天
if(null == aiChatParams){
if (null == aiChatParams) {
aiChatParams = new AIChatParams();
}
// 如果是默认app,加载系统默认工具
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
}
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
TokenStream chatStream;
try {
// 打印流程耗时日志
printChatDuration(requestId, "开始向LLM发送消息");
if (oConvertUtils.isNotEmpty(modelId)) {
chatStream = aiChatHandler.chat(modelId, messages, aiChatParams);
} else {
chatStream = aiChatHandler.chatByDefaultModel(messages, aiChatParams);
}
} catch (Exception e) {
log.error(e.getMessage(),e);
log.error(e.getMessage(), e);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
return;
}
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
if(e instanceof JeecgBootException){
errMsg = e.getMessage();
}
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);
throw new JeecgBootBizTipException("调用大模型接口失败:" + e.getMessage());
}
/**
@ -708,92 +860,135 @@ public class AiragChatServiceImpl implements IAiragChatService {
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
// ai聊天响应逻辑
chatStream.onNext((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
chatStream.onPartialResponse((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
EventMessageData messageEventData = EventMessageData.builder().message(resMessage).build();
eventData.setData(messageEventData);
eventData.setRequestId(requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
sendMessage2Client(emitter, eventData);
}).onCompleteResponse((responseMessage) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息完成");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭");
return;
}
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 保存会话
saveChatConversation(chatConversation, false, httpRequest);
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else if (FinishReason.LENGTH.equals(finishReason)) {
// 上下文长度超过限制
log.error("调用模型异常:上下文长度超过限制:{}", responseMessage.tokenUsage());
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n上下文长度超过限制请调整模型最大Tokens").build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大语言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
}).onError((Throwable error) -> {
// 打印流程耗时日志
printChatDuration(requestId, "LLM输出消息异常");
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
// sse
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
if (null == emitter) {
log.warn("[AI应用]接收LLM返回会话已关闭{}", requestId);
return;
}
log.error(error.getMessage(), error);
String errMsg = error.getMessage();
if (errMsg != null && errMsg.contains("timeout")) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
errMsg = "当前用户较多,排队中,请稍后再试!";
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
eventData.setData(EventMessageData.builder().message("\n" + errMsg).build());
sendMessage2Client(emitter, eventData);
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
closeSSE(emitter, eventData);
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
} else {
errMsg = "调用大模型接口失败,详情请查看后台日志。";
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : AIChatHandler.MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (error.getMessage().contains(key)) {
errMsg = value;
}
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();
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
}
}).start();
}
/**
* 发送消息到客户端
*
* @param emitter
* @param eventData
* @author chenrui
* @date 2025/4/22 19:58
*/
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
try {
log.info("发送消息:{}", eventData.getRequestId());
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
List<EventData> historyMsg = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, eventData.getRequestId());
if (null == historyMsg) {
historyMsg = new CopyOnWriteArrayList<>();
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, eventData.getRequestId(), historyMsg);
}
historyMsg.add(eventData);
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
/**
@ -837,12 +1032,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
CompletableFuture.runAsync(() -> {
List<ChatMessage> messages = new LinkedList<>();
String systemMsgStr = "根据用户的问题,总结会话标题.\n" +
"要求如下:\n" +
"1. 使用中文回答.\n" +
"2. 标题长度控制在5个汉字10个英文字符以内\n" +
"3. 直接回复会话标题,不要有其他任何无关描述\n" +
"4. 如果无法总结,回复不知道\n";
String systemMsgStr = "根据用户的问题,总结会话标题.\n" + "要求如下:\n" + "1. 使用中文回答.\n" + "2. 标题长度控制在5个汉字10个英文字符以内\n" + "3. 直接回复会话标题,不要有其他任何无关描述\n" + "4. 如果无法总结,回复不知道\n";
messages.add(new SystemMessage(systemMsgStr));
messages.add(new UserMessage(question));
String summaryTitle;
@ -876,6 +1066,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
/**
* 获取用户名
*
* @param httpRequest
* @return
* @author chenrui
@ -885,9 +1076,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
try {
TokenUtils.getTokenByRequest();
String token;
if(null != httpRequest){
if (null != httpRequest) {
token = TokenUtils.getTokenByRequest(httpRequest);
}else{
} else {
token = TokenUtils.getTokenByRequest();
}
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
@ -898,4 +1089,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
}
return null;
}
/**
* 打印耗时
* @param requestId
* @param message
* @author chenrui
* @date 2025/4/28 15:15
*/
private static void printChatDuration(String requestId,String message) {
Long beginTime = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
if (null != beginTime) {
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
}
}
}

View File

@ -0,0 +1,83 @@
package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Java增强Demo: Excel数据读取器
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
* @Author: chenrui
* @Date: 2025/4/29 16:51
*/
@Component("jimuDataReader")
@Slf4j
public class JimuDataReader implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
// inputParams: {"bizData":"/xxxx/xxxx/xxxx/xxxx.xls"}
try {
String filePath = (String) inputParams.get("bizData");
if (filePath == null || filePath.isEmpty()) {
throw new IllegalArgumentException("File path is empty");
}
File excelFile = new File(filePath);
if (!excelFile.exists() || !excelFile.isFile()) {
throw new IllegalArgumentException("File not found: " + filePath);
}
// Since we don't know the target entity class, we'll read the Excel generically
return readExcelData(excelFile);
} catch (Exception e) {
log.error("Error processing Excel file", e);
throw new JeecgBootBizTipException("调用java增强失败", e);
}
}
/**
* Excel导入工具方法基于ExcelImportUtil
*
* @param file Excel文件
* @return Excel读取结果包含字段和数据
* @throws Exception 导入过程中的异常
*/
public static Map<String, Object> readExcelData(File file) throws Exception {
Map<String, Object> result = new HashMap<>();
// 设置导入参数
ImportParams params = new ImportParams();
params.setTitleRows(0); // 没有标题
params.setHeadRows(1); // 第一行是表头
// 读取Excel数据
List<Map<String, Object>> dataList = ExcelImportUtil.importExcel(file, Map.class, params);
// 如果没有数据,返回空结果
if (dataList == null || dataList.isEmpty()) {
result.put("fields", new ArrayList<>());
result.put("datas", new ArrayList<>());
return result;
}
// 从第一行数据中获取字段名
List<String> fieldNames = new ArrayList<>(dataList.get(0).keySet());
result.put("fields", fieldNames);
result.put("datas", dataList);
return result;
}
}

View File

@ -0,0 +1,22 @@
package org.jeecg.modules.airag.demo;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Map;
/**
* @Description: Java增强节点示例类
* @Author: chenrui
* @Date: 2025/3/6 11:42
*/
@Component("testAiragEnhance")
public class TestAiragEnhance implements IAiRagEnhanceJava {
@Override
public Map<String, Object> process(Map<String, Object> inputParams) {
Object arg1 = inputParams.get("arg1");
Object arg2 = inputParams.get("arg2");
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
}
}

View File

@ -35,6 +35,11 @@ public class LLMConsts {
*/
public static final String MODEL_TYPE_LLM = "LLM";
/**
* 向量模型:默认维度
*/
public static final Integer EMBED_MODEL_DEFAULT_DIMENSION = 1536;
/**
* 知识库:文档状态:草稿
*/
@ -47,7 +52,10 @@ public class LLMConsts {
* 知识库:文档状态:构建完成
*/
public static final String KNOWLEDGE_DOC_STATUS_COMPLETE = "complete";
/**
* 知识库:文档状态:构建失败
*/
public static final String KNOWLEDGE_DOC_STATUS_FAILED = "failed";
/**
* 知识库:文档类型:文本

View File

@ -4,9 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
@ -74,6 +77,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 17:09
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:knowledge:add")
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
airagKnowledgeService.save(airagKnowledge);
@ -90,6 +94,7 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:knowledge:edit")
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
if (airagKnowledgeEntity == null) {
@ -113,6 +118,7 @@ public class AiragKnowledgeController {
* @date 2025/3/12 17:05
*/
@PutMapping(value = "/rebuild")
@RequiresPermissions("airag:knowledge:rebuild")
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
String[] knowIdArr = knowIds.split(",");
for (String knowId : knowIdArr) {
@ -131,29 +137,24 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:knowledge:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragKnowledge know = airagKnowledgeService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == know || !know.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI知识库失败不能删除其他租户的AI知识库");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagKnowledgeDocService.removeByKnowIds(Collections.singletonList(id));
airagKnowledgeService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除知识库
*
* @param ids
* @return
* @author chenrui
* @date 2025/2/18 17:09
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
List<String> idsList = Arrays.asList(ids.split(","));
airagKnowledgeDocService.removeByKnowIds(idsList);
airagKnowledgeService.removeByIds(idsList);
return Result.OK("批量删除成功!");
}
/**
* 通过id查询知识库
*
@ -203,6 +204,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47
*/
@PostMapping(value = "/doc/edit")
@RequiresPermissions("airag:knowledge:doc:edit")
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
}
@ -215,6 +217,7 @@ public class AiragKnowledgeController {
* @date 2025/3/20 11:29
*/
@PostMapping(value = "/doc/import/zip")
@RequiresPermissions("airag:knowledge:doc:zip")
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
@RequestParam(name = "file", required = true) MultipartFile file) {
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
@ -241,6 +244,7 @@ public class AiragKnowledgeController {
* @date 2025/2/18 15:47
*/
@PutMapping(value = "/doc/rebuild")
@RequiresPermissions("airag:knowledge:doc:rebuild")
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
return airagKnowledgeDocService.rebuildDocument(docIds);
}
@ -255,12 +259,49 @@ public class AiragKnowledgeController {
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteBatch")
public Result<String> deleteDocumentBatch(@RequestParam(name = "ids", required = true) String ids) {
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
List<String> idsList = Arrays.asList(ids.split(","));
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
List<AiragKnowledgeDoc> docList = airagKnowledgeDocService.listByIds(idsList);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
docList.forEach(airagKnowledgeDoc -> {
if (null == airagKnowledgeDoc || !airagKnowledgeDoc.getTenantId().equals(currentTenantId)) {
throw new IllegalArgumentException("删除AI知识库文档失败不能删除其他租户的AI知识库文档");
}
});
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagKnowledgeDocService.removeDocByIds(idsList);
return Result.OK("批量删除成功!");
}
/**
* 清空知识库文档
*
* @param
* @return
*/
@Transactional(rollbackFor = Exception.class)
@DeleteMapping(value = "/doc/deleteAll")
@RequiresPermissions("airag:knowledge:doc:deleteAll")
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragKnowledge know = airagKnowledgeService.getById(knowId);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == know || !know.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI知识库失败不能删除其他租户的AI知识库");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
return airagKnowledgeDocService.deleteAllByKnowId(knowId);
}
/**
* 命中测试

View File

@ -3,12 +3,24 @@ package org.jeecg.modules.airag.llm.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.embedding.EmbeddingModel;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.airag.llm.consts.LLMConsts;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -17,6 +29,7 @@ import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collections;
/**
* @Description: AiRag模型配置
@ -32,6 +45,8 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
@Autowired
private IAiragModelService airagModelService;
@Autowired
AIChatHandler aiChatHandler;
/**
* 分页列表查询
@ -57,7 +72,17 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:model:add")
public Result<String> add(@RequestBody AiragModel airagModel) {
// 验证 模型名称/模型类型/基础模型
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
// 默认未激活
if(oConvertUtils.isObjectEmpty(airagModel.getActivateFlag())){
airagModel.setActivateFlag(0);
}
airagModel.setActivateFlag(0);
airagModelService.save(airagModel);
return Result.OK("添加成功!");
}
@ -69,6 +94,7 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:model:edit")
public Result<String> edit(@RequestBody AiragModel airagModel) {
airagModelService.updateById(airagModel);
return Result.OK("编辑成功!");
@ -81,23 +107,23 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
* @return
*/
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
@RequiresPermissions("airag:model:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//update-begin---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
//如果是saas隔离的情况下判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragModel model = airagModelService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == model || !model.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI模型失败不能删除其他租户的AI模型");
}
}
//update-end---author:chenrui ---date:20250606 for[issues/8337]关于ai工作列表的数据权限问题 #8337------------
airagModelService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.airagModelService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
@ -136,4 +162,28 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
return super.importExcel(request, response, AiragModel.class);
}
@PostMapping(value = "/test")
public Result<?> test(@RequestBody AiragModel airagModel) {
// 验证 模型名称/模型类型/基础模型
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
try {
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
}else{
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
embeddingModel.embed("test text");
}
}catch (Exception e){
log.error("测试模型连接失败", e);
return Result.error("测试模型连接失败:" + e.getMessage());
}
// 测试成功激活数据
airagModel.setActivateFlag(1);
airagModelService.updateById(airagModel);
return Result.OK("");
}
}

View File

@ -30,13 +30,14 @@ public class AiragKnowledge implements Serializable {
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
private java.lang.String id;
/**
* 创建人
*/
@Schema(description = "创建人")
private String createBy;
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private java.lang.String createBy;
/**
* 创建日期
@ -50,7 +51,7 @@ public class AiragKnowledge implements Serializable {
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
private java.lang.String updateBy;
/**
* 更新日期
@ -64,21 +65,21 @@ public class AiragKnowledge implements Serializable {
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
private java.lang.String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
private java.lang.String tenantId;
/**
* 知识库名称
*/
@Excel(name = "知识库名称", width = 15)
@Schema(description = "知识库名称")
private String name;
private java.lang.String name;
/**
* 向量模型id
@ -86,19 +87,19 @@ public class AiragKnowledge implements Serializable {
@Excel(name = "向量模型id", width = 15, dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
@Dict(dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
@Schema(description = "向量模型id")
private String embedId;
private java.lang.String embedId;
/**
* 描述
*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private String descr;
private java.lang.String descr;
/**
* 状态
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
private java.lang.String status;
}

View File

@ -3,6 +3,7 @@ package org.jeecg.modules.airag.llm.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.*;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
@ -40,6 +41,7 @@ public class AiragKnowledgeDoc implements Serializable {
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
@ -119,9 +121,4 @@ public class AiragKnowledgeDoc implements Serializable {
@Schema(description = "状态")
private String status;
/**
* 服务器基础路径
*/
@TableField(exist = false)
private String baseUrl;
}

View File

@ -45,6 +45,7 @@ public class AiragModel implements Serializable {
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
* 创建日期
@ -120,4 +121,11 @@ public class AiragModel implements Serializable {
@Excel(name = "模型参数", width = 15)
@Schema(description = "模型参数")
private String modelParams;
/**
* 是否激活(0=未激活,1=已激活)
*/
@Excel(name = "是否激活", width = 15)
@Schema(description = "是否激活")
private Integer activateFlag;
}

View File

@ -6,13 +6,14 @@ 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.exception.JeecgBootException;
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.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@ -22,9 +23,7 @@ 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.*;
import java.util.regex.Matcher;
/**
@ -38,7 +37,7 @@ import java.util.regex.Matcher;
public class AIChatHandler implements IAIChatHandler {
@Autowired
IAiragModelService airagModelService;
AiragModelMapper airagModelMapper;
@Autowired
EmbeddingHandler embeddingHandler;
@ -82,7 +81,8 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelService.getById(modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
return completions(airagModel, messages, params);
}
@ -96,9 +96,27 @@ public class AIChatHandler implements IAIChatHandler {
* @author chenrui
* @date 2025/2/24 17:30
*/
private String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
params = mergeParams(airagModel, params);
String resp = llmHandler.completions(messages, params);
String resp;
try {
resp = llmHandler.completions(messages, params);
} catch (Exception e) {
// langchain4j 异常友好提示
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
if (oConvertUtils.isNotEmpty(e.getMessage())) {
// 根据常见异常关键字做细致翻译
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (errMsg.contains(key)) {
errMsg = value;
}
}
}
log.error("AI模型调用异常: {}", errMsg, e);
throw new JeecgBootException(errMsg);
}
if (resp.contains("</think>")
&& (null == params.getNoThinking() || params.getNoThinking())) {
String[] thinkSplit = resp.split("</think>");
@ -150,7 +168,8 @@ public class AIChatHandler implements IAIChatHandler {
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
AssertUtils.assertNotEmpty("请选择模型", modelId);
AiragModel airagModel = airagModelService.getById(modelId);
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
return chat(airagModel, messages, params);
}
@ -226,7 +245,7 @@ public class AIChatHandler implements IAIChatHandler {
params.setMaxTokens(modelParams.getInteger("maxTokens"));
}
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
params.setMaxTokens(modelParams.getInteger("timeout"));
params.setTimeout(modelParams.getInteger("timeout"));
}
}
@ -237,6 +256,16 @@ public class AIChatHandler implements IAIChatHandler {
params.setQueryRouter(queryRouter);
}
// 设置确保maxTokens值正确
if (oConvertUtils.isObjectNotEmpty(params.getMaxTokens()) && params.getMaxTokens() <= 0) {
params.setMaxTokens(null);
}
// 默认超时时间
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
params.setTimeout(60);
}
return params;
}

View File

@ -9,7 +9,6 @@ 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;
@ -35,8 +34,9 @@ import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
import org.jeecg.modules.airag.llm.entity.AiragModel;
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.jeecg.modules.airag.llm.service.IAiragModelService;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@ -69,13 +69,15 @@ public class EmbeddingHandler implements IEmbeddingHandler {
EmbedStoreConfigBean embedStoreConfigBean;
@Autowired
@Lazy
private IAiragModelService airagModelService;
private AiragModelMapper airagModelMapper;
@Autowired
@Lazy
private IAiragKnowledgeService airagKnowledgeService;
@Autowired
private AiragKnowledgeMapper airagKnowledgeMapper;
@Value(value = "${jeecg.path.upload:}")
private String uploadpath;
@ -112,6 +114,13 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
/**
* 正则匹配: md图片
* "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)"
*/
private static final Pattern PATTERN_MD_IMAGE = Pattern.compile("!\\[(.*?)]\\((.*?)\\)");
/**
* 向量化文档
*
@ -157,7 +166,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
// 删除旧数据
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isEqualTo(doc.getId()));
// 分段器
DocumentSplitter splitter = DocumentSplitters.recursive(DEFAULT_SEGMENT_SIZE, DEFAULT_OVERLAP_SIZE, new OpenAiTokenizer());
DocumentSplitter splitter = DocumentSplitters.recursive(DEFAULT_SEGMENT_SIZE, DEFAULT_OVERLAP_SIZE);
// 分段并存储
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(splitter)
@ -183,6 +192,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
* @author chenrui
* @date 2025/2/18 16:52
*/
@Override
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
@ -223,7 +233,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowId);
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
@ -268,6 +278,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
* @author chenrui
* @date 2025/2/20 21:03
*/
@Override
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
List<ContentRetriever> retrievers = Lists.newArrayList();
@ -275,7 +286,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
if (oConvertUtils.isEmpty(knowId)) {
continue;
}
AiragKnowledge knowledge = airagKnowledgeService.getById(knowId);
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
AiModelOptions modelOptions = buildModelOptions(model);
@ -345,7 +356,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
*/
private AiragModel getEmbedModelData(String modelId) {
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
AiragModel model = airagModelService.getById(modelId);
AiragModel model = airagModelMapper.getByIdIgnoreTenant(modelId);
AssertUtils.assertNotEmpty("向量模型不存在", model);
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
return model;
@ -371,6 +382,18 @@ public class EmbeddingHandler implements IEmbeddingHandler {
AiModelOptions modelOp = buildModelOptions(model);
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
String tableName = embedStoreConfigBean.getTable();
// update-begin---author:sunjianlei ---date:20250509 for【QQYUN-12345】向量模型维度不一致问题
// 如果该模型不是默认的向量维度
int dimension = embeddingModel.dimension();
if (!LLMConsts.EMBED_MODEL_DEFAULT_DIMENSION.equals(dimension)) {
// 就加上维度后缀,防止因维度不一致导致保存失败
tableName += ("_" + dimension);
}
// update-end-----author:sunjianlei ---date:20250509 for【QQYUN-12345】向量模型维度不一致问题
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
// Connection and table parameters
.host(embedStoreConfigBean.getHost())
@ -378,7 +401,7 @@ public class EmbeddingHandler implements IEmbeddingHandler {
.database(embedStoreConfigBean.getDatabase())
.user(embedStoreConfigBean.getUser())
.password(embedStoreConfigBean.getPassword())
.table(embedStoreConfigBean.getTable())
.table(tableName)
// Embedding dimension
// Required: Must match the embedding models output dimension
.dimension(embeddingModel.dimension())
@ -449,16 +472,20 @@ public class EmbeddingHandler implements IEmbeddingHandler {
String fileType = FilenameUtils.getExtension(docFile.getName());
if ("md".contains(fileType)) {
// 如果是md文件查找所有图片语法如果是本地图片替换成网络图片
String baseUrl = doc.getBaseUrl() + "/sys/common/static/";
String baseUrl = "#{domainURL}/sys/common/static/";
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
if(oConvertUtils.isNotEmpty(sourcePath)) {
String escapedPath = uploadpath;
if (File.separator.equals("\\")){
//update-begin---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
/*if (File.separator.equals("\\")){
escapedPath = uploadpath.replace("//", "\\\\");
}
}*/
//update-end---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
baseUrl = baseUrl + sourcePath + "/";
StringBuffer sb = replaceImageUrl(content, baseUrl);
String docFilePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
docFilePath = FilenameUtils.getPath(docFilePath);
docFilePath = docFilePath.replace("\\", "/");
StringBuffer sb = replaceImageUrl(content, baseUrl + sourcePath + "/", baseUrl + docFilePath);
content = sb.toString();
}
}
@ -469,11 +496,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
}
@NotNull
private static StringBuffer replaceImageUrl(String content, String baseUrl) {
private static StringBuffer replaceImageUrl(String content, String abstractBaseUrl, String relativeBaseUrl) {
// 正则表达式匹配md文件中的图片语法 ![alt text](image url)
String mdImagePattern = "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)";
Pattern pattern = Pattern.compile(mdImagePattern);
Matcher matcher = pattern.matcher(content);
Matcher matcher = PATTERN_MD_IMAGE.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
@ -481,7 +506,16 @@ public class EmbeddingHandler implements IEmbeddingHandler {
// 检查是否是本地图片路径
if (!imageUrl.startsWith("http")) {
// 替换成网络图片路径
String networkImageUrl = baseUrl + imageUrl;
String networkImageUrl = abstractBaseUrl + imageUrl;
if(imageUrl.startsWith("/")) {
// 绝对路径
networkImageUrl = abstractBaseUrl + imageUrl;
}else{
// 相对路径
networkImageUrl = relativeBaseUrl + imageUrl;
}
// 修改图片路径中//->/但保留http://和https://
networkImageUrl = networkImageUrl.replaceAll("(?<!http:)(?<!https:)//", "/");
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + networkImageUrl + ")");
} else {
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + imageUrl + ")");

View File

@ -0,0 +1,42 @@
package org.jeecg.modules.airag.llm.handler;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.Getter;
import java.util.Map;
/**
* for [QQYUN-13565]【AI助手】新增创建用户和查询用户的工具扩展
* @Description: jeecg llm工具提供者
* @Author: chenrui
* @Date: 2025/8/26 18:06
*/
public interface JeecgToolsProvider {
/**
* 获取默认的工具列表
* @return
* @author chenrui
* @date 2025/8/27 09:49
*/
public Map<ToolSpecification, ToolExecutor> getDefaultTools();
/**
* jeecgLlm工具类
* @author chenrui
* @date 2025/8/27 09:49
*/
@Getter
class JeecgLlmTools{
ToolSpecification toolSpecification;
ToolExecutor toolExecutor;
public JeecgLlmTools(ToolSpecification toolSpecification, ToolExecutor toolExecutor) {
this.toolSpecification = toolSpecification;
this.toolExecutor = toolExecutor;
}
}
}

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