Compare commits

...

115 Commits

Author SHA1 Message Date
97c76675e7 更新DESFORM_NAME_MAX_LENGTH常量值为40 2026-01-22 14:25:56 +08:00
d335ba8612 【issues/9282】下拉搜索框设置为自定义数据字典时,生成代码后台报错 #9282 2026-01-22 10:25:49 +08:00
3cd987b2e9 SysDataSourceController的queryOptions建议添加权限检查 #9288 2026-01-22 10:04:24 +08:00
03f27067d4 v3.6.1 数据库脚本 2026-01-22 10:01:42 +08:00
e94cf00ec0 默认激活微服务 profile 2026-01-22 09:51:44 +08:00
c71e6a6d25 升级版本号 v3.9.1 2026-01-22 09:31:15 +08:00
030dc503eb 升级online依赖 2026-01-22 09:23:46 +08:00
918b59120e 前端升级online依赖和打印组件 2026-01-22 09:23:32 +08:00
e2402c75b0 v3.9.1 后台代码 2026-01-21 19:02:45 +08:00
3735ca1687 前端源码 v3.9.1 2026-01-21 19:00:29 +08:00
be466f0b03 前端源码 v3.9.1 2026-01-21 19:00:21 +08:00
4092eed2a2 升级版本号 v3.9.1 2026-01-21 19:00:06 +08:00
901f05ed21 前端源码 v3.9.1 2026-01-21 18:14:55 +08:00
41877a6e8b 【issues/9275】用户组件第二次点击取消时勾选值还是回显了 2026-01-20 12:02:54 +08:00
b7a3da89ca 同步钉钉部门报错 #9228 2026-01-09 14:39:15 +08:00
de4a8ce652 --author:scott--date:20260108--for:升级minidao版本至1.10.18 2026-01-08 16:20:28 +08:00
e533af285c 【issues/9217】当配置了pagination: true时,BasicTable组件自适应高度异常 2025-12-31 16:43:12 +08:00
23dc7b3f03 字段类型是int,生成的代码,字典未匹配上,默认加上转换 2025-12-26 10:54:38 +08:00
e57aef0708 使用useListPage的导出异常 #9209 2025-12-26 10:50:11 +08:00
42087c0bf8 国际化 2025-12-24 18:27:13 +08:00
606edcc82f 国际化 2025-12-24 18:06:13 +08:00
9082e986f1 国际化 2025-12-24 18:05:16 +08:00
40cd525bba Merge branch 'main' of https://github.com/jeecgboot/jeecg-boot 2025-12-17 18:38:32 +08:00
d6b6cf079e 支持更多AI模型 2025-12-17 18:38:04 +08:00
1b688e7cd2 添加JUnit平台启动器依赖用于测试 2025-12-16 14:09:54 +08:00
58915a6410 Merge pull request #8878 from WolfCat-ICE/patch-1
Update renderUtils.ts 修复字典渲染renderTag使用tag渲染没使用字典配置颜色的问题
2025-12-15 17:42:56 +08:00
b67096dc54 Merge pull request #9004 from SunJary/patch-1
fix#9002 解决字典注解查询出现异常之后,数据源不能恢复问题
2025-12-15 17:24:33 +08:00
67795493bd 支持加签注解 @SignatureCheck,针对获取租户信息接口加签
【严重安全漏洞】用户可加入任意租户 #9196
2025-12-15 17:07:22 +08:00
e1c8f00bf2 【严重安全漏洞】用户可加入任意租户 #9196
jeecgboot模式的租户未做申请加入租户和审批逻辑,所以这俩接口注释掉
2025-12-15 17:02:16 +08:00
17a81e89a5 “用于后端字典翻译”,同一枚举dictCode,keys传多个也只add第1个DictModel #9124 2025-12-15 15:04:17 +08:00
bcbf775756 列表选字段导出异常 #9173 2025-12-15 10:56:06 +08:00
462365890e 表单添加了按钮并设置排序,代码生成报错 #9190 2025-12-15 10:49:19 +08:00
b686f9fbd1 【严重安全漏洞】未授权用户可强制任意在线用户下线,存在DOS攻击风险 #9195-- 2025-12-15 09:25:12 +08:00
872f84d006 【issues/9169】切换页码时,pageChange事件加载了两次 2025-12-10 09:43:36 +08:00
26087172df 更新文档说明 2025-12-04 14:57:41 +08:00
281c3ff3c8 措辞优化 2025-12-04 13:55:53 +08:00
38d44c2487 修改一个参数,就实现默认的四个系统主题快速切换 2025-12-03 19:55:13 +08:00
8c88f8adf5 【issues/9098】tabs标签页关闭异常 2025-12-03 19:06:21 +08:00
526734c5a5 v3.9.0 其他数据库脚本,增加aiflow调试接口权限 2025-12-02 12:19:35 +08:00
44b48ad916 AI流程调试接口加权限,存在命令执行漏洞 #9144 2025-12-01 15:18:34 +08:00
1a3ae4f61c 解决AI流程两个严重问题
1、AI流程调试接口,存在命令执行漏洞 #9144
2、AI流程导入后缀改成 *.jeecgai
2025-12-01 15:13:18 +08:00
859c509f08 解决AI流程两个严重问题
1、AI流程调试接口,存在命令执行漏洞 #9144
2、AI流程导入后缀改成 *.jeecgai
2025-12-01 15:13:11 +08:00
0704f187af 遗漏方法提交 2025-12-01 13:52:23 +08:00
199d2b439e GUI代码生成,更新文档地址 2025-11-28 15:59:10 +08:00
5f898ed034 V3.9.0 微服务启动失败 缺少配置application-liteflow.yml 2025-11-28 15:06:06 +08:00
5a9cb05c86 V3.9.0 打包太大,默认不集成积木报表的nosql支持包 2025-11-28 11:47:44 +08:00
98936680d5 V3.9.0 打包太大,删除demo模块无用代码 2025-11-28 11:42:58 +08:00
fc043fd5f3 V3.9.0 打包太大,删除非必须依赖 2025-11-28 11:35:15 +08:00
54531002a7 v3.9.0 oracle、SqlServer、postgresql初始化脚本 2025-11-28 11:04:50 +08:00
728a95c00d 描述 2025-11-28 10:09:04 +08:00
13c9951c1f 优化描述 2025-11-28 10:05:36 +08:00
8dfaa3c3e1 优化 2025-11-28 10:02:23 +08:00
050c478dce 产品描述优化 2025-11-28 10:00:44 +08:00
2688e8b6e2 解决yarn安装,启动报错问题 2025-11-27 22:32:09 +08:00
a9f30f0ca5 解决yarn安装,启动报错问题 2025-11-27 22:25:57 +08:00
1bf4a0595a V3.9.0 修复部门老数据,类型不对 2025-11-27 18:50:13 +08:00
74cd57fd99 V3.9.0 Oracle11g 数据库 登录提示 无效的列类型: 1111 #9145 2025-11-27 18:44:35 +08:00
c9ac4c9945 v3.9.0 修复历史机构的部门类型 2025-11-27 18:42:27 +08:00
3b3371ee1a v3.9.0 更新数据库 2025-11-27 18:21:31 +08:00
1d3bde9fe7 MCP示例 2025-11-27 15:46:51 +08:00
90b50a51a7 简化MCP示例 2025-11-27 15:46:40 +08:00
668ac59a5c AI聊天创建用户等功能,移除工号和邮箱的唯一性要求 2025-11-27 15:36:57 +08:00
5f01bdd29b v3.9.0 增加MCP插件示例数据 2025-11-27 13:23:56 +08:00
82bfcc7b14 启动脚本,host检查失败,不影响启动 2025-11-27 11:59:04 +08:00
ef210a2242 前端依赖包有问题,导致build报错 2025-11-27 11:53:01 +08:00
435445cb4e v3.9.0 APP推送方法优化 2025-11-26 22:22:25 +08:00
fdf7cd1f6b v3.9.0 数据库更新,解决多屏大屏示例不展示问题 2025-11-26 13:42:36 +08:00
92a38f41b0 v3.9.0 数据库升级 2025-11-26 12:11:25 +08:00
7ed3bcc912 分支名称调整 2025-11-26 11:41:35 +08:00
2706c0a519 v3.9.0 支持MCP和插件,实现聊天式业务操作(如 “一句话创建用户”)! 2025-11-26 11:38:16 +08:00
f4b80365a9 描述修改 2025-11-26 11:36:56 +08:00
1108aa5288 更新版本号至v3.9.0 2025-11-26 11:29:52 +08:00
9919ae2bc5 v3.9.0 里程碑版本发布 2025-11-26 11:25:35 +08:00
1f73837b7d uniapp推送设置为空 2025-11-26 11:25:30 +08:00
9571e0b169 v3.9.0 里程碑版本发布 2025-11-26 11:14:47 +08:00
1a923596db 显示说明增量sql位置 2025-11-25 17:11:47 +08:00
62549e0a1c 打开flyway 数据库自动升级 2025-11-25 17:11:28 +08:00
2740a2f419 升级积木报表和积木BI到v2.2.0、poi升级到5 2025-11-24 22:04:12 +08:00
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
adc191f03e fix#9002 解决字典注解查询出现异常之后,数据源不能恢复问题 2025-10-20 22:21:47 +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
f6f2ef6316 Update renderUtils.ts 修复字典渲染renderTag使用tag渲染没使用字典配置颜色的问题
在renderDict方法中增加颜色属性传递,支持标签颜色渲染
render.renderDict(text, 'bpm_status',true)
2025-09-19 18:07:21 +08:00
971 changed files with 67711 additions and 175550 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

@ -3,9 +3,6 @@ AIGC应用平台介绍
一个全栈式 AI 开发平台,旨在帮助开发者快速构建和部署个性化的 AI 应用。
> JDK说明AI流程编排引擎暂时不支持jdk21所以目前只能使用jdk8或者jdk17启动项目。
JeecgBoot平台的AIGC功能模块是一套类似`Dify``AIGC应用开发平台`+`知识库问答`是一款基于LLM大语言模型AI应用平台和 RAG 的知识库问答系统。
其直观的界面结合了 AI 流程编排、RAG 管道、知识库管理、模型管理、对接向量库、实时运行可观察等让您可以快速从原型到生产拥有AI服务能力。
@ -109,6 +106,10 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
| ChatGTP | √ |
| Qwq | √ |
| 智库 | √ |
| claude | √ |
| vl模型 | √ |
| 千帆大模型 | √ |
| 通义千问 | √ |
| Ollama本地搭建大模型 | √ |
| 等等。。 | √ |

View File

@ -1,124 +0,0 @@
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)

View File

@ -1,4 +1,4 @@
[中文](./README.md) | English
![JEECG](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/logov3.png "JeecgBoot低代码开发平台")
@ -7,12 +7,12 @@
JEECG BOOT AI Low Code Platform
===============
Current version: 3.8.3 (Release date: 2025-10-09)
Current version: 3.9.1 (Release date: 2026-01-22)
[![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.2-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.9.1-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)

View File

@ -1,14 +1,15 @@
中文 | [English](./README.en-US.md)
JeecgBoot AI低代码平台
===============
当前最新版本: 3.8.3发布日期2025-10-09
当前最新版本: 3.9.1发布日期2026-01-22
[![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)](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)
[![](https://img.shields.io/badge/version-3.9.1-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)
@ -19,14 +20,17 @@ JeecgBoot AI低代码平台
<h3 align="center">企业级AI低代码平台</h3>
JeecgBoot 是一款基于BPM流程和代码生成AI低代码平台助力企业快速实现低代码开发和构建AI应用。
JeecgBoot 是一款融合代码生成AI应用的低代码开发平台助力企业快速实现低代码开发和构建AI应用。平台支持MCP和插件扩展提供聊天式业务操作(如“一句话创建用户”),大幅提升开发效率与用户便捷性。
采用前后端分离架构Ant Design&Vue3SpringBoot3SpringCloud AlibabaMybatis-plus强大代码生成器实现前后端一键生成无需手写代码。
平台引领AI低代码开发模式AI生成→在线编码→代码生成→手工合并解决Java项目80%重复工作,提升效率,节省成本,兼顾灵活性。
具备强大且颗粒化的权限控制支持按钮权限和数据权限设置满足大型业务系统需求。功能涵盖在线表单、表单设计、流程设计、门户设计、报表与大屏设计、OA办公、AI应用、AI知识库、大模型管理、AI流程编排、AI聊天支持ChatGPT、DeepSeek、Ollama等多种AI大模型。
`AI赋能报表:` 积木报表是一款自主研发的强大开源企业级Web报表与大屏工具。它通过零编码的拖拽式操作,赋能用户如同搭积木般轻松构建各类复杂报表和数据大屏,全面满足企业数据可视化与分析需求,助力企业级数据产品的高效打造与应用。
`傻瓜式报表:` JimuReport是一款自主研发的强大开源企业级Web报表工具。它通过零编码的拖拽式操作赋能用户如同搭积木般轻松构建各类复杂报表全面满足企业数据可视化与分析需求助力企业级数据产品的高效打造与应用。
`AI赋能低代码:` 提供完善成熟的AI应用平台涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表等多项功能。平台兼容多种主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合
`傻瓜式大屏:` JimuBI一款自主研发的强大的大屏和仪表盘设计工具。专注数字孪生与数据可视化支持交互式大屏、仪表盘、门户和移动端实现“一次开发多端适配”。 大屏设计类Word风格支持多屏切换自由拖拽轻松打造炫酷动态界面
`成熟AI应用功能:` 提供一套完善AI应用平台: 涵盖AI应用管理、AI模型管理、智能对话助手、知识库问答、流程编排与设计器、AI建表、MCP插件配置等功能。平台兼容主流大模型包括ChatGPT、DeepSeek、Ollama、智普、千问等助力企业高效构建智能化应用推动低代码开发与AI深度融合。
`JEECG宗旨是:` JEECG旨在通过OnlineCoding平台实现简单功能的零代码快速搭建同时针对复杂功能采用代码生成器生成代码并手工合并打造智能且灵活的低代码开发模式有效解决了当前低代码产品普遍缺乏灵活性的问题提升开发效率的同时兼顾系统的扩展性和定制化能力。
@ -50,15 +54,16 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
版本说明
-----------------------------------
|下载 | JDK17 + SpringBoot3.3 + Shiro |JDK17 + SpringBoot3.3+ SpringAuthorizationServer | JDK17/JDK8 + SpringBoot2.7 |
|------|----------------------------------------------------|--------------------------------------------|--------------------------------------------|
| Github | [`springboot3`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 |[`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) 分支 |[`master`](https://gitee.com/jeecg/JeecgBoot) 分支 |
|下载 | SpringBoot3.5 + Shiro |SpringBoot3.5+ SpringAuthorizationServer | SpringBoot3.5 + Sa-Token | SpringBoot2.7(JDK17/JDK8) |
|------|---------------------------------------------------------|----------------------------|-------------------|--------------------------------------------|
| Github | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3_sas) 分支 | [`springboot3-satoken`](https://github.com/jeecgboot/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支|
| Gitee | [`main`](https://github.com/jeecgboot/JeecgBoot) | [`springboot3_sas`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3_sas) 分支| [`springboot3-satoken`](https://gitee.com/jeecg/JeecgBoot/tree/springboot3-satoken) 分支|[`springboot2`](https://github.com/jeecgboot/JeecgBoot/tree/springboot2) 分支 |
- `jeecg-boot` 是后端JAVA源码项目Springboot3+SpringCloudAlibaba支持单体和微服务切换.
- `jeecg-boot` 是后端JAVA源码项目Springboot3+Shiro+Mybatis+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制作一个精简版本
@ -83,6 +88,7 @@ JeecgBoot低代码平台兼容所有J2EE项目开发支持信创国产化
- 官方网站: [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(满)、其他(满)
@ -157,6 +163,9 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
#### 前端
- 前端环境要求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、动态菜单、权限校验、按钮级别权限控制等功能
@ -224,20 +233,6 @@ JeecgBoot平台提供了一套完善的AI应用管理系统模块是一套类
开源版与企业版区别?
-----------------------------------
- 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 产品功能蓝图
![功能蓝图](https://jeecgos.oss-cn-beijing.aliyuncs.com/upload/test/Jeecg-Boot-lantu202005_1590912449914.jpg "在这里输入图片标题")

View File

@ -1,13 +1,12 @@
JeecgBoot 低代码开发平台
===============
当前最新版本: 3.8.3发布日期2025-09-22
当前最新版本: 3.9.1(发布日期: 2026-01-22
[![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.3-brightgreen.svg)](https://github.com/zhangdaiscott/jeecg-boot)
[![](https://img.shields.io/badge/version-3.9.1-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 +15,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交流群 964611995、⑩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 +144,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

@ -63,6 +63,7 @@ services:
build:
context: ./jeecg-module-system/jeecg-system-start
restart: on-failure
mac_address: 02:42:ac:11:00:02
depends_on:
- jeecg-boot-mysql
- jeecg-boot-redis

View File

@ -4,7 +4,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.8.3</version>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-base-core</artifactId>
@ -44,6 +44,12 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-common</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--集成springmvc框架并实现自动配置 -->
<dependency>
@ -173,7 +179,6 @@
<artifactId>DmDialect-for-hibernate5.0</artifactId>
<version>${dm8.version}</version>
</dependency>
<!-- Quartz定时任务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
@ -223,6 +228,12 @@
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<exclusions>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
@ -253,13 +264,6 @@
</exclusions>
</dependency>
<!-- <dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j-spring-boot-starter.version}</version>
</dependency>-->
<!-- knife4j 升级springboot3.4.5报错 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
@ -291,8 +295,8 @@
<!-- AutoPoi Excel工具类-->
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>autopoi-web</artifactId>
<groupId>org.jeecgframework</groupId>
<artifactId>autopoi-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>xerces</groupId>
@ -301,7 +305,7 @@
<optional>true</optional>
</dependency>
<!-- mini文件存储服务 -->
<!-- minio文件存储服务 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
@ -310,6 +314,14 @@
<artifactId>checker-qual</artifactId>
<groupId>org.checkerframework</groupId>
</exclusion>
<exclusion>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_annotations</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
@ -367,5 +379,21 @@
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- 腾讯云 -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>${tencentcloud-sdk-java-sms.version}</version>
<exclusions>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@ -132,7 +132,6 @@ public interface CommonAPI {
*/
Map<String, List<DictModel>> translateManyDict(String dictCodes, String keys);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 15 字典表的 翻译,可批量
* @param table
@ -143,7 +142,6 @@ public interface CommonAPI {
* @return
*/
List<DictModel> translateDictFromTableByKeys(String table, String text, String code, String keys, String dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 16 运行AIRag流程

View File

@ -33,4 +33,10 @@ public class AiragFlowDTO implements Serializable {
* 输入参数
*/
private Map<String, Object> inputParams;
/**
* 是否流式返回
*/
private boolean isStream;
}

View File

@ -0,0 +1,48 @@
package org.jeecg.common.api.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 流程审批意见DTO
* @author scott
* @date 2025-01-29
*/
@Data
public class ApprovalCommentDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 任务ID
*/
private String taskId;
/**
* 任务名称
*/
private String taskName;
/**
* 审批人ID
*/
private String approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 审批意见
*/
private String approvalComment;
/**
* 审批时间
*/
private Date approvalTime;
}

View File

@ -30,12 +30,10 @@ public class OnlineAuthDTO implements Serializable {
*/
private String onlineFormUrl;
//update-begin---author:chenrui ---date:20240123 for[QQYUN-7992]【online】工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
/**
* online工单的地址
*/
private String onlineWorkOrderUrl;
//update-end---author:chenrui ---date:20240123 for[QQYUN-7992]【online】工单申请下的online表单未配置online表单开发菜单操作报错无权限------------
public OnlineAuthDTO(){

View File

@ -0,0 +1,55 @@
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.List;
import java.util.Map;
/**
* 移动端消息推送
* @author liusq
* @date 2025/11/12 14:11
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PushMessageDTO implements Serializable {
private static final long serialVersionUID = 7431775881170684867L;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 推送形式all全推送 single单用户推送
*/
private String pushType;
/**
* 用户名usernameList
*/
List<String> usernames;
/**
* 用户名idList
*/
List<String> userIds;
/**
* 消息附加参数
*/
Map<String,Object> payload;
}

View File

@ -121,9 +121,8 @@ public class AutoLogAspect {
if (operateType > 0) {
return operateType;
}
//update-begin---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
// 代码逻辑说明: 阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
return OperateTypeEnum.getTypeByMethodName(methodName);
//update-end---author:wangshuai ---date:20220331 for阿里云代码扫描规范(不允许任何魔法值出现在代码中)------------
}
/**
@ -143,14 +142,15 @@ public class AutoLogAspect {
// https://my.oschina.net/mengzhang6/blog/2395893
Object[] arguments = new Object[paramsArray.length];
for (int i = 0; i < paramsArray.length; i++) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile) {
if (paramsArray[i] instanceof BindingResult || paramsArray[i] instanceof ServletRequest || paramsArray[i] instanceof ServletResponse || paramsArray[i] instanceof MultipartFile || paramsArray[i] instanceof MultipartFile[]) {
//ServletRequest不能序列化从入参里排除否则报异常java.lang.IllegalStateException: It is illegal to call this method if the current request is not in asynchronous mode (i.e. isAsyncStarted() returns false)
//ServletResponse不能序列化 从入参里排除否则报异常java.lang.IllegalStateException: getOutputStream() has already been called for this response
//MultipartFile和MultipartFile[]不能序列化,从入参里排除
continue;
}
arguments[i] = paramsArray[i];
}
//update-begin-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
// 代码逻辑说明: 日志数据太长的直接过滤掉
PropertyFilter profilter = new PropertyFilter() {
@Override
public boolean apply(Object o, String name, Object value) {
@ -165,14 +165,13 @@ public class AutoLogAspect {
}
};
params = JSONObject.toJSONString(arguments, profilter);
//update-end-author:taoyan date:20200724 for:日志数据太长的直接过滤掉
} else {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
StandardReflectionParameterNameDiscoverer u=new StandardReflectionParameterNameDiscoverer();
StandardReflectionParameterNameDiscoverer u= new StandardReflectionParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
for (int i = 0; i < args.length; i++) {

View File

@ -105,29 +105,24 @@ public class DictAspect {
Map<String, List<String>> dataListMap = new HashMap<>(5);
//取出结果集
List<Object> records=((IPage) ((Result) result).getResult()).getRecords();
//update-begin--Author:zyf -- Date:20220606 ----for【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
// 代码逻辑说明: 【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
Boolean hasDict= checkHasDict(records);
if(!hasDict){
return result;
}
log.debug(" __ 进入字典翻译切面 DictAspect —— " );
//update-end--Author:zyf -- Date:20220606 ----for【VUEN-1230】 判断是否含有字典注解,没有注解返回-----
for (Object record : records) {
String json="{}";
try {
//update-begin--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
json = objectMapper.writeValueAsString(record);
//update-end--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
} catch (JsonProcessingException e) {
log.error("json解析失败"+e.getMessage(),e);
}
//update-begin--Author:scott -- Date:20211223 ----for【issues/3303】restcontroller返回json数据后key顺序错乱 -----
// 代码逻辑说明: 【issues/3303】restcontroller返回json数据后key顺序错乱 -----
JSONObject item = JSONObject.parseObject(json, Feature.OrderedField);
//update-end--Author:scott -- Date:20211223 ----for【issues/3303】restcontroller返回json数据后key顺序错乱 -----
//update-begin--Author:scott -- Date:20190603 ----for解决继承实体字段无法翻译问题------
//for (Field field : record.getClass().getDeclaredFields()) {
// 遍历所有字段把字典Code取出来放到 map 里
for (Field field : oConvertUtils.getAllFields(record)) {
@ -135,7 +130,6 @@ public class DictAspect {
if (oConvertUtils.isEmpty(value)) {
continue;
}
//update-end--Author:scott -- Date:20190603 ----for解决继承实体字段无法翻译问题------
if (field.getAnnotation(Dict.class) != null) {
if (!dictFieldList.contains(field)) {
dictFieldList.add(field);
@ -143,26 +137,22 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
List<String> dataList;
String dictCode = code;
if (!StringUtils.isEmpty(table)) {
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
dictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
dataList = dataListMap.computeIfAbsent(dictCode, k -> new ArrayList<>());
this.listAddAllDeduplicate(dataList, Arrays.asList(value.split(",")));
}
//date类型默认转换string格式化日期
//update-begin--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
//if (JAVA_UTIL_DATE.equals(field.getType().getName())&&field.getAnnotation(JsonFormat.class)==null&&item.get(field.getName())!=null){
//SimpleDateFormat aDate=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
//}
//update-end--Author:zyf -- Date:20220531 ----for【issues/#3629】 DictAspect Jackson序列化报错-----
}
items.add(item);
}
@ -176,15 +166,12 @@ public class DictAspect {
String code = field.getAnnotation(Dict.class).dicCode();
String text = field.getAnnotation(Dict.class).dicText();
String table = field.getAnnotation(Dict.class).dictTable();
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的字典表数据源
String dataSource = field.getAnnotation(Dict.class).ds();
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
String fieldDictCode = code;
if (!StringUtils.isEmpty(table)) {
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 代码逻辑说明: [issues/#5643]解决分布式下表字典跨库无法查询问题------------
fieldDictCode = String.format("%s,%s,%s,%s", table, text, code, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}
String value = record.getString(field.getName());
@ -286,25 +273,20 @@ public class DictAspect {
String[] arr = dictCode.split(",");
String table = arr[0], text = arr[1], code = arr[2];
String values = String.join(",", needTranslDataTable);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
// 自定义的数据源
String dataSource = null;
if (arr.length > 3) {
dataSource = arr[3];
}
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.dictCode:" + dictCode);
log.debug("translateDictFromTableByKeys.values:" + values);
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
//update-begin---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
// 代码逻辑说明: 微服务下为空报错没有参数需要传递空字符串---
if(null == dataSource){
dataSource = "";
}
//update-end---author:wangshuai---date:2024-01-09---for:微服务下为空报错没有参数需要传递空字符串---
List<DictModel> texts = commonApi.translateDictFromTableByKeys(table, text, code, values, dataSource);
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
log.debug("translateDictFromTableByKeys.result:" + texts);
List<DictModel> list = translText.computeIfAbsent(dictCode, k -> new ArrayList<>());
list.addAll(texts);
@ -313,10 +295,8 @@ public class DictAspect {
for (DictModel dict : texts) {
String redisKey = String.format("sys:cache:dictTable::SimpleKey [%s,%s]", dictCode, dict.getValue());
try {
// update-begin-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
// 保留5分钟
redisTemplate.opsForValue().set(redisKey, dict.getText(), 300, TimeUnit.SECONDS);
// update-end-author:taoyan date:20211012 for: 字典表翻译注解缓存未更新 issues/3061
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
@ -400,7 +380,7 @@ public class DictAspect {
if (k.trim().length() == 0) {
continue; //跳过循环
}
//update-begin--Author:scott -- Date:20210531 ----for !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
// 代码逻辑说明: !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
if (!StringUtils.isEmpty(table)){
log.debug("--DictAspect------dicTable="+ table+" ,dicText= "+text+" ,dicCode="+code);
String keyString = String.format("sys:cache:dictTable::SimpleKey [%s,%s,%s,%s]",table,text,code,k.trim());
@ -425,7 +405,6 @@ public class DictAspect {
tmpValue = commonApi.translateDict(code, k.trim());
}
}
//update-end--Author:scott -- Date:20210531 ----for !56 优化微服务应用下存在表字段需要字典翻译时加载缓慢问题-----
if (tmpValue != null) {
if (!"".equals(textValue.toString())) {

View File

@ -57,7 +57,6 @@ public class PermissionDataAspect {
String requestMethod = request.getMethod();
String requestPath = request.getRequestURI().substring(request.getContextPath().length());
requestPath = filterUrl(requestPath);
//update-begin-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
//先判断是否online报表请求
if(requestPath.indexOf(UrlMatchEnum.CGREPORT_DATA.getMatchUrl())>=0 || requestPath.indexOf(UrlMatchEnum.CGREPORT_ONLY_DATA.getMatchUrl())>=0){
// 获取地址栏参数
@ -66,7 +65,6 @@ public class PermissionDataAspect {
requestPath+="?"+urlParamString;
}
}
//update-end-author:taoyan date:20211027 for:JTC-132【online报表权限】online报表带参数的菜单配置数据权限无效
log.debug("拦截请求 >> {} ; 请求类型 >> {} . ", requestPath, requestMethod);
String username = JwtUtil.getUserNameByToken(request);
//查询数据权限信息

View File

@ -41,7 +41,6 @@ public @interface Dict {
String dictTable() default "";
//update-begin---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
/**
* 方法描述: 数据字典表所在数据源名称
* 作 者: chenrui
@ -50,5 +49,4 @@ public @interface Dict {
* @return 返回类型: String
*/
String ds() default "";
//update-end---author:chenrui ---date:20231221 for[issues/#5643]解决分布式下表字典跨库无法查询问题------------
}

View File

@ -91,6 +91,23 @@ public interface CommonConstant {
public static String PREFIX_USER_SHIRO_CACHE = "shiro:cache:org.jeecg.config.shiro.ShiroRealm.authorizationCache:";
/** 登录用户Token令牌缓存KEY前缀 */
String PREFIX_USER_TOKEN = "prefix_user_token:";
/** 登录用户Token令牌作废提示信息比如 “不允许同一账号多地同时登录,会往这个变量存提示信息” */
String PREFIX_USER_TOKEN_ERROR_MSG = "prefix_user_token:error:msg_";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
/** 客户端类型PC端 */
String CLIENT_TYPE_PC = "PC";
/** 客户端类型APP端 */
String CLIENT_TYPE_APP = "APP";
/** 客户端类型:手机号登录 */
String CLIENT_TYPE_PHONE = "PHONE";
String PREFIX_USER_TOKEN_PC = "prefix_user_token:single_login:pc:";
/** 单点登录用户在APP端的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_APP = "prefix_user_token:single_login:app:";
/** 单点登录用户在手机号登录的Token缓存KEY前缀 (username -> token) */
String PREFIX_USER_TOKEN_PHONE = "prefix_user_token:single_login:phone:";
/**============================== 【是否允许同一账号多地同时登录】登录客户端类型常量 ==============================*/
// /** Token缓存时间3600秒即一小时 */
// int TOKEN_EXPIRE_TIME = 3600;
@ -308,6 +325,10 @@ public interface CommonConstant {
*/
String SYS_ROLE_ADMIN = "admin";
/**
* 考勤补卡业务状态 0处理中
*/
String SIGN_PATCH_BIZ_STATUS_0 = "0";
/**
* 考勤补卡业务状态 1同意 2不同意
*/
@ -591,7 +612,6 @@ public interface CommonConstant {
String ORDER_TYPE_DESC = "DESC";
//update-begin---author:scott ---date:2023-09-10 for积木报表常量----
/**
* 报表允许设计开发的角色
*/
@ -606,9 +626,7 @@ public interface CommonConstant {
* 数据隔离模式: 按照租户隔离
*/
public static final String SAAS_MODE_TENANT = "tenant";
//update-end---author:scott ---date::2023-09-10 for积木报表常量----
//update-begin---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号短信验证码redis-key的前缀
*/
@ -633,7 +651,6 @@ public interface CommonConstant {
* 修改手机号
*/
String UPDATE_PHONE = "updatePhone";
//update-end---author:wangshuai---date:2024-04-07---for:修改手机号常量---
/**
* 修改手机号验证码请求次数超出
@ -709,4 +726,19 @@ public interface CommonConstant {
* 部门名称redisKey(全路径)
*/
String DEPART_NAME_REDIS_KEY_PRE = "sys:cache:departPathName:";
/**
* 默认用户排序值
*/
Integer DEFAULT_USER_SORT = 1000;
/**
* 发送短信方式:腾讯
*/
String SMS_SEND_TYPE_TENCENT = "tencent";
/**
* 发送短信方式:阿里云
*/
String SMS_SEND_TYPE_ALI_YUN = "aliyun";
}

View File

@ -39,20 +39,18 @@ public class ProvinceCityArea {
this.initAreaList();
if(areaList!=null && areaList.size()>0){
for(int i=areaList.size()-1;i>=0;i--){
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
String areaText = areaList.get(i).getText();
String cityText = areaList.get(i).getAheadText();
if(text.indexOf(areaText)>=0 && (cityText!=null && text.indexOf(cityText)>=0)){
return areaList.get(i).getId();
}
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
}
}
}
return null;
}
// update-begin-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
/**
* 获取省市区code精准匹配
* @param texts 文本数组,省,市,区
@ -117,7 +115,6 @@ public class ProvinceCityArea {
}
return null;
}
// update-end-author:sunjianlei date:20220121 for:【JTC-704】数据导入错误 省市区组件,文件中为北京市,导入后,导为了山西省
public void getAreaByCode(String code,List<String> ls){
for(Area area: areaList){
@ -154,9 +151,8 @@ public class ProvinceCityArea {
for(String areaKey:areaJson.keySet()){
//System.out.println("········"+areaKey);
Area area = new Area(areaKey,areaJson.getString(areaKey),cityKey);
//update-begin-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
// 代码逻辑说明: VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
area.setAheadText(cityJson.getString(cityKey));
//update-end-author:taoyan date:2022-5-24 for:VUEN-1088 online 导入 省市区导入后 导入数据错乱 北京市/市辖区/西城区-->山西省/晋城市/城区
this.areaList.add(area);
}
}

View File

@ -47,4 +47,8 @@ public interface TenantConstant {
*/
String APP_ADMIN = "appAdmin";
/**
* 增加SignatureCheck注解POST请求的URL
*/
String[] SIGNATURE_CHECK_POST_URL = { "/sys/tenant/joinTenantByHouseNumber", "/sys/tenant/invitationUser" };
}

View File

@ -23,7 +23,11 @@ public enum NoticeTypeEnum {
/**
* 督办
*/
NOTICE_TYPE_SUPERVISE("督办管理", "supe");
NOTICE_TYPE_SUPERVISE("督办管理", "supe"),
/**
* 考勤
*/
NOTICE_TYPE_ATTENDANCE("考勤消息", "attendance");
/**
* 文件类型名称

View File

@ -0,0 +1,82 @@
package org.jeecg.common.constant.enums;
import org.jeecg.common.util.oConvertUtils;
/**
* UniPush 消息推送枚举
* @author: jeecg-boot
*/
public enum UniPushTypeEnum {
/**
* 聊天
*/
CHAT("chat", "聊天消息", "收到%s发来的聊天消息"),
/**
* 流程跳转到我的任务
*/
BPM("bpm_task", "待办任务", "收到%s待办任务"),
/**
* 流程抄送任务
*/
BPM_VIEW("bpm_cc", "知会任务", "收到%s知会任务"),
/**
* 系统消息
*/
SYS_MSG("system", "系统消息", "收到一条系统通告");
/**
* 业务类型(chat:聊天 bpm_task:流程 bpm_cc:流程抄送)
*/
private String type;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
UniPushTypeEnum(String type, String title, String content) {
this.type = type;
this.title = title;
this.content = content;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getTitle() {
return title ;
}
public void setTitle(String openType) {
this.title = openType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static UniPushTypeEnum getByType(String type) {
if (oConvertUtils.isEmpty(type)) {
return null;
}
for (UniPushTypeEnum val : values()) {
if (val.getType().equals(type)) {
return val;
}
}
return null;
}
}

View File

@ -73,7 +73,7 @@ public class SensitiveDataAspect {
SensitiveInfoUtil.handleNestedObject(result, entity, isEncode);
}
long endTime=System.currentTimeMillis();
log.info((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时" + (endTime - startTime) + "ms");
log.debug((isEncode ? "加密操作," : "解密操作,") + "Aspect程序耗时" + (endTime - startTime) + "ms");
return result;
}

View File

@ -387,7 +387,7 @@ public class JeecgElasticsearchTemplate {
data.remove("id");
bodySb.append(data.toJSONString()).append("\n");
}
System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
//System.out.println("+-+-+-: bodySb.toString(): " + bodySb.toString());
HttpHeaders headers = RestUtil.getHeaderApplicationJson();
RestUtil.request(url, HttpMethod.PUT, headers, null, bodySb, JSONObject.class);
return true;

View File

@ -121,13 +121,12 @@ public class JeecgBootExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e){
log.error(e.getMessage(), e);
//update-begin---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
// 代码逻辑说明: 处理Sentinel限流自定义异常
Throwable throwable = e.getCause();
SentinelErrorInfoEnum errorInfoEnum = SentinelErrorInfoEnum.getErrorByException(throwable);
if (ObjectUtil.isNotEmpty(errorInfoEnum)) {
return Result.error(errorInfoEnum.getError());
}
//update-end---author:zyf ---date:20220411 for处理Sentinel限流自定义异常
addSysLog(e);
return Result.error("操作失败,"+e.getMessage());
}
@ -224,7 +223,6 @@ public class JeecgBootExceptionHandler {
return Result.error("校验失败存在SQL注入风险" + msg);
}
//update-begin---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 添加异常新系统日志
* @param e 异常
@ -243,7 +241,6 @@ public class JeecgBootExceptionHandler {
} catch (NullPointerException | BeansException ignored) {
}
if (null != request) {
//update-begin---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
//请求的参数
if (!isTooBigException(e)) {
// 文件上传过大异常时不能获取参数,否则会报错
@ -252,7 +249,6 @@ public class JeecgBootExceptionHandler {
log.setMethod(oConvertUtils.mapToString(request.getParameterMap()));
}
}
//update-end---author:chenrui ---date:20250408 for[QQYUN-11716]上传大图片失败没有精确提示------------
// 请求地址
log.setRequestUrl(request.getRequestURI());
//设置IP地址
@ -276,7 +272,6 @@ public class JeecgBootExceptionHandler {
baseCommonService.addLog(log);
}
//update-end---author:chenrui ---date:20240423 for[QQYUN-8732]把错误的日志都抓取了 方便后续处理,单独弄个日志类型------------
/**
* 是否文件过大异常

View File

@ -68,12 +68,16 @@ public class JeecgController<T, S extends IService<T>> {
//此处设置的filename无效 ,前端会重更新设置一下
mv.addObject(NormalExcelConstants.FILE_NAME, title);
mv.addObject(NormalExcelConstants.CLASS, clazz);
//update-begin--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置--------------------
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title);
// 代码逻辑说明: 【QQYUN-13930】统一改成导出xlsx格式---
ExportParams exportParams=new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF);
exportParams.setImageBasePath(jeecgBaseConfig.getPath().getUpload());
//update-end--Author:liusq Date:20210126 for图片导出报错ImageBasePath未设置----------------------
mv.addObject(NormalExcelConstants.PARAMS,exportParams);
mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
// 代码逻辑说明: 【issues/9052】BasicTable列表页导出excel可以指定列---
String exportFields = request.getParameter(NormalExcelConstants.EXPORT_FIELDS);
if(oConvertUtils.isNotEmpty(exportFields)){
mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
}
return mv;
}
/**
@ -94,14 +98,12 @@ public class JeecgController<T, S extends IService<T>> {
// Step.2 计算分页sheet数据
double total = service.count();
int count = (int)Math.ceil(total/pageNum);
//update-begin-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.3 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
}
//update-end-author:liusq---date:20220629--for: 多sheet导出根据选择导出写法调整 ---
// Step.4 多sheet处理
List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
for (int i = 1; i <=count ; i++) {
@ -220,16 +222,15 @@ public class JeecgController<T, S extends IService<T>> {
params.setNeedSave(true);
try {
List<T> list = ExcelImportUtil.importExcel(file.getInputStream(), clazz, params);
//update-begin-author:taoyan date:20190528 for:批量插入数据
// 代码逻辑说明: 批量插入数据
long start = System.currentTimeMillis();
service.saveBatch(list);
//400条 saveBatch消耗时间1592毫秒 循环插入消耗时间1947毫秒
//1200条 saveBatch消耗时间3687毫秒 循环插入消耗时间5212毫秒
log.info("消耗时间" + (System.currentTimeMillis() - start) + "毫秒");
//update-end-author:taoyan date:20190528 for:批量插入数据
return Result.ok("文件导入成功!数据行数:" + list.size());
} catch (Exception e) {
//update-begin-author:taoyan date:20211124 for: 导入数据重复增加提示
// 代码逻辑说明: 导入数据重复增加提示
String msg = e.getMessage();
log.error(msg, e);
if(msg!=null && msg.indexOf("Duplicate entry")>=0){
@ -237,7 +238,6 @@ public class JeecgController<T, S extends IService<T>> {
}else{
return Result.error("文件导入失败:" + e.getMessage());
}
//update-end-author:taoyan date:20211124 for: 导入数据重复增加提示
} finally {
try {
file.getInputStream().close();

View File

@ -13,7 +13,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: Entity基类

View File

@ -97,7 +97,6 @@ public class QueryGenerator {
return queryWrapper;
}
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
/**
* 获取查询条件构造器QueryWrapper实例 通用查询条件已被封装完成
* @param searchObj 查询实体
@ -112,7 +111,6 @@ public class QueryGenerator {
log.debug("---查询条件构造器初始化完成,耗时:"+(System.currentTimeMillis()-start)+"毫秒----");
return queryWrapper;
}
//update-end---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
/**
* 组装Mybatis Plus 查询条件
@ -142,7 +140,6 @@ public class QueryGenerator {
}
String name, type, column;
// update-begin--Author:taoyan Date:20200923 forissues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
//定义实体字段和数据库字段名称的映射 高级查询中 只能获取实体字段 如果设置TableField注解 那么查询条件会出问题
Map<String,String> fieldColumnMap = new HashMap<>(5);
for (int i = 0; i < origDescriptors.length; i++) {
@ -188,7 +185,7 @@ public class QueryGenerator {
queryWrapper.and(j -> j.like(field,vals[0]));
}
}else {
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
// 代码逻辑说明: [TV360X-378]增加自定义字段查询规则功能------------
QueryRuleEnum rule;
if(null != customRuleMap && customRuleMap.containsKey(name)) {
// 有自定义规则,使用自定义规则.
@ -197,7 +194,6 @@ public class QueryGenerator {
//根据参数值带什么关键字符串判断走什么类型的查询
rule = convert2Rule(value);
}
//update-end---author:chenrui ---date:20240527 for[TV360X-378]增加自定义字段查询规则功能------------
value = replaceValue(rule,value);
// add -begin 添加判断为字符串时设为全模糊查询
//if( (rule==null || QueryRuleEnum.EQ.equals(rule)) && "class java.lang.String".equals(type)) {
@ -217,7 +213,6 @@ public class QueryGenerator {
//高级查询
doSuperQuery(queryWrapper, parameterMap, fieldColumnMap);
// update-end--Author:taoyan Date:20200923 forissues/1671 如果字段加注解了@TableField(exist = false),不走DB查询-------
}
@ -260,16 +255,16 @@ public class QueryGenerator {
}
if(oConvertUtils.isNotEmpty(column)){
log.info("单字段排序规则>> column:" + column + ",排序方式:" + order);
log.debug("单字段排序规则>> column:" + column + ",排序方式:" + order);
}
// 1. 列表多字段排序优先
if(parameterMap!=null&& parameterMap.containsKey("sortInfoString")) {
// 多字段排序
String sortInfoString = parameterMap.get("sortInfoString")[0];
log.info("多字段排序规则>> sortInfoString:" + sortInfoString);
log.debug("多字段排序规则>> sortInfoString:" + sortInfoString);
List<OrderItem> orderItemList = SqlConcatUtil.getQueryConditionOrders(column, order, sortInfoString);
log.info(orderItemList.toString());
log.debug(orderItemList.toString());
if (orderItemList != null && !orderItemList.isEmpty()) {
for (OrderItem item : orderItemList) {
// 一、获取排序数据库字段
@ -321,13 +316,11 @@ public class QueryGenerator {
return;
}
//update-begin-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
//TODO 避免用户自定义表无默认字段创建时间,导致排序报错
if(DataBaseConstant.CREATE_TIME.equals(column) && !fieldColumnMap.containsKey(DataBaseConstant.CREATE_TIME)){
column = "id";
log.warn("检测到实体里没有字段createTime改成采用ID排序");
}
//update-end-author:scott date:2022-11-07 for:避免用户自定义表无默认字段{创建时间},导致排序报错
if (oConvertUtils.isNotEmpty(column) && oConvertUtils.isNotEmpty(order)) {
//字典字段,去掉字典翻译文本后缀
@ -335,15 +328,12 @@ public class QueryGenerator {
column = column.substring(0, column.lastIndexOf(CommonConstant.DICT_TEXT_SUFFIX));
}
//update-begin-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时使用SQL注入生效
//判断column是不是当前实体的
log.debug("当前字段有:"+ allFields);
if (!allColumnExist(column, allFields)) {
throw new JeecgBootException("请注意,将要排序的列字段不存在:" + column);
}
//update-end-author:taoyan date:2022-5-16 for: issues/3676 获取系统用户列表时使用SQL注入生效
//update-begin-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
//多字段排序方法没有读取 MybatisPlus 注解 @TableField 里 value 的值
if (column.contains(",")) {
List<String> columnList = Arrays.asList(column.split(","));
@ -354,12 +344,10 @@ public class QueryGenerator {
}else{
column = fieldColumnMap.get(column);
}
//update-end-author:scott date:2022-10-10 for:【jeecg-boot/issues/I5FJU6】doMultiFieldsOrder() 多字段排序方法存在问题
//SQL注入check
SqlInjectionUtil.filterContentMulti(column);
//update-begin--Author:scott Date:20210531 for36 多条件排序无效问题修正-------
// 排序规则修改
// 将现有排序 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1,column2 desc"
// 修改为 _ 前端传递排序条件{....,column: 'column1,column2',order: 'desc'} 翻译成sql "column1 desc,column2 desc"
@ -368,11 +356,9 @@ public class QueryGenerator {
} else {
queryWrapper.orderByDesc(SqlInjectionUtil.getSqlInjectSortFields(column.split(",")));
}
//update-end--Author:scott Date:20210531 for36 多条件排序无效问题修正-------
}
}
//update-begin-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时使用SQL注入生效
/**
* 多字段排序 判断所传字段是否存在
* @return
@ -392,7 +378,6 @@ public class QueryGenerator {
}
return exist;
}
//update-end-author:taoyan date:2022-5-23 for: issues/3676 获取系统用户列表时使用SQL注入生效
/**
* 高级查询
@ -405,14 +390,14 @@ public class QueryGenerator {
String superQueryParams = parameterMap.get(SUPER_QUERY_PARAMS)[0];
String superQueryMatchType = parameterMap.get(SUPER_QUERY_MATCH_TYPE) != null ? parameterMap.get(SUPER_QUERY_MATCH_TYPE)[0] : MatchTypeEnum.AND.getValue();
MatchTypeEnum matchType = MatchTypeEnum.getByValue(superQueryMatchType);
// update-begin--Author:sunjianlei Date:20200325 for高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
// 代码逻辑说明: 高级查询的条件要用括号括起来,防止和用户的其他条件冲突 -------
try {
superQueryParams = URLDecoder.decode(superQueryParams, "UTF-8");
List<QueryCondition> conditions = JSON.parseArray(superQueryParams, QueryCondition.class);
if (conditions == null || conditions.size() == 0) {
return;
}
// update-begin-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
// 代码逻辑说明: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
List<QueryCondition> filterConditions = conditions.stream().filter(
rule -> (oConvertUtils.isNotEmpty(rule.getField())
&& oConvertUtils.isNotEmpty(rule.getRule())
@ -423,7 +408,6 @@ public class QueryGenerator {
if (filterConditions.size() == 0) {
return;
}
// update-end-author:sunjianlei date:20220119 for: 【JTC-573】 过滤空条件查询,防止 sql 拼接多余的 and
log.debug("---高级查询参数-->" + filterConditions);
queryWrapper.and(andWrapper -> {
@ -438,14 +422,14 @@ public class QueryGenerator {
log.debug("SuperQuery ==> " + rule.toString());
//update-begin-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
// 代码逻辑说明: 【高级查询】 oracle 日期等于查询报错
Object queryValue = rule.getVal();
if("date".equals(rule.getType())){
queryValue = DateUtils.str2Date(rule.getVal(),DateUtils.date_sdf.get());
}else if("datetime".equals(rule.getType())){
queryValue = DateUtils.str2Date(rule.getVal(), DateUtils.datetimeFormat.get());
}
// update-begin--author:sunjianlei date:20210702 for【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
String dbType = rule.getDbType();
if (oConvertUtils.isNotEmpty(dbType)) {
try {
@ -478,9 +462,8 @@ public class QueryGenerator {
log.error("高级查询值转换失败:", e);
}
}
// update-begin--author:sunjianlei date:20210702 for【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
// 代码逻辑说明: 【/issues/I3VR8E】高级查询没有类型转换查询参数都是字符串类型 ----
addEasyQuery(andWrapper, fieldColumnMap.get(rule.getField()), QueryRuleEnum.getByValue(rule.getRule()), queryValue);
//update-end-author:taoyan date:20201228 for: 【高级查询】 oracle 日期等于查询报错
// 如果拼接方式是OR就拼接OR
if (MatchTypeEnum.OR == matchType && i < (filterConditions.size() - 1)) {
@ -496,7 +479,6 @@ public class QueryGenerator {
log.error("--高级查询拼接失败:" + e.getMessage());
e.printStackTrace();
}
// update-end--Author:sunjianlei Date:20200325 for高级查询的条件要用括号括起来防止和用户的其他条件冲突 -------
}
//log.info(" superQuery getCustomSqlSegment: "+ queryWrapper.getCustomSqlSegment());
}
@ -508,7 +490,7 @@ public class QueryGenerator {
*/
public static QueryRuleEnum convert2Rule(Object value) {
// 避免空数据
// update-begin-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
// 代码逻辑说明: 查询条件输入空格导致return null后续判断导致抛出null异常
if (value == null) {
return QueryRuleEnum.EQ;
}
@ -516,10 +498,8 @@ public class QueryGenerator {
if (val.length() == 0) {
return QueryRuleEnum.EQ;
}
// update-end-author:taoyan date:20210629 for: 查询条件输入空格导致return null后续判断导致抛出null异常
QueryRuleEnum rule =null;
//update-begin--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
//TODO 此处规则,只适用于 le lt ge gt
// step 2 .>= =<
int length2 = 2;
@ -535,14 +515,12 @@ public class QueryGenerator {
rule = QueryRuleEnum.getByValue(val.substring(0, 1));
}
}
//update-end--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284---------------------
// step 3 like
//update-begin-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
// 代码逻辑说明: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
if(rule == null && val.equals(STAR)){
rule = QueryRuleEnum.EQ;
}
//update-end-author:taoyan for: /issues/3382 默认带*就走模糊,但是如果只有一个*,那么走等于查询
if (rule == null && val.contains(STAR)) {
if (val.startsWith(STAR) && val.endsWith(STAR)) {
rule = QueryRuleEnum.LIKE;
@ -567,12 +545,10 @@ public class QueryGenerator {
rule = QueryRuleEnum.EQ_WITH_ADD;
}
//update-begin--Author:taoyan Date:20201229 forinitQueryWrapper组装sql查询条件错误 #284---------------------
//特殊处理Oracle的表达式to_date('xxx','yyyy-MM-dd')含有逗号会被识别为in查询转为等于查询
if(rule == QueryRuleEnum.IN && val.indexOf(YYYY_MM_DD)>=0 && val.indexOf(TO_DATE)>=0){
rule = QueryRuleEnum.EQ;
}
//update-end--Author:taoyan Date:20201229 forinitQueryWrapper组装sql查询条件错误 #284---------------------
return rule != null ? rule : QueryRuleEnum.EQ;
}
@ -592,11 +568,10 @@ public class QueryGenerator {
return value;
}
String val = (value + "").toString().trim();
//update-begin-author:taoyan date:20220302 for: 查询条件的值为等号(=bug #3443
// 代码逻辑说明: 查询条件的值为等号(=bug #3443
if(QueryRuleEnum.EQ.getValue().equals(val)){
return val;
}
//update-end-author:taoyan date:20220302 for: 查询条件的值为等号(=bug #3443
if (rule == QueryRuleEnum.LIKE) {
value = val.substring(1, val.length() - 1);
//mysql 模糊查询之特殊字符下划线 _、\
@ -614,21 +589,19 @@ public class QueryGenerator {
} else if (rule == QueryRuleEnum.EQ_WITH_ADD) {
value = val.replaceAll("\\+\\+", COMMA);
}else {
//update-begin--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
// 代码逻辑说明: initQueryWrapper组装sql查询条件错误 #284-------------------
if(val.startsWith(rule.getValue())){
//TODO 此处逻辑应该注释掉-> 如果查询内容中带有查询匹配规则符号,就会被截取的(比如:>=您好)
value = val.replaceFirst(rule.getValue(),"");
}else if(val.startsWith(rule.getCondition()+QUERY_SEPARATE_KEYWORD)){
value = val.replaceFirst(rule.getCondition()+QUERY_SEPARATE_KEYWORD,"").trim();
}
//update-end--Author:scott Date:20190724 forinitQueryWrapper组装sql查询条件错误 #284-------------------
}
return value;
}
private static void addQueryByRule(QueryWrapper<?> queryWrapper,String name,String type,String value,QueryRuleEnum rule) throws ParseException {
if(oConvertUtils.isNotEmpty(value)) {
//update-begin--Author:sunjianlei Date:20220104 for【JTC-409】修复逗号分割情况下没有转换类型导致类型严格的数据库查询报错 -------------------
// 针对数字类型字段,多值查询
if(value.contains(COMMA)){
Object[] temp = Arrays.stream(value.split(COMMA)).map(v -> {
@ -644,7 +617,6 @@ public class QueryGenerator {
}
Object temp = QueryGenerator.parseByType(value, type, rule);
addEasyQuery(queryWrapper, name, rule, temp);
//update-end--Author:sunjianlei Date:20220104 for【JTC-409】修复逗号分割情况下没有转换类型导致类型严格的数据库查询报错 -------------------
}
}
@ -759,13 +731,12 @@ public class QueryGenerator {
}else if(value instanceof String[]) {
queryWrapper.in(name, (Object[]) value);
}
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
else if(value.getClass().isArray()) {
queryWrapper.in(name, (Object[])value);
}else {
queryWrapper.in(name, value);
}
//update-end-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
break;
case LIKE:
queryWrapper.like(name, value);
@ -782,7 +753,7 @@ public class QueryGenerator {
case NOT_RIGHT_LIKE:
queryWrapper.notLikeRight(name, value);
break;
//update-begin---author:chenrui ---date:20240527 for[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
// 代码逻辑说明: [TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
case LIKE_WITH_OR:
final String nameFinal = name;
Object[] vals;
@ -791,7 +762,7 @@ public class QueryGenerator {
} else if (value instanceof String[]) {
vals = (Object[]) value;
}
//update-begin-author:taoyan date:20200909 for:【bug】in 类型多值查询 不适配postgresql #1671
// 代码逻辑说明: 【bug】in 类型多值查询 不适配postgresql #1671
else if (value.getClass().isArray()) {
vals = (Object[]) value;
} else {
@ -806,7 +777,6 @@ public class QueryGenerator {
}
});
break;
//update-end---author:chenrui ---date:20240527 for[TV360X-378]下拉多框根据条件查询不出来:增加自定义字段查询规则功能------------
default:
log.info("--查询规则未匹配到---");
break;
@ -820,10 +790,8 @@ public class QueryGenerator {
private static boolean judgedIsUselessField(String name) {
return "class".equals(name) || "ids".equals(name)
|| "page".equals(name) || "rows".equals(name)
//// update-begin--author:sunjianlei date:20240808 for【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
//// https://github.com/jeecgboot/JeecgBoot/issues/6937
// || "sort".equals(name) || "order".equals(name)
//// update-end----author:sunjianlei date:20240808 for【TV360X-2009】取消过滤 sort、order 字段,防止前端排序报错 ------
;
}
@ -836,13 +804,12 @@ public class QueryGenerator {
public static Map<String, SysPermissionDataRuleModel> getRuleMap() {
Map<String, SysPermissionDataRuleModel> ruleMap = new HashMap<>(5);
List<SysPermissionDataRuleModel> list = null;
//update-begin-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
// 代码逻辑说明: QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
try {
list = JeecgDataAutorUtils.loadDataSearchConditon();
}catch (Exception e){
log.error("根据request对象获取权限数据失败可能是定时任务中执行的。", e);
}
//update-end-author:taoyan date:2023-6-1 for:QQYUN-5441 【简流】获取多个用户/部门/角色 设置部门查询 报错
if(list != null&&list.size()>0){
if(list.get(0)==null){
return ruleMap;
@ -879,9 +846,8 @@ public class QueryGenerator {
addEasyQuery(queryWrapper, name, rule, DateUtils.str2Date(dateStr,DateUtils.datetimeFormat.get()));
}
}else {
//update-begin---author:chenrui ---date:20241125 for[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
// 代码逻辑说明: [issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
addEasyQuery(queryWrapper, name, rule, NumberUtils.parseNumber(converRuleValue(dataRule.getRuleValue()), propertyType));
//update-end---author:chenrui ---date:20241125 for[issues/7481]多租户模式下 数据权限使用变量:#{tenant_id} 报错------------
}
}
}
@ -935,9 +901,8 @@ public class QueryGenerator {
return null;
}
Set<String> varParams = new HashSet<String>();
//update-begin---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
String regex = "#\\{\\[*\\w+]*}";
//update-end---author:chenrui ---date:20250108 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(sql);
@ -993,9 +958,8 @@ public class QueryGenerator {
Class propType = origDescriptors[i].getPropertyType();
boolean isString = propType.equals(String.class);
Object value;
//update-begin---author:chenrui ---date:20240527 for[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
// 代码逻辑说明: [TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
if(isString || Date.class.equals(propType)) {
//update-end---author:chenrui ---date:20240527 for[TV360X-539]数据权限,配置日期等于条件时后端报转换错误------------
value = converRuleValue(dataRule.getRuleValue());
}else {
value = NumberUtils.parseNumber(dataRule.getRuleValue(),propType);

View File

@ -41,8 +41,10 @@ import org.jeecg.common.util.oConvertUtils;
@Slf4j
public class JwtUtil {
/**Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000;
/**PC端Token有效期为7天Token在reids中缓存时间为两倍*/
public static final long EXPIRE_TIME = (7 * 12) * 60 * 60 * 1000L;
/**APP端Token有效期为30天Token在reids中缓存时间为两倍*/
public static final long APP_EXPIRE_TIME = (30 * 12) * 60 * 60 * 1000L;
static final String WELL_NUMBER = SymbolConstant.WELL_NUMBER + SymbolConstant.LEFT_CURLY_BRACKET;
/**
@ -86,7 +88,7 @@ public class JwtUtil {
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
log.warn("Token验证失败" + e.getMessage(),e);
return false;
}
}
@ -112,7 +114,9 @@ public class JwtUtil {
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
@ -121,6 +125,68 @@ public class JwtUtil {
}
/**
* 生成签名,5min后过期
*
* @param username 用户名
* @param secret 用户的密码
* @param expireTime 过期时间
* @return 加密的token
* @deprecated 请使用sign(String username, String secret, String clientType)方法代替
*/
@Deprecated
public static String sign(String username, String secret, Long expireTime) {
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 生成签名,根据客户端类型自动选择过期时间
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param username 用户名
* @param secret 用户的密码
* @param clientType 客户端类型PC或APP
* @return 加密的token
*/
public static String sign(String username, String secret, String clientType) {
// 根据客户端类型选择对应的过期时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? APP_EXPIRE_TIME
: EXPIRE_TIME;
Date date = new Date(System.currentTimeMillis() + expireTime);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username和clientType信息
return JWT.create()
.withClaim("username", username)
.withClaim("clientType", clientType)
.withExpiresAt(date)
.sign(algorithm);
}
/**
* 从token中获取客户端类型
* for [JHHB-1030]【鉴权】移动端用户token到期后续期时间变成pc端时长
*
* @param token JWT token
* @return 客户端类型如果不存在则返回PC兼容旧token
*/
public static String getClientType(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
String clientType = jwt.getClaim("clientType").asString();
// 如果clientType为空返回默认值PC兼容旧token
return oConvertUtils.isNotEmpty(clientType) ? clientType : CommonConstant.CLIENT_TYPE_PC;
} catch (JWTDecodeException e) {
log.warn("解析token中的clientType失败使用默认值PC" + e.getMessage());
return CommonConstant.CLIENT_TYPE_PC;
}
}
/**
* 根据request中的token获取用户账号
*
@ -200,7 +266,6 @@ public class JwtUtil {
} else {
key = key;
}
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 是否存在字符串标志
boolean multiStr;
if(oConvertUtils.isNotEmpty(key) && key.trim().matches("^\\[\\w+]$")){
@ -209,7 +274,6 @@ public class JwtUtil {
} else {
multiStr = false;
}
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
//替换为当前系统时间(年月日)
if (key.equals(DataBaseConstant.SYS_DATE)|| key.toLowerCase().equals(DataBaseConstant.SYS_DATE_TABLE)) {
returnValue = DateUtils.formatDate();
@ -278,20 +342,17 @@ public class JwtUtil {
if(user==null){
//TODO 暂时使用用户登录部门,存在逻辑缺陷,不是用户所拥有的部门
returnValue = sysUser.getOrgCode();
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else{
if(user.isOneDepart()) {
returnValue = user.getSysMultiOrgCode().get(0);
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = multiStr ? "'" + returnValue + "'" : returnValue;
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}else {
//update-begin---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
// 代码逻辑说明: [QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
returnValue = user.getSysMultiOrgCode().stream()
.filter(Objects::nonNull)
//update-begin---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.map(orgCode -> {
if (multiStr) {
return "'" + orgCode + "'";
@ -299,9 +360,7 @@ public class JwtUtil {
return orgCode;
}
})
//update-end---author:chenrui ---date:20250224 for[issues/7288]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
.collect(Collectors.joining(", "));
//update-end---author:chenrui ---date:20250107 for[QQYUN-10785]数据权限,查看自己拥有部门的权限中存在问题 #7288------------
}
}
}
@ -315,7 +374,7 @@ public class JwtUtil {
}
}
//update-begin-author:taoyan date:20210330 for:多租户ID作为系统变量
// 代码逻辑说明: 多租户ID作为系统变量
else if (key.equals(TenantConstant.TENANT_ID) || key.toLowerCase().equals(TenantConstant.TENANT_ID_TABLE)){
try {
returnValue = SpringContextUtils.getHttpServletRequest().getHeader(CommonConstant.TENANT_ID);
@ -323,7 +382,6 @@ public class JwtUtil {
log.warn("获取系统租户异常:" + e.getMessage());
}
}
//update-end-author:taoyan date:20210330 for:多租户ID作为系统变量
if(returnValue!=null){returnValue = returnValue + moshi;}
return returnValue;
}

View File

@ -3,9 +3,7 @@ package org.jeecg.common.system.util;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.annotation.EnumDict;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
@ -13,6 +11,7 @@ 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.lang.reflect.Method;
import java.util.*;
@ -67,13 +66,13 @@ public class ResourceUtil {
synchronized (ResourceUtil.class) {
if (!initialized) {
long startTime = System.currentTimeMillis();
log.info("【枚举字典加载】开始初始化枚举字典数据...");
log.debug("【枚举字典加载】开始初始化枚举字典数据...");
initEnumDictData();
initialized = true;
long endTime = System.currentTimeMillis();
log.info("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
log.debug("【枚举字典加载】枚举字典数据初始化完成,共加载 {} 个字典,总耗时: {}ms", enumDictData.size(), endTime - startTime);
}
}
}
@ -103,7 +102,7 @@ public class ResourceUtil {
}
long scanEndTime = System.currentTimeMillis();
log.info("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
log.debug("【枚举字典加载】文件扫描完成,总共找到 {} 个枚举类文件,扫描耗时: {}ms", allResources.size(), scanEndTime - scanStartTime);
MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
@ -126,7 +125,7 @@ public class ResourceUtil {
}
long processEndTime = System.currentTimeMillis();
log.info("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
log.debug("【枚举字典加载】处理完成,实际处理 {} 个带注解的枚举类,处理耗时: {}ms", processedCount, processEndTime - processStartTime);
}
/**
@ -183,10 +182,10 @@ public class ResourceUtil {
for (DictModel dm : dictItemList) {
String value = dm.getValue();
if (keySet.contains(value)) {
List<DictModel> list = new ArrayList<>();
// 修复bug获取或创建该dictCode对应的list而不是每次都创建新的list
List<DictModel> list = map.computeIfAbsent(code, k -> new ArrayList<>());
list.add(new DictModel(value, dm.getText()));
map.put(code, list);
break;
//break;
}
}
}

View File

@ -150,7 +150,7 @@ public class SqlConcatUtil {
}
private static String getInConditionValue(Object value,boolean isString) {
//update-begin-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
// 代码逻辑说明: 查询条件如果输入,导致sql报错
String[] temp = value.toString().split(",");
if(temp.length==0){
return "('')";
@ -168,7 +168,6 @@ public class SqlConcatUtil {
}else {
return "("+value.toString()+")";
}
//update-end-author:taoyan date:20210628 for: 查询条件如果输入,导致sql报错
}
/**
@ -215,7 +214,6 @@ public class SqlConcatUtil {
}
}else {
//update-begin-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
// 走到这里说明 value不带有任何模糊查询的标识(*或者%)
if (ruleEnum == QueryRuleEnum.LEFT_LIKE) {
if (DataBaseConstant.DB_TYPE_SQLSERVER.equals(getDbType())) {
@ -236,7 +234,6 @@ public class SqlConcatUtil {
return "'%" + str + "%'";
}
}
//update-end-author:taoyan date:2022-6-30 for: issues/3810 数据权限规则问题
}
}

View File

@ -56,7 +56,9 @@ public class CommonUtils {
public static String uploadOnlineImage(byte[] data,String basePath,String bizPath,String uploadType){
String dbPath = null;
String fileName = "image" + Math.round(Math.random() * 100000000000L);
fileName += "." + PoiPublicUtil.getFileExtendName(data);
//update-begin---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
fileName += "." + PoiPublicUtil.getFileExtendName(data).toLowerCase();
//update-end---author:wangshuai---date:2026-01-08---for:【QQYUN-14535】ai生成图片的后缀不一致的导致不展示---
try {
if(CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)){
File file = new File(basePath + File.separator + bizPath + File.separator );
@ -153,9 +155,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()) {

View File

@ -10,7 +10,9 @@ import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.enums.DySmsEnum;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.JeecgSmsTemplateConfig;
import org.jeecg.config.StaticConfig;
import org.slf4j.Logger;
@ -61,17 +63,21 @@ public class DySmsHelper {
public static boolean sendSms(String phone, JSONObject templateParamJson, DySmsEnum dySmsEnum) throws ClientException {
//可自助调整超时时间
JeecgBaseConfig config = SpringContextUtils.getBean(JeecgBaseConfig.class);
String smsSendType = config.getSmsSendType();
if(oConvertUtils.isNotEmpty(smsSendType) && CommonConstant.SMS_SEND_TYPE_TENCENT.equals(smsSendType)){
return TencentSms.sendTencentSms(phone, templateParamJson, config.getTencent(), dySmsEnum);
}
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//update-begin-authortaoyan date:20200811 for:配置类数据获取
// 代码逻辑说明: 配置类数据获取
StaticConfig staticConfig = SpringContextUtils.getBean(StaticConfig.class);
//logger.info("阿里大鱼短信秘钥 accessKeyId" + staticConfig.getAccessKeyId());
//logger.info("阿里大鱼短信秘钥 accessKeySecret"+ staticConfig.getAccessKeySecret());
setAccessKeyId(staticConfig.getAccessKeyId());
setAccessKeySecret(staticConfig.getAccessKeySecret());
//update-end-authortaoyan date:20200811 for:配置类数据获取
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
@ -81,7 +87,7 @@ public class DySmsHelper {
//验证json参数
validateParam(templateParamJson,dySmsEnum);
//update-begin---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理阿里云---
// 代码逻辑说明: 【QQYUN-9422】短信模板管理阿里云---
String templateCode = dySmsEnum.getTemplateCode();
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
if(baseConfig != null && CollectionUtil.isNotEmpty(baseConfig.getTemplateCode())){
@ -97,7 +103,6 @@ public class DySmsHelper {
logger.info("yml中读取签名名称{}",baseConfig.getSignature());
signName = baseConfig.getSignature();
}
//update-end---author:wangshuai---date:2024-11-05---for:【QQYUN-9422】短信模板管理阿里云---
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();

View File

@ -1,7 +1,7 @@
package org.jeecg.common.util;
import jakarta.servlet.http.HttpServletResponse;
import cn.hutool.core.io.IoUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;

View File

@ -38,14 +38,12 @@ public class HTMLUtils {
* @return
*/
public static String parseMarkdown(String markdownContent) {
//update-begin---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
/*PegDownProcessor pdp = new PegDownProcessor();
return pdp.markdownToHtml(markdownContent);*/
Parser parser = Parser.builder().build();
Node document = parser.parse(markdownContent);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
//update-end---author:wangshuai---date:2024-06-26---for:【TV360X-1344】JDK17 邮箱发送失败,需要换写法---
}
}

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)){
@ -163,11 +161,10 @@ public class MinioUtil {
public static String getObjectUrl(String bucketName, String objectName, Integer expires) {
initMinio(minioUrl, minioName,minioPass);
try{
//update-begin---author:liusq Date:20220121 for获取文件外链报错提示method不能为空导致文件下载和预览失败----
// 代码逻辑说明: 获取文件外链报错提示method不能为空导致文件下载和预览失败----
GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder().object(objectName)
.bucket(bucketName)
.expiry(expires).method(Method.GET).build();
//update-begin---author:liusq Date:20220121 for获取文件外链报错提示method不能为空导致文件下载和预览失败----
String url = minioClient.getPresignedObjectUrl(objectArgs);
return URLDecoder.decode(url,"UTF-8");
}catch (Exception e){

View File

@ -0,0 +1,78 @@
package org.jeecg.common.util;
import org.apache.commons.fileupload.FileItem;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
/**
* @date 2025-09-04
* @author scott
*
* 升级springboot3 无法使用 CommonsMultipartFile
* 自定义 MultipartFile 实现类,支持从 FileItem 构造
*/
public class MyCommonsMultipartFile implements MultipartFile {
private final byte[] fileContent;
private final String fileName;
private final String contentType;
// 新增构造方法,支持 FileItem 参数
public MyCommonsMultipartFile(FileItem fileItem) throws IOException {
this.fileName = fileItem.getName();
this.contentType = fileItem.getContentType();
try (InputStream inputStream = fileItem.getInputStream()) {
this.fileContent = inputStream.readAllBytes();
}
}
// 现有构造方法
public MyCommonsMultipartFile(InputStream inputStream, String fileName, String contentType) throws IOException {
this.fileName = fileName;
this.contentType = contentType;
this.fileContent = inputStream.readAllBytes();
}
@Override
public String getName() {
return fileName;
}
@Override
public String getOriginalFilename() {
return fileName;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return fileContent.length == 0;
}
@Override
public long getSize() {
return fileContent.length;
}
@Override
public byte[] getBytes() {
return fileContent;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(fileContent);
}
@Override
public void transferTo(File dest) throws IOException {
try (OutputStream os = new FileOutputStream(dest)) {
os.write(fileContent);
}
}
}

View File

@ -99,9 +99,8 @@ public class PasswordUtil {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
//update-begin-author:sccott date:20180815 for:中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
// 代码逻辑说明: 中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
encipheredData = cipher.doFinal(plaintext.getBytes("utf-8"));
//update-end-author:sccott date:20180815 for:中文作为用户名时加密的密码windows和linux会得到不同的结果 gitee/issues/IZUD7
} catch (Exception e) {
}
return bytesToHexString(encipheredData);

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;
@ -46,7 +46,7 @@ public class RestUtil {
public static String getBaseUrl() {
String basepath = getDomain() + getPath();
log.info(" RestUtil.getBaseUrl: " + basepath);
log.debug(" RestUtil.getBaseUrl: " + basepath);
return basepath;
}
@ -56,12 +56,19 @@ public class RestUtil {
private final static RestTemplate RT;
static {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
// 解决[issues/8859]online表单java增强失效------------
// 使用 Apache HttpClient 避免 JDK HttpURLConnection 的 too many bytes written 问题
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(30000);
requestFactory.setReadTimeout(30000);
RT = new RestTemplate(requestFactory);
// 解决乱码问题
RT.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 解决乱码问题(替换 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;
}
}
}
public static RestTemplate getRestTemplate() {
@ -192,7 +199,7 @@ public class RestUtil {
* @return ResponseEntity<responseType>
*/
public static <T> ResponseEntity<T> request(String url, HttpMethod method, HttpHeaders headers, JSONObject variables, Object params, Class<T> responseType) {
log.info(" RestUtil --- request --- url = "+ url);
log.debug(" RestUtil --- request --- url = "+ url);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
}
@ -216,6 +223,16 @@ public class RestUtil {
if (variables != null && !variables.isEmpty()) {
url += ("?" + asUrlVariables(variables));
}
// 解决[issues/8951]从jeecgboot 3.8.2 升级到 3.8.3 在线表单java增强功能报错------------
// Content-Length 强制设置(解决可能出现的截断问题)
if (StringUtils.isNotEmpty(body)) {
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.debug(" RestUtil --- request --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return RT.exchange(url, method, request, responseType);
@ -235,7 +252,7 @@ public class RestUtil {
*/
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);
log.debug(" RestUtil --- request --- url = "+ url + ", timeout = " + timeout);
if (StringUtils.isEmpty(url)) {
throw new RuntimeException("url 不能为空");
@ -250,12 +267,18 @@ public class RestUtil {
// 创建自定义RestTemplate如果需要设置超时
RestTemplate restTemplate = RT;
if (timeout > 0) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
// 代码逻辑说明: [issues/8859]online表单java增强失效------------
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(timeout);
requestFactory.setReadTimeout(timeout);
restTemplate = new RestTemplate(requestFactory);
// 解决乱码问题
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
// 解决乱码问题(替换 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;
}
}
}
// 请求体
@ -273,11 +296,21 @@ public class RestUtil {
url += ("?" + asUrlVariables(variables));
}
// Content-Length 强制设置(解决可能出现的截断问题)
if (StringUtils.isNotEmpty(body) && !headers.containsKey(HttpHeaders.CONTENT_LENGTH)) {
int contentLength = body.getBytes(StandardCharsets.UTF_8).length;
String current = headers.getFirst(HttpHeaders.CONTENT_LENGTH);
if (current == null || !current.equals(String.valueOf(contentLength))) {
headers.setContentLength(contentLength);
log.debug(" RestUtil --- request(timeout) --- 修正/设置 Content-Length = " + contentLength + (current!=null?" (原值="+current+")":""));
}
}
// 发送请求
HttpEntity<String> request = new HttpEntity<>(body, headers);
return restTemplate.exchange(url, method, request, responseType);
}
/**
* 获取JSON请求头
*/

View File

@ -335,13 +335,12 @@ public class SqlInjectionUtil {
return table;
}
//update-begin---author:scott ---date:2024-05-28 for表单设计器列表翻译存在表名带条件,导致翻译出问题----
// 代码逻辑说明: 表单设计器列表翻译存在表名带条件,导致翻译出问题----
int index = table.toLowerCase().indexOf(" where ");
if (index != -1) {
table = table.substring(0, index);
log.info("截掉where之后的新表名" + table);
}
//update-end---author:scott ---date::2024-05-28 for表单设计器列表翻译存在表名带条件导致翻译出问题----
table = table.trim();
/**

View File

@ -0,0 +1,184 @@
package org.jeecg.common.util;
import com.alibaba.fastjson.JSONObject;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.sms.v20210111.SmsClient;
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.enums.DySmsEnum;
import org.jeecg.config.JeecgSmsTemplateConfig;
import org.jeecg.config.tencent.JeecgTencent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description: 腾讯发送短信
* @author: wangshuai
* @date: 2025/11/4 19:27
*/
public class TencentSms {
private final static Logger logger = LoggerFactory.getLogger(TencentSms.class);
/**
* 发送腾讯短信
*
* @param phone
* @param templateParamJson
* @param tencent
* @param dySmsEnum
* @return
*/
public static boolean sendTencentSms(String phone, JSONObject templateParamJson, JeecgTencent tencent, DySmsEnum dySmsEnum) {
//获取客户端链接
SmsClient client = getSmsClient(tencent);
//构建腾讯云短信发送请求
SendSmsRequest req = buildSendSmsRequest(phone, templateParamJson, dySmsEnum, tencent);
try {
//发送短信
SendSmsResponse resp = client.SendSms(req);
// 处理响应
SendStatus[] statusSet = resp.getSendStatusSet();
if (statusSet != null && statusSet.length > 0) {
SendStatus status = statusSet[0];
if ("Ok".equals(status.getCode())) {
logger.info("短信发送成功,手机号:{}", phone);
return true;
} else {
logger.error("短信发送失败,手机号:{},错误码:{},错误信息:{}",
phone, status.getCode(), status.getMessage());
}
}
} catch (TencentCloudSDKException e) {
logger.error("短信发送失败{}", e.getMessage());
}
return false;
}
/**
* 获取sms客户端
*
* @param tencent 腾讯云配置
* @return SmsClient对象
*/
private static SmsClient getSmsClient(JeecgTencent tencent) {
Credential cred = new Credential(tencent.getSecretId(), tencent.getSecretKey());
// 实例化一个http选项可选的没有特殊需求可以跳过
HttpProfile httpProfile = new HttpProfile();
//指定接入地域域名*/
httpProfile.setEndpoint(tencent.getEndpoint());
//实例化一个客户端配置对象
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
//实例化要请求产品的client对象第二个参数是地域信息
return new SmsClient(cred, tencent.getRegion(), clientProfile);
}
/**
* 构建腾讯云短信发送请求
*
* @param phone 手机号码
* @param templateParamJson 模板参数JSON对象
* @param dySmsEnum 短信枚举配置
* @param tencent 腾讯云配置
* @return 构建好的SendSmsRequest对象
*/
private static SendSmsRequest buildSendSmsRequest(
String phone,
JSONObject templateParamJson,
DySmsEnum dySmsEnum,
JeecgTencent tencent) {
SendSmsRequest req = new SendSmsRequest();
// 1. 设置短信应用ID
String sdkAppId = tencent.getSdkAppId();
req.setSmsSdkAppId(sdkAppId);
// 2. 设置短信签名
String signName = getSmsSignName(dySmsEnum);
req.setSignName(signName);
// 3. 设置模板ID
String templateId = getSmsTemplateId(dySmsEnum);
req.setTemplateId(templateId);
// 4. 设置模板参数
String[] templateParams = extractTemplateParams(templateParamJson);
req.setTemplateParamSet(templateParams);
// 5. 设置手机号码
String[] phoneNumberSet = { phone };
req.setPhoneNumberSet(phoneNumberSet);
logger.debug("构建短信请求完成 - 应用ID: {}, 签名: {}, 模板ID: {}, 手机号: {}",
sdkAppId, signName, templateId, phone);
return req;
}
/**
* 获取短信签名名称
*
* @param dySmsEnum 腾讯云对象
*/
private static String getSmsSignName(DySmsEnum dySmsEnum) {
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
String signName = dySmsEnum.getSignName();
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
logger.debug("yml中读取签名名称: {}", baseConfig.getSignature());
signName = baseConfig.getSignature();
}
return signName;
}
/**
* 获取短信模板ID
*
* @param dySmsEnum 腾讯云对象
*/
private static String getSmsTemplateId(DySmsEnum dySmsEnum) {
JeecgSmsTemplateConfig baseConfig = SpringContextUtils.getBean(JeecgSmsTemplateConfig.class);
String templateCode = dySmsEnum.getTemplateCode();
if (StringUtils.isNotEmpty(baseConfig.getSignature())) {
Map<String, String> smsTemplate = baseConfig.getTemplateCode();
if (smsTemplate.containsKey(templateCode) &&
StringUtils.isNotEmpty(smsTemplate.get(templateCode))) {
templateCode = smsTemplate.get(templateCode);
logger.debug("yml中读取短信模板ID: {}", templateCode);
}
}
return templateCode;
}
/**
* 从JSONObject中提取模板参数按原始顺序
*
* @param templateParamJson 模板参数
*/
private static String[] extractTemplateParams(JSONObject templateParamJson) {
if (templateParamJson == null || templateParamJson.isEmpty()) {
return new String[0];
}
List<String> params = new ArrayList<>();
for (String key : templateParamJson.keySet()) {
Object value = templateParamJson.get(key);
if (value != null) {
params.add(value.toString());
} else {
// 处理null值
params.add("");
}
}
logger.debug("提取模板参数: {}", params);
return params.toArray(new String[0]);
}
}

View File

@ -121,7 +121,9 @@ public class TokenUtils {
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, user.getPassword(), redisUtil)) {
throw new JeecgBoot401Exception(CommonConstant.TOKEN_IS_INVALID_MSG);
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new JeecgBoot401Exception(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
}
return true;
}
@ -139,10 +141,15 @@ public class TokenUtils {
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置Toekn缓存有效时间
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
}
return true;
}

View File

@ -54,11 +54,10 @@ public class FreemarkerParseFactory {
//classic_compatible设置解决报空指针错误
SQL_CONFIG.setClassicCompatible(true);
//update-begin-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
// 解决freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
//https://ackcent.com/in-depth-freemarker-template-injection/
TPL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
SQL_CONFIG.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//update-end-author:taoyan date:2022-8-10 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
}
/**
@ -73,13 +72,12 @@ public class FreemarkerParseFactory {
return false;
}
} catch (Exception e) {
//update-begin--Author:scott Date:20180320 for解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误-----
// 代码逻辑说明: 解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误-----
if (e instanceof ParseException) {
log.error(e.getMessage(), e.fillInStackTrace());
throw new Exception(e);
}
log.debug("----isExistTemplate----" + e.toString());
//update-end--Author:scott Date:20180320 for解决问题 - 错误提示sql文件不存在实际问题是sql freemarker用法错误------
return false;
}
return true;

View File

@ -1,123 +1,107 @@
package org.jeecg.common.util.encryption;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.lang.codec.Base64;
import org.jeecg.common.util.oConvertUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
* @Description: AES 加密
* @author: jeecg-boot
* @date: 2022/3/30 11:48
* AES 工具 (兼容历史 NoPadding + 新 PKCS5Padding)
*/
@Slf4j
public class AesEncryptUtil {
/**
* 使用AES-128-CBC加密模式 key和iv可以相同
*/
private static String KEY = EncryptedString.key;
private static String IV = EncryptedString.iv;
private static final String KEY = EncryptedString.key;
private static final String IV = EncryptedString.iv;
/**
* 加密方法
* @param data 要加密的数据
* @param key 加密key
* @param iv 加密iv
* @return 加密的结果
* @throws Exception
*/
public static String encrypt(String data, String key, String iv) throws Exception {
try {
/* -------- 新版CBC + PKCS5Padding (与前端 CryptoJS Pkcs7 兼容) -------- */
private static String decryptPkcs5(String cipherBase64) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] plain = cipher.doFinal(Base64.decode(cipherBase64));
return new String(plain, StandardCharsets.UTF_8);
}
//"算法/模式/补码方式"NoPadding PkcsPadding
/* -------- 旧版CBC + NoPadding (手工补 0) -------- */
private static String decryptLegacyNoPadding(String cipherBase64) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec ks = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.DECRYPT_MODE, ks, ivSpec);
byte[] data = cipher.doFinal(Base64.decode(cipherBase64));
return new String(data, StandardCharsets.UTF_8)
.replace("\u0000",""); // 旧填充 0
}
/* -------- 兼容入口:登录使用 -------- */
public static String resolvePassword(String input){
if(oConvertUtils.isEmpty(input)){
return input;
}
// 1. 先尝试新版
try{
String p = decryptPkcs5(input);
return clean(p);
}catch(Exception ignore){
//log.debug("【AES解密】Password not AES PKCS5 cipher, try legacy.");
}
// 2. 回退旧版
try{
String legacy = decryptLegacyNoPadding(input);
return clean(legacy);
}catch(Exception e){
log.debug("【AES解密】Password not AES cipher, raw used.");
}
// 3. 视为明文
return input;
}
/* -------- 可选:统一清理尾部不可见控制字符 -------- */
private static String clean(String s){
if(s==null) return null;
// 去除结尾控制符/空白(不影响中间合法空格)
return s.replaceAll("[\\p{Cntrl}]+","").trim();
}
/* -------- 若仍需要旧接口,可保留 (不建议再用于新前端) -------- */
@Deprecated
public static String desEncrypt(String data) throws Exception {
return decryptLegacyNoPadding(data);
}
/* 加密(若前端不再使用,可忽略;保留旧实现避免影响历史) */
@Deprecated
public static String encrypt(String data) throws Exception {
try{
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes();
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
plaintextLength += (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "AES");
IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeToString(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}catch(Exception e){
throw new IllegalStateException("legacy encrypt error", e);
}
}
/**
* 解密方法
* @param data 要解密的数据
* @param key 解密key
* @param iv 解密iv
* @return 解密的结果
* @throws Exception
*/
public static String desEncrypt(String data, String key, String iv) throws Exception {
//update-begin-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
byte[] encrypted1 = Base64.decode(data);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
//加密解码后的字符串会出现\u0000
return originalString.replaceAll("\\u0000", "");
//update-end-author:taoyan date:2022-5-23 for:VUEN-1084 【vue3】online表单测试发现的新问题 6、解密报错 ---解码失败应该把异常抛出去,在外面处理
}
/**
* 使用默认的key和iv加密
* @param data
* @return
* @throws Exception
*/
public static String encrypt(String data) throws Exception {
return encrypt(data, KEY, IV);
}
/**
* 使用默认的key和iv解密
* @param data
* @return
* @throws Exception
*/
public static String desEncrypt(String data) throws Exception {
return desEncrypt(data, KEY, IV);
}
// /**
// * 测试
// */
// public static void main(String args[]) throws Exception {
// String test1 = "sa";
// String test =new String(test1.getBytes(),"UTF-8");
// String data = null;
// String key = KEY;
// String iv = IV;
// // /g2wzfqvMOeazgtsUVbq1kmJawROa6mcRAzwG1/GeJ4=
// data = encrypt(test, key, iv);
// System.out.println("数据:"+test);
// System.out.println("加密:"+data);
// String jiemi =desEncrypt(data, key, iv).trim();
// System.out.println("解密:"+jiemi);
// public static void main(String[] args) throws Exception {
// // 前端 CBC/Pkcs7 密文测试
// String frontCipher = encrypt("sa"); // 仅验证管道是否可用(旧方式)
// System.out.println(resolvePassword(frontCipher));
// }
}
}

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;
@ -149,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);
}
}
@ -183,8 +193,8 @@ public class SsrfFileTypeFilter {
* @throws Exception
*/
private static String getFileType(MultipartFile file) throws Exception {
//update-begin-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
private static String getFileType(MultipartFile file, String customPath) throws Exception {
// 代码逻辑说明: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
String fileExtendName = null;
InputStream is = null;
try {
@ -203,7 +213,7 @@ public class SsrfFileTypeFilter {
break;
}
}
log.info("-----获取到的指定文件类型------"+fileExtendName);
log.debug("-----获取到的指定文件类型------"+fileExtendName);
// 如果不是上述类型,则判断扩展名
if (StringUtils.isBlank(fileExtendName)) {
String fileName = file.getOriginalFilename();
@ -214,7 +224,6 @@ public class SsrfFileTypeFilter {
// 如果有扩展名,则返回扩展名
return getFileTypeBySuffix(fileName);
}
log.info("-----最終的文件类型------"+fileExtendName);
is.close();
return fileExtendName;
} catch (Exception e) {
@ -225,7 +234,6 @@ public class SsrfFileTypeFilter {
is.close();
}
}
//update-end-author:liusq date:20230404 for: [issue/4672]方法造成的文件被占用注释掉此方法tomcat就能自动清理掉临时文件
}
/**
@ -249,4 +257,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

@ -11,6 +11,10 @@ import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.springframework.beans.BeanUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -23,6 +27,7 @@ import java.sql.Date;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
*
@ -563,10 +568,8 @@ public class oConvertUtils {
return "";
} else if (!name.contains(SymbolConstant.UNDERLINE)) {
// 不含下划线,仅将首字母小写
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
// 代码逻辑说明: TASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
return name.substring(0, 1).toLowerCase() + name.substring(1).toLowerCase();
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
@ -611,7 +614,6 @@ public class oConvertUtils {
return result.substring(0, result.length() - 1);
}
//update-begin--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将下划线大写方式命名的字符串转换为驼峰式。(首字母写)
* 如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
@ -644,7 +646,6 @@ public class oConvertUtils {
}
return result.toString();
}
//update-end--Author:zhoujf Date:20180503 forTASK #2500 【代码生成器】代码生成器开发一通用模板生成功能
/**
* 将驼峰命名转化成下划线
@ -982,17 +983,18 @@ public class oConvertUtils {
/**
* 判断 list1中的元素是否在list2中出现
* 判断 sourceList中的元素是否在targetList中出现
*
* QQYUN-5326【简流】获取组织人员 单/多 筛选条件 没有部门筛选
* @param list1
* @param list2
* @return
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中有任何元素在targetList中存在则返回true否则返回false
*/
public static boolean isInList(List<String> list1, List<String> list2){
for(String str1: list1){
public static boolean isInList(List<String> sourceList, List<String> targetList){
for(String sourceItem: sourceList){
boolean flag = false;
for(String str2: list2){
if(str1.equals(str2)){
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
flag = true;
break;
}
@ -1004,6 +1006,35 @@ public class oConvertUtils {
return false;
}
/**
* 判断 sourceList中的所有元素是否都在targetList中存在
* @param sourceList 源列表,要检查的元素列表
* @param targetList 目标列表,用于匹配的列表
* @return 如果sourceList中的所有元素都在targetList中存在则返回true否则返回false
*/
public static boolean isAllInList(List<String> sourceList, List<String> targetList){
if(sourceList == null || sourceList.isEmpty()){
return true; // 空列表视为所有元素都存在
}
if(targetList == null || targetList.isEmpty()){
return false; // 目标列表为空源列表非空时返回false
}
for(String sourceItem: sourceList){
boolean found = false;
for(String targetItem: targetList){
if(sourceItem.equals(targetItem)){
found = true;
break;
}
}
if(!found){
return false; // 有任何一个元素不在目标列表中返回false
}
}
return true; // 所有元素都找到了
}
/**
* 计算文件大小转成MB
* @param uploadCount
@ -1168,5 +1199,58 @@ public class oConvertUtils {
public static boolean isEffectiveTenant(String tenantId) {
return MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && isNotEmpty(tenantId) && !("0").equals(tenantId);
}
/**
* 复制源对象的非空属性到目标对象(同名属性)
*
* @param source 源对象(页面)
* @param target 目标对象(数据库实体)
*/
public static void copyNonNullFields(Object source, Object target) {
if (source == null || target == null) {
return;
}
// 获取源对象的非空属性名数组
String[] nullPropertyNames = getNullPropertyNames(source);
// 复制:忽略源对象的空属性,仅覆盖目标对象的对应非空属性
BeanUtils.copyProperties(source, target, nullPropertyNames);
}
/**
* 获取源对象中值为 null 的属性名数组
*
* @param source
*/
private static String[] getNullPropertyNames(Object source) {
BeanWrapper beanWrapper = new BeanWrapperImpl(source);
//获取类的属性
PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();
// 过滤出值为 null 的属性名
return Stream.of(propertyDescriptors)
.map(PropertyDescriptor::getName)
.filter(name -> beanWrapper.getPropertyValue(name) == null)
.toArray(String[]::new);
}
/**
* String转换long类型
*
* @param v
* @param def
* @return
*/
public static long getLong(Object v, long def) {
if (v == null) {
return def;
};
if (v instanceof Number) {
return ((Number) v).longValue();
}
try {
return Long.parseLong(v.toString());
} catch (Exception e) {
return def;
}
}
}

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);
@ -125,9 +124,8 @@ public class OssBootUtil {
if (!fileDir.endsWith(SymbolConstant.SINGLE_SLASH)) {
fileDir = fileDir.concat(SymbolConstant.SINGLE_SLASH);
}
//update-begin-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
// 代码逻辑说明: 过滤上传文件夹名特殊字符,防止攻击
fileDir=StrAttackFilter.filter(fileDir);
//update-end-author:wangshuai date:20201012 for: 过滤上传文件夹名特殊字符,防止攻击
fileUrl = fileUrl.append(fileDir + fileName);
if (oConvertUtils.isNotEmpty(staticDomain) && staticDomain.toLowerCase().startsWith(CommonConstant.STR_HTTP)) {
@ -264,9 +262,8 @@ public class OssBootUtil {
newBucket = bucket;
}
initOss(endPoint, accessKeyId, accessKeySecret);
//update-begin---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
// 代码逻辑说明: 替换objectName前缀防止key不一致导致获取不到文件----
objectName = OssBootUtil.replacePrefix(objectName,bucket);
//update-end---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
OSSObject ossObject = ossClient.getObject(newBucket,objectName);
inputStream = new BufferedInputStream(ossObject.getObjectContent());
}catch (Exception e){
@ -294,9 +291,8 @@ public class OssBootUtil {
public static String getObjectUrl(String bucketName, String objectName, Date expires) {
initOss(endPoint, accessKeyId, accessKeySecret);
try{
//update-begin---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
// 代码逻辑说明: 替换objectName前缀防止key不一致导致获取不到文件----
objectName = OssBootUtil.replacePrefix(objectName,bucketName);
//update-end---author:liusq Date:20220120 for替换objectName前缀防止key不一致导致获取不到文件----
if(ossClient.doesObjectExist(bucketName,objectName)){
URL url = ossClient.generatePresignedUrl(bucketName,objectName,expires);
//log.info("原始url : {}", url.toString());

View File

@ -63,7 +63,7 @@ public abstract class AbstractQueryBlackListHandler {
if(list==null){
return true;
}
log.info(" 获取sql信息 {} ", list.toString());
log.debug(" 获取sql信息 {} ", list.toString());
boolean flag = checkTableAndFieldsName(list);
if(flag == false){
return false;

View File

@ -0,0 +1,24 @@
package org.jeecg.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* ai配置类通用的配置可以放到这里面
*
* @Author: wangshuai
* @Date: 2025/12/17 14:00
*/
@Data
@Component
@ConfigurationProperties(prefix = AiRagConfigBean.PREFIX)
public class AiRagConfigBean {
public static final String PREFIX = "jeecg.airag";
/**
* 敏感节点
* stdio mpc命令行功能开启sqlAI流程SQL节点开启
*/
private String allowSensitiveNodes = "";
}

View File

@ -60,17 +60,15 @@ public class AutoPoiDictConfig implements AutoPoiDictServiceI {
for (DictModel t : dictList) {
//update-begin---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
// 代码逻辑说明: [issues/4917]excel 导出异常---
if(t!=null && t.getText()!=null && t.getValue()!=null){
//update-end---author:liusq Date:20230517 for[issues/4917]excel 导出异常---
//update-begin---author:scott Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
// 代码逻辑说明: [issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
if(t.getValue().contains(EXCEL_SPLIT_TAG)){
String val = t.getValue().replace(EXCEL_SPLIT_TAG,TEMP_EXCEL_SPLIT_TAG);
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + val);
}else{
dictReplaces.add(t.getText() + EXCEL_SPLIT_TAG + t.getValue());
}
//update-end---author:20211220 Date:20211220 for[issues/I4MBB3]@Excel dicText字段的值有下划线时导入功能不能正确解析---
}
}
if (dictReplaces != null && dictReplaces.size() != 0) {

View File

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

View File

@ -1,9 +1,11 @@
package org.jeecg.config;
import lombok.Getter;
import lombok.Setter;
import org.jeecg.config.tencent.JeecgTencent;
import org.jeecg.config.vo.*;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.stereotype.Component;
@ -74,7 +76,35 @@ public class JeecgBaseConfig {
/**
* 百度开放API配置
*/
private BaiduApi baiduApi;
private BaiduApi baiduApi;
/**
* minio配置
*/
@Getter
@Setter
private JeecgMinio minio;
/**
* oss配置
*/
@Getter
@Setter
private JeecgOSS oss;
/**
* 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
*/
@Getter
@Setter
private String smsSendType = "aliyun";
/**
* 腾讯配置
*/
@Getter
@Setter
private JeecgTencent tencent;
public String getCustomResourcePrefixPath() {
return customResourcePrefixPath;

View File

@ -95,13 +95,11 @@
// 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;
// }

View File

@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.filters.GlobalOpenApiMethodFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@ -22,18 +23,24 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author eightmonth
*/
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "knife4j", name = "production", havingValue = "false", matchIfMissing = true)
@PropertySource("classpath:config/default-spring-doc.properties")
public class Swagger3Config implements WebMvcConfigurer {
// 路径匹配结果缓存,避免重复计算
private static final Map<String, Boolean> EXCLUDED_PATHS_CACHE = new ConcurrentHashMap<>();
// 定义不需要注入安全要求的路径集合
Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/{key}",
private static final Set<String> excludedPaths = new HashSet<>(Arrays.asList(
"/sys/randomImage/**",
"/sys/login",
"/sys/phoneLogin",
"/sys/mLogin",
@ -43,7 +50,20 @@ public class Swagger3Config implements WebMvcConfigurer {
"/sys/thirdLogin/**",
"/sys/user/register"
));
// 预处理通配符模式,提高匹配效率
private static final Set<String> wildcardPatterns = new HashSet<>();
private static final Set<String> exactPatterns = new HashSet<>();
static {
// 初始化时分离精确匹配和通配符匹配
for (String pattern : excludedPaths) {
if (pattern.endsWith("/**")) {
wildcardPatterns.add(pattern.substring(0, pattern.length() - 3));
} else {
exactPatterns.add(pattern);
}
}
}
/**
*
* 显示swagger-ui.html文档展示页还必须注入swagger资源
@ -97,19 +117,18 @@ public class Swagger3Config implements WebMvcConfigurer {
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);
});
// 使用缓存避免重复计算
return EXCLUDED_PATHS_CACHE.computeIfAbsent(path, p -> {
// 精确匹配
if (exactPatterns.contains(p)) {
return true;
}
// 通配符匹配
return wildcardPatterns.stream().anyMatch(p::startsWith);
});
}
@Bean
@ -117,7 +136,7 @@ public class Swagger3Config implements WebMvcConfigurer {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.version("3.8.3")
.version("3.9.1")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description("后台API接口")
.termsOfService("NO terms of service")

View File

@ -0,0 +1,32 @@
package org.jeecg.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 任务调度器配置
* 提供 ThreadPoolTaskScheduler Bean 用于 AI RAG 流程调度等功能
* 仅当容器中不存在 ThreadPoolTaskScheduler 时才创建
*
* @author jeecg
*/
@Slf4j
@Configuration
public class TaskSchedulerConfig {
@Bean
@ConditionalOnMissingBean(ThreadPoolTaskScheduler.class)
public ThreadPoolTaskScheduler taskScheduler() {
log.info("初始化定时任务调度器 ThreadPoolTaskScheduler");
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("airag-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
return scheduler;
}
}

View File

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

View File

@ -11,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.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@ -50,6 +47,7 @@ import java.util.concurrent.TimeUnit;
* @Author qinfeng
*
*/
@Slf4j
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@ -144,7 +142,6 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
return objectMapper;
}
//update-begin---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
// /**
// * SpringBootAdmin的Httptrace不见了
// * https://blog.csdn.net/u013810234/article/details/110097201
@ -153,20 +150,20 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
// public InMemoryHttpTraceRepository getInMemoryHttpTrace(){
// return new InMemoryHttpTraceRepository();
// }
//update-end---author:chenrui ---date:20240514 for[QQYUN-9247]系统监控功能优化------------
/**
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* 在Bean初始化完成后立即配置PrometheusMeterRegistry避免在Meter注册后才配置MeterFilter
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
*/
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
@PostConstruct
public void initPrometheusMeterRegistry() {
// 确保在应用启动早期就配置MeterFilter避免警告
if (null != meterRegistryPostProcessor && null != prometheusMeterRegistry) {
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "prometheusMeterRegistry");
log.info("PrometheusMeterRegistry 配置完成");
}
}

View File

@ -129,20 +129,18 @@ public class MybatisInterceptor implements Interceptor {
Field[] fields = null;
if (parameter instanceof ParamMap) {
ParamMap<?> p = (ParamMap<?>) parameter;
//update-begin-author:scott date:20190729 for:批量更新报错issues/IZA3Q--
// 代码逻辑说明: 批量更新报错issues/IZA3Q--
String et = "et";
if (p.containsKey(et)) {
parameter = p.get(et);
} else {
parameter = p.get("param1");
}
//update-end-author:scott date:20190729 for:批量更新报错issues/IZA3Q-
//update-begin-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
// 代码逻辑说明: 更新指定字段时报错 issues/#516-
if (parameter == null) {
return invocation.proceed();
}
//update-end-author:scott date:20190729 for:更新指定字段时报错 issues/#516-
fields = oConvertUtils.getAllFields(parameter);
} else {
@ -184,7 +182,6 @@ public class MybatisInterceptor implements Interceptor {
// TODO Auto-generated method stub
}
//update-begin--Author:scott Date:20191213 for关于使用Quzrtz 开启线程任务, #465
/**
* 获取登录用户
* @return
@ -199,6 +196,5 @@ public class MybatisInterceptor implements Interceptor {
}
return sysUser;
}
//update-end--Author:scott Date:20191213 for关于使用Quzrtz 开启线程任务, #465
}

View File

@ -21,20 +21,23 @@ import org.jeecg.config.shiro.filters.CustomShiroFilterFactoryBean;
import org.jeecg.config.shiro.filters.JwtFilter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.Environment;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.lang.reflect.Method;
import java.util.*;
/**
@ -45,7 +48,6 @@ import java.util.*;
@Slf4j
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ShiroConfig {
@Resource
@ -111,7 +113,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/sys/checkAuth", "anon"); //授权接口排除
filterChainDefinitionMap.put("/openapi/call/**", "anon"); // 开放平台接口排除
//update-begin--Author:scott Date:20221116 for排除静态资源后缀
// 代码逻辑说明: 排除静态资源后缀
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
@ -129,7 +131,6 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/**/*.glb", "anon");
filterChainDefinitionMap.put("/**/*.wasm", "anon");
//update-end--Author:scott Date:20221116 for排除静态资源后缀
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
@ -137,9 +138,7 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v3/**", "anon");
// update-begin--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
filterChainDefinitionMap.put("/sys/annountCement/show/**", "anon");
// update-end--Author:sunjianlei Date:20210510 for排除消息通告查看详情页面用于第三方APP
//积木报表排除
filterChainDefinitionMap.put("/jmreport/**", "anon");
@ -158,7 +157,7 @@ public class ShiroConfig {
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");
@ -191,6 +190,8 @@ public class ShiroConfig {
// 企业微信证书排除
filterChainDefinitionMap.put("/WW_verify*", "anon");
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】
@ -207,7 +208,6 @@ public class ShiroConfig {
return shiroFilterFactoryBean;
}
//update-begin---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
/**
* spring过滤装饰器 <br/>
@ -223,21 +223,24 @@ public class ShiroConfig {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilterFactoryBean"));
registration.setEnabled(true);
//update-begin---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
// 代码逻辑说明: [issues/7491]运行时长,效率慢
registration.addUrlPatterns("/test/ai/chat/send");
//update-end---author:chenrui ---date:20241202 for[issues/7491]运行时间好长,效率慢 ------------
registration.addUrlPatterns("/airag/flow/run");
registration.addUrlPatterns("/airag/flow/debug");
registration.addUrlPatterns("/airag/chat/send");
registration.addUrlPatterns("/airag/app/debug");
registration.addUrlPatterns("/airag/app/prompt/generate");
registration.addUrlPatterns("/airag/chat/receive/**");
// 添加SSE接口的异步支持
registration.addUrlPatterns("/airag/extData/evaluator/debug");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateChartSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/updateChartOptSse");
registration.addUrlPatterns("/drag/onlDragDatasetHead/generateSqlSse");
//支持异步
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
return registration;
}
//update-end---author:chenrui ---date:20240126 for【QQYUN-7932】AI助手------------
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
@ -358,7 +361,6 @@ public class ShiroConfig {
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
}
//update-end--Author:scott Date:20210531 for修改集群模式下未设置redis密码的bug issues/I3QNIC
manager = redisManager;
}
return manager;
@ -375,7 +377,7 @@ public class ShiroConfig {
mapping.setUrlPathHelper(new ShiroUrlPathHelper());
return mapping;
}
private List<String> rebuildUrl(String[] bases, String[] uris) {
List<String> urls = new ArrayList<>();
for (String base : bases) {

View File

@ -83,7 +83,7 @@ public class ShiroRealm extends AuthorizingRealm {
Set<String> permissionSet = commonApi.queryUserAuths(userId);
info.addStringPermissions(permissionSet);
//System.out.println(permissionSet);
log.info("===============Shiro权限认证成功==============");
log.debug("===============Shiro权限认证成功==============");
return info;
}
@ -110,8 +110,8 @@ public class ShiroRealm extends AuthorizingRealm {
loginUser = this.checkUserTokenIsEffect(token);
} catch (AuthenticationException e) {
log.error("—————校验 check token 失败——————————"+ e.getMessage(), e);
JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
return null;
// 重新抛出异常让JwtFilter统一处理避免返回两次错误响应
throw e;
}
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
@ -141,9 +141,11 @@ public class ShiroRealm extends AuthorizingRealm {
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
// 用户登录Token过期提示信息
String userLoginTokenErrorMsg = oConvertUtils.getString(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN_ERROR_MSG + token));
throw new AuthenticationException(oConvertUtils.isEmpty(userLoginTokenErrorMsg)? CommonConstant.TOKEN_IS_INVALID_MSG: userLoginTokenErrorMsg);
}
//update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
// 代码逻辑说明: 校验用户的tenant_id和前端传过来的是否一致
String userTenantIds = loginUser.getRelTenantIds();
if(MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL && oConvertUtils.isNotEmpty(userTenantIds)){
String contextTenantId = TenantContext.getTenant();
@ -152,7 +154,7 @@ public class ShiroRealm extends AuthorizingRealm {
//登录用户无租户前端header中租户ID值为 0
String str ="0";
if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
//update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
// 代码逻辑说明: /issues/I4O14W 用户租户信息变更判断漏洞
String[] arr = userTenantIds.split(",");
if(!oConvertUtils.isIn(contextTenantId, arr)){
boolean isAuthorization = false;
@ -177,10 +179,8 @@ public class ShiroRealm extends AuthorizingRealm {
}
//*********************************************
}
//update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
}
}
//update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
return loginUser;
}
@ -202,19 +202,22 @@ public class ShiroRealm extends AuthorizingRealm {
if (oConvertUtils.isNotEmpty(cacheToken)) {
// 校验token有效性
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newAuthorization = JwtUtil.sign(userName, passWord);
// 设置超时时间
// 从token中解析客户端类型保持续期时使用相同的客户端类型
String clientType = JwtUtil.getClientType(token);
String newAuthorization = JwtUtil.sign(userName, passWord, clientType);
// 根据客户端类型设置对应的缓存有效时间
long expireTime = CommonConstant.CLIENT_TYPE_APP.equalsIgnoreCase(clientType)
? JwtUtil.APP_EXPIRE_TIME * 2 / 1000
: JwtUtil.EXPIRE_TIME * 2 / 1000;
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime);
log.debug("——————————用户在线操作更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
}
//update-begin--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
// else {
// // 设置超时时间
// redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
// redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
// }
//update-end--Author:scott Date:20191005 for解决每次请求都重写redis中 token缓存问题
return true;
}
@ -230,8 +233,7 @@ public class ShiroRealm extends AuthorizingRealm {
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
//update-begin---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
// 代码逻辑说明: 【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
super.clearCachedAuthorizationInfo(principals);
//update-end---author:scott ---date::2024-06-18 for【TV360X-1320】分配权限必须退出重新登录才生效造成很多用户困扰---
}
}

View File

@ -56,9 +56,13 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
executeLogin(request, response);
return true;
} catch (Exception e) {
JwtUtil.responseError((HttpServletResponse)response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
// 使用异常中的具体错误信息,保留"不允许同一账号多地同时登录"等具体提示
String errorMsg = e.getMessage();
if (oConvertUtils.isEmpty(errorMsg)) {
errorMsg = CommonConstant.TOKEN_IS_INVALID_MSG;
}
JwtUtil.responseError((HttpServletResponse)response, 401, errorMsg);
return false;
//throw new AuthenticationException("Token失效请重新登录", e);
}
}
@ -69,11 +73,10 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
// update-begin--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
// 代码逻辑说明: JT-355 OA聊天添加token验证获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 forJT-355 OA聊天添加token验证获取token参数
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入如果错误他会抛出异常并被捕获
@ -106,10 +109,9 @@ public class JwtFilter extends BasicHttpAuthenticationFilter {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
//update-begin-author:taoyan date:20200708 for:多租户用到
// 代码逻辑说明: 多租户用到
String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID);
TenantContext.setTenant(tenantId);
//update-end-author:taoyan date:20200708 for:多租户用到
return super.preHandle(request, response);
}

View File

@ -4,6 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.config.shiro.IgnoreAuth;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@ -20,6 +21,7 @@ import java.util.stream.Collectors;
* @date 2024/4/18 11:35
*/
@Slf4j
@Lazy(false)
@Component
@AllArgsConstructor
public class IgnoreAuthPostProcessor implements InitializingBean {
@ -33,10 +35,15 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
long startTime = System.currentTimeMillis();
List<String> ignoreAuthUrls = new ArrayList<>();
Set<Class<?>> restControllers = requestMappingHandlerMapping.getHandlerMethods().values().stream().map(HandlerMethod::getBeanType).collect(Collectors.toSet());
for (Class<?> restController : restControllers) {
ignoreAuthUrls.addAll(postProcessRestController(restController));
}
// 优化直接从HandlerMethod过滤避免重复扫描
requestMappingHandlerMapping.getHandlerMethods().values().stream()
.filter(handlerMethod -> handlerMethod.getMethod().isAnnotationPresent(IgnoreAuth.class))
.forEach(handlerMethod -> {
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
ignoreAuthUrls.addAll(processIgnoreAuthMethod(clazz, method));
});
log.info("Init Token ignoreAuthUrls Config [ 集合 ] {}", ignoreAuthUrls);
if (!CollectionUtils.isEmpty(ignoreAuthUrls)) {
@ -46,44 +53,30 @@ public class IgnoreAuthPostProcessor implements InitializingBean {
// 计算方法的耗时
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] " + elapsedTime + "毫秒");
log.info("Init Token ignoreAuthUrls Config [ 耗时 ] " + elapsedTime + "ms");
}
private List<String> postProcessRestController(Class<?> clazz) {
List<String> ignoreAuthUrls = new ArrayList<>();
// 优化:新方法处理单个@IgnoreAuth方法减少重复注解检查
private List<String> processIgnoreAuthMethod(Class<?> clazz, Method method) {
RequestMapping base = clazz.getAnnotation(RequestMapping.class);
String[] baseUrl = Objects.nonNull(base) ? base.value() : new String[]{};
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(GetMapping.class)) {
GetMapping requestMapping = method.getAnnotation(GetMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PostMapping.class)) {
PostMapping requestMapping = method.getAnnotation(PostMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PutMapping.class)) {
PutMapping requestMapping = method.getAnnotation(PutMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(DeleteMapping.class)) {
DeleteMapping requestMapping = method.getAnnotation(DeleteMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
} else if (method.isAnnotationPresent(IgnoreAuth.class) && method.isAnnotationPresent(PatchMapping.class)) {
PatchMapping requestMapping = method.getAnnotation(PatchMapping.class);
String[] uri = requestMapping.value();
ignoreAuthUrls.addAll(rebuildUrl(baseUrl, uri));
}
String[] uri = null;
if (method.isAnnotationPresent(RequestMapping.class)) {
uri = method.getAnnotation(RequestMapping.class).value();
} else if (method.isAnnotationPresent(GetMapping.class)) {
uri = method.getAnnotation(GetMapping.class).value();
} else if (method.isAnnotationPresent(PostMapping.class)) {
uri = method.getAnnotation(PostMapping.class).value();
} else if (method.isAnnotationPresent(PutMapping.class)) {
uri = method.getAnnotation(PutMapping.class).value();
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
uri = method.getAnnotation(DeleteMapping.class).value();
} else if (method.isAnnotationPresent(PatchMapping.class)) {
uri = method.getAnnotation(PatchMapping.class).value();
}
return ignoreAuthUrls;
return uri != null ? rebuildUrl(baseUrl, uri) : Collections.emptyList();
}
private List<String> rebuildUrl(String[] bases, String[] uris) {

View File

@ -0,0 +1,32 @@
package org.jeecg.config.sign.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 签名校验注解
* 用于方法级别的签名验证功能等同于yml中的jeecg.signUrls配置
* 参考DragSignatureAspect的设计思路使用AOP切面实现
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignatureCheck {
/**
* 是否启用签名校验
* @return true-启用(默认), false-禁用
*/
boolean enabled() default true;
/**
* 签名校验失败时的错误消息
* @return 错误消息
*/
String errorMessage() default "Sign签名校验失败";
}

View File

@ -0,0 +1,112 @@
package org.jeecg.config.sign.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.config.sign.annotation.SignatureCheck;
import org.jeecg.config.sign.interceptor.SignAuthInterceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestBody;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 基于AOP的签名验证切面
* 复用SignAuthInterceptor的成熟签名验证逻辑
*
* @author GitHub Copilot
* @since 2025-12-15
*/
@Aspect
@Slf4j
@Component("signatureCheckAspect")
public class SignatureCheckAspect {
/**
* 复用SignAuthInterceptor的签名验证逻辑
*/
private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor();
/**
* 验签切点:拦截所有标记了@SignatureCheck注解的方法
*/
@Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)")
private void signatureCheckPointCut() {
}
/**
* 开始验签
*/
@Before("signatureCheckPointCut()")
public void doSignatureValidation(JoinPoint point) throws Exception {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class);
log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName());
// 如果注解被禁用,直接返回
if (!signatureCheck.enabled()) {
log.info("签名验证已禁用,跳过");
return;
}
// update-begin---author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
Object bodyParam = null;
Object[] args = point.getArgs();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
Annotation[] annotations = parameterAnnotations[i];
boolean hasRequestBodyAnnotation = Arrays.stream(annotations).anyMatch(annotation -> annotation.annotationType().equals(RequestBody.class));
if (hasRequestBodyAnnotation) {
// 捕获携带@RequestBody注解的参数供签名校验使用
bodyParam = arg;
}
}
// update-end-----author:sjlei---date:20260115 for: 查找带有@RequestBody注解的参数解决签名校验时读取请求体为空的问题
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
log.error("无法获取请求上下文");
throw new IllegalArgumentException("无法获取请求上下文");
}
HttpServletRequest request = attributes.getRequest();
log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP"));
try {
// 直接调用SignAuthInterceptor的验证逻辑
signAuthInterceptor.validateSignature(request, bodyParam);
log.info("AOP签名验证通过");
} catch (IllegalArgumentException e) {
// 使用注解中配置的错误消息,或者保留原始错误消息
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证失败: {}", e.getMessage());
if ("Sign签名校验失败".equals(errorMessage)) {
// 如果是默认错误消息,使用原始的详细错误信息
throw e;
} else {
// 如果是自定义错误消息,使用自定义消息
throw new IllegalArgumentException(errorMessage, e);
}
} catch (Exception e) {
// 包装其他异常
String errorMessage = signatureCheck.errorMessage();
log.error("AOP签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException(errorMessage, e);
}
}
}

View File

@ -1,6 +1,7 @@
package org.jeecg.config.sign.interceptor;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.TenantConstant;
import org.jeecg.common.util.PathMatcherUtil;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.filter.RequestBodyReserveFilter;
@ -41,7 +42,7 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
registry.addInterceptor(signAuthInterceptor()).addPathPatterns(signUrlsArray);
}
//update-begin-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
// 代码逻辑说明: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
@Bean
public RequestBodyReserveFilter requestBodyReserveFilter(){
return new RequestBodyReserveFilter();
@ -64,8 +65,9 @@ public class SignAuthConfiguration implements WebMvcConfigurer {
//------------------------------------------------------------
// 建议此处只添加post请求地址而不是所有的都需要走过滤器
registration.addUrlPatterns(signUrlsArray);
// 增加注解签名请求
registration.addUrlPatterns(TenantConstant.SIGNATURE_CHECK_POST_URL);
return registration;
}
//update-end-author:taoyan date:20220427 for: issues/I53J5E post请求X_SIGN签名拦截校验后报错, request body 为空
}

View File

@ -33,63 +33,104 @@ public class SignAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Sign Interceptor request URI = " + request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI());
if(oConvertUtils.isEmpty(xTimestamp)){
Result<?> result = Result.error("Sign签名校验失败时间戳为空");
log.error("Sign 签名校验失败Header xTimestamp 为空");
//校验失败返回前端
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
out.print(JSON.toJSON(result));
return false;
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) {
log.error("签名验证失败:X-TIMESTAMP已过期注意系统时间和服务器时间是否有误差");
throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("Sign 签名通过Header Sign : {}",headerSign);
try {
// 调用验证逻辑
validateSignature(request);
return true;
} else {
log.info("sign allParams: {}", allParams);
log.error("request URI = " + request.getRequestURI());
log.error("Sign 签名校验失败Header Sign : {}",headerSign);
//校验失败返回前端
} catch (IllegalArgumentException e) {
// 验证失败,返回错误响应
log.error("Sign 签名校验失败!{}", e.getMessage());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
Result<?> result = Result.error("Sign签名校验失败");
Result<?> result = Result.error(e.getMessage());
out.print(JSON.toJSON(result));
return false;
}
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request) throws IllegalArgumentException {
validateSignature(request, null);
}
/**
* 签名验证核心逻辑
* 提取出来供AOP切面复用
* @param request HTTP请求
* @throws IllegalArgumentException 验证失败时抛出异常
*/
public void validateSignature(HttpServletRequest request, Object bodyParam) throws IllegalArgumentException {
try {
log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI());
HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
//获取全部参数(包括URL和body上的)
SortedMap<String, String> allParams = HttpUtils.getAllParams(requestWrapper, bodyParam);
log.debug("提取参数: {}", allParams);
//对参数进行签名验证
String headerSign = request.getHeader(CommonConstant.X_SIGN);
String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP);
if(oConvertUtils.isEmpty(xTimestamp)){
log.error("Sign签名校验失败时间戳为空");
throw new IllegalArgumentException("Sign签名校验失败请求参数不完整");
}
//客户端时间
Long clientTimestamp = Long.parseLong(xTimestamp);
int length = 14;
int length1000 = 1000;
//1.校验签名时间兼容X_TIMESTAMP的新老格式
if (xTimestamp.length() == length) {
//a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子20220308152143)
long currentTimestamp = DateUtils.getCurrentTimestamp();
long timeDiff = currentTimestamp - clientTimestamp;
log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff);
if (timeDiff > MAX_EXPIRE) {
log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
} else {
//b. X_TIMESTAMP格式是 时间戳 (例子1646552406000)
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - clientTimestamp;
long maxExpireMs = MAX_EXPIRE * length1000;
log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff);
if (timeDiff > maxExpireMs) {
log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs);
throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!");
}
}
//2.校验签名
boolean isSigned = SignUtil.verifySign(allParams,headerSign);
if (isSigned) {
log.debug("签名验证通过");
} else {
log.error("签名验证失败, 参数: {}", allParams);
throw new IllegalArgumentException("Sign签名校验失败");
}
} catch (IllegalArgumentException e) {
// 重新抛出签名验证异常
throw e;
} catch (Exception e) {
// 包装其他异常如IOException
log.error("签名验证异常: {}", e.getMessage());
throw new IllegalArgumentException("Sign签名校验失败" + e.getMessage());
}
}
}

View File

@ -35,25 +35,25 @@ public class HttpUtils {
* @date 20210621
* @param request
*/
public static SortedMap<String, String> getAllParams(HttpServletRequest request) throws IOException {
public static SortedMap<String, String> getAllParams(HttpServletRequest request, Object bodyParam) throws IOException {
SortedMap<String, String> result = new TreeMap<>();
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
String pathVariable = request.getRequestURI().substring(request.getRequestURI().lastIndexOf("/") + 1);
if (pathVariable.contains(SymbolConstant.COMMA)) {
log.info(" pathVariable: {}",pathVariable);
log.debug(" pathVariable: {}",pathVariable);
String deString = URLDecoder.decode(pathVariable, "UTF-8");
//https://www.52dianzi.com/category/article/37/565371.html
if(deString.contains("%")){
try {
deString = URLDecoder.decode(deString, "UTF-8");
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
} catch (Exception e) {
//e.printStackTrace();
}
}
log.info(" pathVariable decode: {}",deString);
log.debug(" pathVariable decode: {}",deString);
result.put(SignUtil.X_PATH_VARIABLE, deString);
}
// 获取URL上的参数
@ -65,7 +65,13 @@ public class HttpUtils {
Map<String, String> allRequestParam = new HashMap<>(16);
// get请求不需要拿body参数
if (!HttpMethod.GET.name().equals(request.getMethod())) {
allRequestParam = getAllRequestParam(request);
if (bodyParam != null) {
// update-begin---author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
allRequestParam = JSONObject.parseObject(JSONObject.toJSONString(bodyParam), Map.class);
// update-end-----author:sjlei---date:20260115 for: 解决签名校验时读取请求体为空的问题
} else {
allRequestParam = getAllRequestParam(request);
}
}
// 将URL的参数和body参数进行合并
if (allRequestParam != null) {
@ -91,15 +97,15 @@ public class HttpUtils {
// 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
String pathVariable = url.substring(url.lastIndexOf("/") + 1);
if (pathVariable.contains(SymbolConstant.COMMA)) {
log.info(" pathVariable: {}",pathVariable);
log.debug(" pathVariable: {}",pathVariable);
String deString = URLDecoder.decode(pathVariable, "UTF-8");
//https://www.52dianzi.com/category/article/37/565371.html
if(deString.contains("%")){
deString = URLDecoder.decode(deString, "UTF-8");
log.info("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
log.debug("存在%情况下,执行两次解码 — pathVariable decode: {}",deString);
}
log.info(" pathVariable decode: {}",deString);
log.debug(" pathVariable decode: {}",deString);
result.put(SignUtil.X_PATH_VARIABLE, deString);
}
// 获取URL上的参数
@ -174,11 +180,10 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
}
return result;
}
@ -202,11 +207,10 @@ public class HttpUtils {
String[] params = param.split("&");
for (String s : params) {
int index = s.indexOf("=");
//update-begin---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
// 代码逻辑说明: [issues/5879]数据查询传ds=“”造成的异常------------
if (index != -1) {
result.put(s.substring(0, index), s.substring(index + 1));
}
//update-end---author:chenrui ---date:20240222 for[issues/5879]数据查询传ds=“”造成的异常------------
}
return result;
}

View File

@ -11,7 +11,12 @@ import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 签名工具类
@ -34,7 +39,7 @@ public class SignUtil {
}
// 把参数加密
String paramsSign = getParamsSign(params);
log.info("Param Sign : {}", paramsSign);
log.debug("Param Sign : {}", paramsSign);
return !StringUtils.isEmpty(paramsSign) && headerSign.equals(paramsSign);
}
@ -47,14 +52,9 @@ public class SignUtil {
//去掉 Url 里的时间戳
params.remove("_t");
String paramsJsonStr = JSONObject.toJSONString(params);
log.info("Param paramsJsonStr : {}", paramsJsonStr);
log.debug("Param paramsJsonStr : {}", paramsJsonStr);
//设置签名秘钥
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if(oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)){
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
String signatureSecret = SignUtil.getSignatureSecret();
try {
//【issues/I484RW】2.4.6部署后下拉搜索框提示“sign签名检验失败”
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes("UTF-8")).toUpperCase();
@ -63,4 +63,129 @@ public class SignUtil {
return null;
}
}
}
/**
* 通过前端签名算法生成签名
*
* @param url 请求的完整URL包含查询参数
* @param requestParams 使用 @RequestParam 获取的参数集合
* @param requestBodyParams 使用 @RequestBody 获取的参数集合
* @return 计算得到的签名大写MD5若参数不足返回 null
*/
public static String generateRequestSign(String url, Map<String, Object> requestParams, Map<String, Object> requestBodyParams) {
if (oConvertUtils.isEmpty(url)) {
return null;
}
// 解析URL上的查询参数与路径变量
Map<String, String> urlParams = parseQueryString(url);
// 合并URL参数与@RequestParam参数确保数值和布尔类型转换为字符串
Map<String, String> mergedParams = mergeObject(urlParams, requestParams);
// 按需合并@RequestBody参数
if (requestBodyParams != null && !requestBodyParams.isEmpty()) {
mergedParams = mergeObject(mergedParams, requestBodyParams);
}
// 按键名升序排序,保持与前端一致的签名顺序
SortedMap<String, String> sortedParams = new TreeMap<>(mergedParams);
// 去除时间戳字段,避免参与签名
sortedParams.remove("_t");
// 序列化为JSON字符串
String paramsJsonStr = JSONObject.toJSONString(sortedParams);
// 读取签名秘钥
String signatureSecret = getSignatureSecret();
// 计算MD5摘要并转大写
return DigestUtils.md5DigestAsHex((paramsJsonStr + signatureSecret).getBytes(StandardCharsets.UTF_8)).toUpperCase();
}
/**
* 解析URL中的查询参数并处理末尾逗号分隔的路径变量片段。
*
* @param url 请求的完整URL
* @return 解析后的参数映射,数值与布尔类型均转换为字符串
*/
private static Map<String, String> parseQueryString(String url) {
Map<String, String> result = new HashMap<>(16);
int fragmentIndex = url.indexOf('#');
if (fragmentIndex >= 0) {
url = url.substring(0, fragmentIndex);
}
int questionIndex = url.indexOf('?');
String paramString = null;
if (questionIndex >= 0 && questionIndex < url.length() - 1) {
paramString = url.substring(questionIndex + 1);
}
// 处理路径变量末尾以逗号分隔的段,例如 /sys/dict/getDictItems/sys_user,realname,username
int lastSlashIndex = url.lastIndexOf(SymbolConstant.SINGLE_SLASH);
if (lastSlashIndex >= 0 && lastSlashIndex < url.length() - 1) {
String lastPathVariable = url.substring(lastSlashIndex + 1);
int qIndexInPath = lastPathVariable.indexOf('?');
if (qIndexInPath >= 0) {
lastPathVariable = lastPathVariable.substring(0, qIndexInPath);
}
if (lastPathVariable.contains(SymbolConstant.COMMA)) {
String decodedPathVariable = URLDecoder.decode(lastPathVariable, StandardCharsets.UTF_8);
result.put(X_PATH_VARIABLE, decodedPathVariable);
}
}
if (oConvertUtils.isNotEmpty(paramString)) {
String[] pairs = paramString.split(SymbolConstant.AND);
for (String pair : pairs) {
int equalIndex = pair.indexOf('=');
if (equalIndex > 0 && equalIndex < pair.length() - 1) {
String key = pair.substring(0, equalIndex);
String value = pair.substring(equalIndex + 1);
// 解码并统一类型为字符串
String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
String decodedValue = URLDecoder.decode(value, StandardCharsets.UTF_8);
result.put(decodedKey, decodedValue);
}
}
}
return result;
}
/**
* 合并两个参数映射,并保证数值与布尔类型统一转为字符串。
*
* @param target 初始参数映射
* @param source 待合并的参数映射
* @return 合并后的新映射
*/
private static Map<String, String> mergeObject(Map<String, String> target, Map<String, Object> source) {
Map<String, String> merged = new HashMap<>(16);
if (target != null && !target.isEmpty()) {
merged.putAll(target);
}
if (source != null && !source.isEmpty()) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Number) {
// 数值类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value instanceof Boolean) {
// 布尔类型转字符串,保持前后端一致
merged.put(key, String.valueOf(value));
} else if (value != null) {
merged.put(key, String.valueOf(value));
}
}
}
return merged;
}
/**
* 读取并校验签名秘钥配置。
*
* @return 有效的签名秘钥
*/
private static String getSignatureSecret() {
JeecgBaseConfig jeecgBaseConfig = SpringContextUtils.getBean(JeecgBaseConfig.class);
String signatureSecret = jeecgBaseConfig.getSignatureSecret();
String curlyBracket = SymbolConstant.DOLLAR + SymbolConstant.LEFT_CURLY_BRACKET;
if (oConvertUtils.isEmpty(signatureSecret) || signatureSecret.contains(curlyBracket)) {
throw new JeecgBootException("签名密钥 ${jeecg.signatureSecret} 缺少配置 ");
}
return signatureSecret;
}
}

View File

@ -0,0 +1,38 @@
package org.jeecg.config.tencent;
import lombok.Data;
/**
* @Description: 腾讯短信配置
*
* @author: wangshuai
* @date: 2025/10/30 18:22
*/
@Data
public class JeecgTencent {
/**
* 接入域名
*/
private String endpoint;
/**
* api秘钥id
*/
private String secretId;
/**
* api秘钥key
*/
private String secretKey;
/**
* 应用id
*/
private String sdkAppId;
/**
* 地域信息
*/
private String region;
}

View File

@ -19,11 +19,42 @@ public class Firewall {
* 低代码模式dev:开发模式prod:发布模式——关闭所有在线开发配置能力)
*/
private String lowCodeMode;
/**
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
*/
private Boolean isConcurrent = true;
/**
* 是否开启默认密码登录提醒true 登录后提示必须修改默认密码)
*/
private Boolean enableDefaultPwdCheck = false;
/**
* 是否开启登录验证码校验true 开启false 关闭并跳过验证码逻辑)
*/
private Boolean enableLoginCaptcha = true;
// /**
// * 表字典安全模式white:白名单——配置了白名单的表才能通过表字典方式访问black:黑名单——配置了黑名单的表不允许表字典方式访问)
// */
// private String tableDictMode;
public Boolean getEnableLoginCaptcha() {
return enableLoginCaptcha;
}
public void setEnableLoginCaptcha(Boolean enableLoginCaptcha) {
this.enableLoginCaptcha = enableLoginCaptcha;
}
public Boolean getEnableDefaultPwdCheck() {
return enableDefaultPwdCheck;
}
public void setEnableDefaultPwdCheck(Boolean enableDefaultPwdCheck) {
this.enableDefaultPwdCheck = enableDefaultPwdCheck;
}
public Boolean getDataSourceSafe() {
return dataSourceSafe;
}
@ -47,4 +78,12 @@ public class Firewall {
public void setDisableSelectAll(Boolean disableSelectAll) {
this.disableSelectAll = disableSelectAll;
}
public Boolean getIsConcurrent() {
return isConcurrent;
}
public void setIsConcurrent(Boolean isConcurrent) {
this.isConcurrent = isConcurrent;
}
}

View File

@ -0,0 +1,11 @@
package org.jeecg.config.vo;
import lombok.Data;
@Data
public class JeecgMinio {
private String minio_url;
private String bucketName;
}

View File

@ -0,0 +1,11 @@
package org.jeecg.config.vo;
import lombok.Data;
@Data
public class JeecgOSS {
private String endpoint;
private String bucketName;
}

View File

@ -0,0 +1 @@
org.jeecg.config.DruidWallConfigRegister

Binary file not shown.

Before

Width:  |  Height:  |  Size: 0 B

After

Width:  |  Height:  |  Size: 487 B

View File

@ -0,0 +1,429 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MCP Stdio 工具 - 修复编码问题
确保所有输出都使用UTF-8编码
"""
import json
import sys
import os
from typing import Dict, Any
import logging
# 强制使用UTF-8编码
if sys.platform == "win32":
# Windows需要特殊处理
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
else:
# Unix-like系统
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
# 设置环境变量
os.environ['PYTHONIOENCODING'] = 'utf-8'
os.environ['PYTHONUTF8'] = '1'
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
encoding='utf-8'
)
logger = logging.getLogger("mcp-tool")
class FixedMCPServer:
"""修复编码问题的MCP服务器"""
def __init__(self):
self.tools = {}
self.initialize_tools()
def initialize_tools(self):
"""初始化工具集"""
# 获取时间
self.tools["get_time"] = {
"name": "get_time",
"description": "获取当前时间",
"inputSchema": {
"type": "object",
"properties": {
"format": {
"type": "string",
"description": "时间格式",
"enum": ["iso", "timestamp", "human", "chinese"],
"default": "iso"
}
}
}
}
# 文本处理工具
self.tools["text_process"] = {
"name": "text_process",
"description": "文本处理工具",
"inputSchema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "输入文本"
},
"operation": {
"type": "string",
"description": "操作类型",
"enum": ["length", "upper", "lower", "reverse", "count_words"],
"default": "length"
}
},
"required": ["text"]
}
}
# 数据格式工具
self.tools["format_data"] = {
"name": "format_data",
"description": "格式化数据",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "原始数据"
},
"format": {
"type": "string",
"description": "格式类型",
"enum": ["json", "yaml", "xml"],
"default": "json"
}
},
"required": ["data"]
}
}
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
"""处理请求"""
try:
method = request.get("method")
params = request.get("params", {})
if method == "tools/list":
return self.handle_tools_list()
elif method == "tools/call":
return self.handle_tool_call(params)
elif method == "ping":
return {"result": "pong"}
else:
return self.create_error_response(
code=-32601,
message="Method not found"
)
except Exception as e:
logger.error(f"Error handling request: {e}")
return self.create_error_response(
code=-32603,
message=f"Internal error: {str(e)}"
)
def handle_tools_list(self) -> Dict[str, Any]:
"""列出所有工具 - 确保返回标准JSON"""
return {
"result": {
"tools": list(self.tools.values())
}
}
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""调用工具 - 修复响应格式"""
name = params.get("name")
arguments = params.get("arguments", {})
if name not in self.tools:
return self.create_error_response(
code=-32602,
message=f"Tool '{name}' not found"
)
try:
if name == "get_time":
result = self.execute_get_time(arguments)
elif name == "text_process":
result = self.execute_text_process(arguments)
elif name == "format_data":
result = self.execute_format_data(arguments)
else:
return self.create_error_response(
code=-32602,
message="Tool not implemented"
)
# 确保返回正确的MCP响应格式
return self.create_success_response(result)
except Exception as e:
logger.error(f"Tool execution error: {e}")
return self.create_error_response(
code=-32603,
message=f"Tool execution failed: {str(e)}"
)
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""获取时间 - 支持中文"""
from datetime import datetime
try:
format_type = args.get("format", "iso")
now = datetime.now()
if format_type == "iso":
result = now.isoformat()
elif format_type == "timestamp":
result = now.timestamp()
elif format_type == "human":
result = now.strftime("%Y-%m-%d %H:%M:%S")
elif format_type == "chinese":
result = now.strftime("%Y年%m月%d%H时%M分%S秒")
else:
result = now.isoformat()
logger.info(f"当前系统时间:{result}")
return {
"status": "success",
"format": format_type,
"time": result,
"timestamp": now.timestamp(),
"date": now.strftime("%Y-%m-%d"),
"time_12h": now.strftime("%I:%M:%S %p")
}
except Exception as e:
return {
"status": "error",
"error": str(e)
}
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""文本处理"""
try:
text = args.get("text", "")
operation = args.get("operation", "length")
if operation == "length":
result = len(text)
result_str = f"文本长度: {result} 个字符"
elif operation == "upper":
result = text.upper()
result_str = f"大写: {result}"
elif operation == "lower":
result = text.lower()
result_str = f"小写: {result}"
elif operation == "reverse":
result = text[::-1]
result_str = f"反转: {result}"
elif operation == "count_words":
words = len(text.split())
result = words
result_str = f"单词数: {words}"
else:
raise ValueError(f"未知操作: {operation}")
return {
"status": "success",
"operation": operation,
"original_text": text,
"result": result,
"result_str": result_str,
"text_length": len(text)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"operation": args.get("operation", "")
}
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""格式化数据"""
try:
data_str = args.get("data", "")
format_type = args.get("format", "json")
# 尝试解析为JSON
try:
data = json.loads(data_str)
is_json = True
except:
data = data_str
is_json = False
if format_type == "json":
if is_json:
result = json.dumps(data, ensure_ascii=False, indent=2)
else:
# 如果不是JSON包装成JSON
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
elif format_type == "yaml":
import yaml
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
elif format_type == "xml":
# 简单的XML格式化
if isinstance(data, dict):
result = "<data>"
for k, v in data.items():
result += f"\n <{k}>{v}</{k}>"
result += "\n</data>"
else:
result = f"<text>{data}</text>"
else:
result = str(data)
return {
"status": "success",
"format": format_type,
"original": data_str,
"formatted": result,
"length": len(result)
}
except Exception as e:
return {
"status": "error",
"error": str(e),
"format": args.get("format", "")
}
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""创建成功响应 - 确保符合MCP规范"""
# 将数据转换为JSON字符串作为文本内容
content_text = json.dumps(data, ensure_ascii=False, indent=2)
return {
"result": {
"content": [
{
"type": "text",
"text": content_text
}
],
"isError": False
}
}
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
"""创建错误响应"""
return {
"error": {
"code": code,
"message": message
}
}
def safe_json_dump(data: Dict[str, Any]) -> str:
"""安全的JSON序列化确保UTF-8编码"""
try:
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
except:
# 如果失败使用ASCII转义
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
def main():
"""主函数 - 修复Stdio通信"""
logger.info("启动MCP Stdio服务器 (修复编码版)...")
server = FixedMCPServer()
# 初始握手消息
init_message = {
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "fixed-mcp-server",
"version": "1.0.0"
}
}
}
# 发送初始化响应
try:
sys.stdout.write(safe_json_dump(init_message) + "\n")
sys.stdout.flush()
except Exception as e:
logger.error(f"发送初始化消息失败: {e}")
return
logger.info("MCP服务器已初始化")
# 主循环
line_num = 0
while True:
try:
line = sys.stdin.readline()
if not line:
logger.info("输入流结束")
break
line = line.strip()
line_num += 1
if not line:
continue
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
try:
request = json.loads(line)
logger.info(f"解析请求: {request.get('method', 'unknown')}")
# 处理请求
response = server.handle_request(request)
response["jsonrpc"] = "2.0"
response["id"] = request.get("id")
# 发送响应
response_json = safe_json_dump(response)
sys.stdout.write(response_json + "\n")
sys.stdout.flush()
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
except json.JSONDecodeError as e:
logger.error(f"JSON解析错误: {e}")
error_response = {
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": f"Parse error at line {line_num}"
},
"id": None
}
sys.stdout.write(safe_json_dump(error_response) + "\n")
sys.stdout.flush()
except KeyboardInterrupt:
logger.info("接收到中断信号")
break
except Exception as e:
logger.error(f"未处理的错误: {e}")
break
logger.info("MCP服务器已停止")
if __name__ == "__main__":
main()

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-module</artifactId>
<version>3.8.3</version>
<version>3.9.1</version>
</parent>
<artifactId>jeecg-boot-module-airag</artifactId>
@ -31,10 +31,30 @@
</repositories>
<properties>
<langchain4j.version>0.35.0</langchain4j.version>
<apache-tika.version>2.9.1</apache-tika.version>
<kotlin.version>2.2.0</kotlin.version>
<liteflow.version>2.15.0</liteflow.version>
<apache-tika.version>3.2.3</apache-tika.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.9.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>1.9.1-beta17</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- system单体 api-->
<dependency>
@ -55,10 +75,24 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-aiflow</artifactId>
<version>1.2.0</version>
<version>3.9.1-beta</version>
<exclusions>
<exclusion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</exclusion>
<exclusion>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</exclusion>
<exclusion>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-python</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- beigin 这两个依赖太多每个50M左右,如果你发布需要使用,请<scope>provided</scope>删掉 -->
<!-- begin 注意:这几个依赖体积较大,每个50MB。若发布需要使用,请<scope>provided</scope> 删除 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
@ -71,12 +105,18 @@
<version>${liteflow.version}</version>
<scope>provided</scope>
</dependency>
<!-- end 这两个依赖太多每个包50M左右如果你发布需要使用请把<scope>provided</scope>删掉 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<!-- end 注意这几个依赖体积较大每个约50MB。若发布时需要使用请将 <scope>provided</scope> 删除 -->
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<artifactId>liteflow-script-python</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
@ -109,13 +149,20 @@
<!-- langChain4j model support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${langchain4j.version}</version>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<!-- langChain4j mcp support -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</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>
@ -129,13 +176,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>
@ -147,13 +192,21 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<!-- langChain4j vextor support -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>${langchain4j.version}</version>
<version>1.3.0-beta9</version>
</dependency>
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
</dependency>
<!-- langChain4j Document Parser -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
@ -180,7 +233,12 @@
<artifactId>tika-parser-text-module</artifactId>
<version>${apache-tika.version}</version>
</dependency>
<!-- word模版引擎 -->
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.12.2</version>
</dependency>
</dependencies>
</project>

View File

@ -38,4 +38,25 @@ public class AiAppConsts {
*/
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
/**
* 应用元数据:流程输入参数
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
/**
* 是否开启记忆
*/
public static final Integer IZ_OPEN_MEMORY = 1;
/**
* 会话标题最大长度
*/
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
/**
* AI写作的应用id
*/
public static final String WRITER_APP_ID = "2010634128233779202";
}

View File

@ -104,4 +104,68 @@ public class Prompts {
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度低于0.7时启动重写\"\n" +
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
/**
* 提示词生成角色及通用要求
*/
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
"你是一位AI提示词专家请根据提供的配置信息生成针对AI智能体的“使用指南”提示词。\n" +
"\n" +
"## 通用要求\n" +
"1. 生成的内容将作为系统提示词的一部分。\n" +
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
"3. **只输出提示词内容**不要包含任何解释、寒暄或Markdown代码块标记。\n" +
"4. 语气专业、清晰、指令性强。\n" +
"5. 说明内容请使用中文。\n\n";
/**
* 变量生成提示词
*/
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
"### 输入信息\n" +
"**变量列表**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**变量使用指南**。\n" +
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}\n" +
" 例如针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
" 例如针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
"4. **通用更新指令**请在变量指南的最后单独生成一条指令明确指示AI“当从用户对话中获取到上述变量<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
/**
* 记忆库生成提示词
*/
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
"### 输入信息\n" +
"**记忆库描述**\n" +
"%s\n" +
"### 要求\n" +
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
" - **全自动检索(强制优先)**\n" +
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
" - **行为准则**\n" +
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
" - **示例演示**\n" +
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
/**
* ai写作提示词
*/
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
/**
* ai写作回复提示词
*/
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
}

View File

@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @Description: AI应用
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
}
/**
* 根据应用ID生成变量和记忆提示词 (SSE)
* for: 【QQYUN-14479】提示词单独拆分
* @param variables
* @return
*/
@PostMapping(value = "/prompt/generateMemoryByAppId")
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
@RequestParam(name = "memoryId") String memoryId) {
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
}
}

View File

@ -8,6 +8,7 @@ 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.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
import org.springframework.beans.factory.annotation.Autowired;
@ -102,6 +103,19 @@ public class AiragChatController {
return chatService.getConversations(appId);
}
/**
* 根据类型获取所有对话
*
* @return 返回一个Result对象包含所有对话的信息
* @author wangshuai
* @date 2025/12/11 11:42
*/
@IgnoreAuth
@GetMapping(value = "/getConversationsByType")
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
return chatService.getConversationsByType(sessionType);
}
/**
* 删除会话
*
@ -113,7 +127,22 @@ public class AiragChatController {
@IgnoreAuth
@DeleteMapping(value = "/conversation/{id}")
public Result<?> deleteConversation(@PathVariable("id") String id) {
return chatService.deleteConversation(id);
return chatService.deleteConversation(id,"");
}
/**
* 删除会话
*
* @param id
* @return
* @author wangshuai
* @date 2025/12/11 20:00
*/
@IgnoreAuth
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
public Result<?> deleteConversationByType(@PathVariable("id") String id,
@PathVariable("sessionType") String sessionType) {
return chatService.deleteConversation(id,sessionType);
}
/**
@ -139,8 +168,9 @@ public class AiragChatController {
*/
@IgnoreAuth
@GetMapping(value = "/messages")
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
return chatService.getMessages(conversationId);
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
@RequestParam(value = "sessionType", required = false) String sessionType) {
return chatService.getMessages(conversationId, sessionType);
}
/**
@ -153,7 +183,21 @@ public class AiragChatController {
@IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}")
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
return chatService.clearMessage(conversationId);
return chatService.clearMessage(conversationId, "");
}
/**
* 清空消息
*
* @return
* @author wangshuai
* @date 2025/12/11 19:06
*/
@IgnoreAuth
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
@PathVariable(value = "sessionType") String sessionType) {
return chatService.clearMessage(conversationId, sessionType);
}
/**
@ -217,4 +261,25 @@ public class AiragChatController {
return result;
}
/**
* ai海报生成
* @return
*/
@PostMapping("/genAiPoster")
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
String imageUrl = chatService.genAiPoster(chatSendParams);
return Result.OK(imageUrl);
}
/**
* 生成ai写作
*
* @param aiWriteGenerateVo
* @return
*/
@PostMapping("/genAiWriter")
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
return chatService.genAiWriter(aiWriteGenerateVo);
}
}

View File

@ -167,6 +167,35 @@ public class AiragApp implements Serializable {
@Schema(description = "元数据")
private java.lang.String metadata;
/**
* 插件 [{pluginId: '123213', pluginName: 'xxxx', category: 'mcp'}]
*/
@Schema(description = "插件")
private java.lang.String plugins;
/**
* 是否开启记忆(0 不开启1开启)
*/
@Schema(description = "是否开启记忆(0 不开启1开启)")
private java.lang.Integer izOpenMemory;
/**
* 记忆库知识库的id
*/
@Schema(description = "记忆库")
private java.lang.String memoryId;
/**
* 变量
*/
@Schema(description = "变量")
private java.lang.String variables;
/**
* 记忆和变量提示词
*/
@Schema(description = "记忆和变量提示词")
private java.lang.String memoryPrompt;
/**
* 知识库ids
*/

View File

@ -1,7 +1,6 @@
package org.jeecg.modules.airag.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.entity.AiragApp;
/**
@ -21,4 +20,14 @@ public interface IAiragAppService extends IService<AiragApp> {
* @date 2025/3/12 14:45
*/
Object generatePrompt(String prompt,boolean blocking);
/**
* 根据应用id生成提示词
*
* @param variables
* @param memoryId
* @param blocking
* @return
*/
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
}

View File

@ -1,6 +1,7 @@
package org.jeecg.modules.airag.app.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.jeecg.modules.airag.app.vo.ChatConversation;
import org.jeecg.modules.airag.app.vo.ChatSendParams;
@ -59,21 +60,23 @@ public interface IAiragChatService {
* 获取对话聊天记录
*
* @param conversationId
* @param sessionType 类型
* @return
* @author chenrui
* @date 2025/2/26 15:16
*/
Result<?> getMessages(String conversationId);
Result<?> getMessages(String conversationId, String sessionType);
/**
* 删除会话
*
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/3/3 16:55
*/
Result<?> deleteConversation(String conversationId);
Result<?> deleteConversation(String conversationId, String sessionType);
/**
* 更新会话标题
@ -87,11 +90,12 @@ public interface IAiragChatService {
/**
* 清空消息
* @param conversationId
* @param sessionType
* @return
* @author chenrui
* @date 2025/3/3 19:49
*/
Result<?> clearMessage(String conversationId);
Result<?> clearMessage(String conversationId, String sessionType);
/**
* 初始化聊天(忽略租户)
@ -111,4 +115,27 @@ public interface IAiragChatService {
* @date 2025/8/11 17:39
*/
SseEmitter receiveByRequestId(String requestId);
/**
* 根据类型获取会话列表
*
* @param sessionType
* @return
*/
Result<?> getConversationsByType(String sessionType);
/**
* 生成海报图片
* @param chatSendParams
* @return
*/
String genAiPoster(ChatSendParams chatSendParams);
/**
* 生成ai创作
*
* @param chatSendParams
* @return
*/
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
}

View File

@ -0,0 +1,44 @@
package org.jeecg.modules.airag.app.service;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.common.handler.AIChatParams;
public interface IAiragVariableService {
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
void updateVariable(String userId, String appId, String name, String value);
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
String additionalPrompt(String username, AiragApp app);
/**
* 初始化变量(仅不存在时设置)
*
* @param userId
* @param appId
* @param name
* @param defaultValue
*/
void initVariable(String userId, String appId, String name, String defaultValue);
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import dev.langchain4j.data.message.AiMessage;
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.UUIDGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
@ -23,6 +27,8 @@ import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@ -31,6 +37,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
/**
* @Description: AI应用
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
@Autowired
IAIChatHandler aiChatHandler;
@Autowired
private IAiragKnowledgeService airagKnowledgeService;
@Override
public Object generatePrompt(String prompt, boolean blocking) {
AssertUtils.assertNotEmpty("请输入提示词", prompt);
@ -62,84 +72,167 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
}
return Result.OK("success", promptValue);
}else{
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.onNext((String resMessage) -> {
// 兼容推理模型
if ("<think>".equals(resMessage)) {
isThinking.set(true);
resMessage = "> ";
}
if ("</think>".equals(resMessage)) {
isThinking.set(false);
resMessage = "\n\n";
}
if (isThinking.get()) {
if (null != resMessage && resMessage.contains("\n")) {
resMessage = "\n> ";
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
return startSseChat(messages, params);
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
}
}
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
@Override
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
}
// 构建变量描述
StringBuilder variablesDesc = new StringBuilder();
if (oConvertUtils.isNotEmpty(variables)) {
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
if (variableList != null && !variableList.isEmpty()) {
for (AppVariableVo var : variableList) {
if (var.getEnable() != null && !var.getEnable()) {
continue;
}
String name = var.getName();
if (oConvertUtils.isNotEmpty(var.getAction())) {
String action = var.getAction();
if (oConvertUtils.isNotEmpty(name)) {
try {
// 使用正则替换未被{{}}包裹的变量名
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
action = action.replaceAll(regex, "{{" + name + "}}");
} catch (Exception e) {
log.warn("变量名替换异常: name={}", name, e);
}
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
variablesDesc.append(action).append("\n");
} else {
variablesDesc.append("- {{").append(name).append("}}");
if (oConvertUtils.isNotEmpty(var.getDescription())) {
variablesDesc.append(": ").append(var.getDescription());
}
variablesDesc.append("\n");
}
}
}
}
// 构建Prompt
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
if (!variablesDesc.isEmpty()) {
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
}
// 构建记忆状态描述
if (oConvertUtils.isNotEmpty(memoryId)) {
String memoryDescr = "";
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
memoryDescr += "记忆库描述:" + memory.getDescr();
}
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
}
String prompt = promptBuilder.toString();
List<ChatMessage> messages = List.of(new UserMessage(prompt));
AIChatParams params = new AIChatParams();
params.setTemperature(0.7);
if(blocking){
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
if (promptValue == null || promptValue.isEmpty()) {
return Result.error("生成失败");
}
return Result.OK("success", promptValue);
}else{
return startSseChat(messages, params);
}
}
/**
* 发送聊天
* @param messages
* @param params
* @return
*/
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
SseEmitter emitter = new SseEmitter(-0L);
// 异步运行(流式)
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
String requestId = UUIDGenerator.generate();
// ai聊天响应逻辑
tokenStream.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);
EventMessageData messageEventData = EventMessageData.builder()
.message(resMessage)
.build();
eventData.setData(messageEventData);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onCompleteResponse((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.aiMessage();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
String eventStr = JSONObject.toJSONString(eventData);
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
emitter.send(SseEmitter.event().data(eventStr));
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.onComplete((responseMessage) -> {
// 记录ai的回复
AiMessage aiMessage = responseMessage.content();
FinishReason finishReason = responseMessage.finishReason();
String respText = aiMessage.text();
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
// 正常结束
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
try {
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
throw new RuntimeException(e);
}
closeSSE(emitter, eventData);
} else if (FinishReason.TOOL_EXECUTION.equals(finishReason)) {
// 需要执行工具
// TODO author: chenrui for: date:2025/3/7
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
} else {
// 异常结束
log.error("调用模型异常:" + respText);
if (respText.contains("insufficient Balance")) {
respText = "大预言模型账号余额不足!";
}
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
closeSSE(emitter, eventData);
}
})
.onError((Throwable error) -> {
// sse
String errMsg = "调用大模型接口失败:" + error.getMessage();
log.error(errMsg, error);
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
closeSSE(emitter, eventData);
})
.start();
return emitter;
}
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆将记忆提示词单独拆分---
private static void closeSSE(SseEmitter emitter, EventData eventData) {
try {

View File

@ -0,0 +1,194 @@
package org.jeecg.modules.airag.app.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.service.IAiragVariableService;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: AI应用变量服务实现
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Service
@Slf4j
public class AiragVariableServiceImpl implements IAiragVariableService {
@Autowired
private RedisTemplate redisTemplate;
private static final String CACHE_PREFIX = "airag:app:var:";
/**
* 初始化变量(仅不存在时设置)
*
* @param username
* @param appId
* @param name
* @param defaultValue
*/
@Override
public void initVariable(String username, String appId, String name, String defaultValue) {
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + username;
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
}
/**
* 追加提示词
*
* @param username
* @param app
* @return
*/
@Override
public String additionalPrompt(String username, AiragApp app) {
String memoryPrompt = app.getMemoryPrompt();
String prompt = app.getPrompt();
if (oConvertUtils.isEmpty(memoryPrompt)) {
return prompt;
}
String variablesStr = app.getVariables();
if (oConvertUtils.isEmpty(variablesStr)) {
return prompt;
}
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
if (variableList == null || variableList.isEmpty()) {
return prompt;
}
String key = CACHE_PREFIX + app.getId() + ":" + username;
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
for (AppVariableVo variable : variableList) {
if (variable.getEnable() != null && !variable.getEnable()) {
continue;
}
String name = variable.getName();
String value = variable.getDefaultValue();
// 优先使用Redis中的值
if (savedValues.containsKey(name)) {
Object savedVal = savedValues.get(name);
if (savedVal != null) {
value = String.valueOf(savedVal);
}
}
if (value == null) {
value = "";
}
// 替换 {{name}}
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
}
return prompt + "\n" + memoryPrompt;
}
/**
* 更新变量值
*
* @param userId
* @param appId
* @param name
* @param value
*/
@Override
public void updateVariable(String userId, String appId, String name, String value) {
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
return;
}
String key = CACHE_PREFIX + appId + ":" + userId;
redisTemplate.opsForHash().put(key, name, value);
}
/**
* 添加变量更新工具
*
* @param params
* @param aiApp
* @param username
*/
@Override
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
if (params.getTools() == null) {
params.setTools(new HashMap<>());
}
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
return;
}
// 构建变量描述信息
String variablesStr = aiApp.getVariables();
List<AppVariableVo> variableList = null;
if (oConvertUtils.isNotEmpty(variablesStr)) {
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
}
//工具描述
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
if (variableList != null && !variableList.isEmpty()) {
descriptionBuilder.append("\n\n可用变量列表");
for (AppVariableVo var : variableList) {
if (var.getEnable() != null && !var.getEnable()) {
continue;
}
descriptionBuilder.append("\n- ").append(var.getName());
if (oConvertUtils.isNotEmpty(var.getDescription())) {
descriptionBuilder.append(": ").append(var.getDescription());
}
}
descriptionBuilder.append("\n\n注意variableName必须是上述列表中的名称之一。");
}
//构建更新变量的工具
ToolSpecification spec = ToolSpecification.builder()
.name("update_variable")
.description(descriptionBuilder.toString())
.parameters(JsonObjectSchema.builder()
.addStringProperty("variableName", "变量名称")
.addStringProperty("value", "变量值")
.required("variableName", "value")
.build())
.build();
//监听工具的调用
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
try {
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
String name = args.getString("variableName");
String value = args.getString("value");
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
//更新变量值
variableService.updateVariable(username, aiApp.getId(), name, value);
return "变量 " + name + " 已更新为: " + value;
} catch (Exception e) {
log.error("更新变量失败", e);
return "更新变量失败: " + e.getMessage();
}
};
params.getTools().put(spec, executor);
}
}

View File

@ -0,0 +1,48 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
/**
* @Description: ai写作生成实体类
*
* @author: wangshuai
* @date: 2026/1/12 15:59
*/
@Data
public class AiWriteGenerateVo {
/**
* 写作类型
*/
private String activeMode;
/**
* 写作内容提示
*/
private String prompt;
/**
* 原文
*/
private String originalContent;
/**
* 长度
*/
private String length;
/**
* 格式
*/
private String format;
/**
* 语气
*/
private String tone;
/**
* 语言
*/
private String language;
}

View File

@ -0,0 +1,46 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @Description: 应用变量配置
* @Author: jeecg-boot
* @Date: 2025-02-26
* @Version: V1.0
*/
@Data
public class AppVariableVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 变量名
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 默认值
*/
private String defaultValue;
/**
* 是否启用
*/
private Boolean enable;
/**
* 动作
*/
private String action;
/**
* 排序
*/
private Integer orderNum;
}

View File

@ -6,6 +6,7 @@ import org.jeecg.modules.airag.common.vo.MessageHistory;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* @Description: 聊天会话
@ -39,4 +40,21 @@ public class ChatConversation {
* 创建时间
*/
private Date createTime;
/**
* 流程入参配置(工作流的额外参数设置)
* key: 参数field, value: 参数值
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
private Map<String, Object> flowInputs;
/**
* portal 应用门户
*/
private String sessionType;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
}

View File

@ -4,6 +4,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* @Description: 发送消息的入参
@ -46,4 +47,56 @@ public class ChatSendParams {
*/
private List<String> images;
/**
* 文件列表
*/
private List<String> files;
/**
* 工作流额外入参配置
* key: 参数field, value: 参数值
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
*/
private Map<String, Object> flowInputs;
/**
* 是否开启网络搜索(仅千问模型支持)
*/
private Boolean enableSearch;
/**
* 是否开启深度思考
*/
private Boolean enableThink;
/**
* 会话类型: portal 应用门户
*/
private String sessionType;
/**
* 是否开启生成绘画
*/
private Boolean enableDraw;
/**
* 绘画模型的id
*/
private String drawModelId;
/**
* 图片尺寸
*/
private String imageSize;
/**
* 一张图片
*/
private String imageUrl;
/**
* 是否保存会话
*/
private Boolean izSaveSession;
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.demo;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
import org.springframework.stereotype.Component;
@ -11,12 +12,15 @@ import java.util.Map;
* @Author: chenrui
* @Date: 2025/3/6 11:42
*/
@Slf4j
@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");
Object index = inputParams.get("index");
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
}
}

View File

@ -0,0 +1,209 @@
package org.jeecg.modules.airag.llm.consts;
/**
* @Description: 流程插件常量
*
* @author: wangshuai
* @date: 2025/12/23 19:37
*/
public interface FlowPluginContent {
/**
* 名称
*/
String NAME = "name";
/**
* 描述
*/
String DESCRIPTION = "description";
/**
* 响应
*/
String RESPONSES = "responses";
/**
* 类型
*/
String TYPE = "type";
/**
* 参数
*/
String PARAMETERS = "parameters";
/**
* 是否必须
*/
String REQUIRED = "required";
/**
* 默认值
*/
String DEFAULT_VALUE = "defaultValue";
/**
* 路径
*/
String PATH = "path";
/**
* 方法
*/
String METHOD = "method";
/**
* 位置
*/
String LOCATION = "location";
/**
* 认证类型
*/
String AUTH_TYPE = "authType";
/**
* token参数名称
*/
String TOKEN_PARAM_NAME = "tokenParamName";
/**
* token参数值
*/
String TOKEN_PARAM_VALUE = "tokenParamValue";
/**
* token
*/
String TOKEN = "token";
/**
* Path位置
*/
String LOCATION_PATH = "Path";
/**
* Header位置
*/
String LOCATION_HEADER = "Header";
/**
* Query位置
*/
String LOCATION_QUERY = "Query";
/**
* Body位置
*/
String LOCATION_BODY = "Body";
/**
* Form-Data位置
*/
String LOCATION_FORM_DATA = "Form-Data";
/**
* String类型
*/
String TYPE_STRING = "String";
/**
* string类型
*/
String TYPE_STRING_LOWER = "string";
/**
* Number类型
*/
String TYPE_NUMBER = "Number";
/**
* number类型
*/
String TYPE_NUMBER_LOWER = "number";
/**
* Integer类型
*/
String TYPE_INTEGER = "Integer";
/**
* integer类型
*/
String TYPE_INTEGER_LOWER = "integer";
/**
* Boolean类型
*/
String TYPE_BOOLEAN = "Boolean";
/**
* boolean类型
*/
String TYPE_BOOLEAN_LOWER = "boolean";
/**
* 工具数量
*/
String TOOL_COUNT = "tool_count";
/**
* 是否启用
*/
String ENABLED = "enabled";
/**
* 输入
*/
String INPUTS = "inputs";
/**
* 输出
*/
String OUTPUTS = "outputs";
/**
* POST请求
*/
String POST = "POST";
/**
* token名称
*/
String X_ACCESS_TOKEN = "X-Access-Token";
/**
* 插件名称
*/
String PLUGIN_NAME = "流程调用";
/**
* 插件描述
*/
String PLUGIN_DESC = "调用工作流";
/**
* 插件请求地址
*/
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
/**
* 记忆库插件名称
*/
String PLUGIN_MEMORY_NAME = "记忆库";
/**
* 记忆库插件描述
*/
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
/**
* 添加记忆路径
*/
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
/**
* 查询记忆路径
*/
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
}

View File

@ -1,5 +1,8 @@
package org.jeecg.modules.airag.llm.consts;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
/**
@ -35,6 +38,11 @@ public class LLMConsts {
*/
public static final String MODEL_TYPE_LLM = "LLM";
/**
* 模型类型: 图像生成
*/
public static final String MODEL_TYPE_IMAGE = "IMAGE";
/**
* 向量模型:默认维度
*/
@ -80,4 +88,34 @@ public class LLMConsts {
*/
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
/**
* DEEPSEEK推理模型
*/
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
/**
* 知识库类型:知识库
*/
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
/**
* 知识库类型:记忆库
*/
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
/**
* 支持文件的后缀
*/
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
/**
* 文件内容最大长度
*/
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
/**
* 上传文件对打数量
*/
public static final int CHAT_FILE_MAX_COUNT = 3;
}

View File

@ -0,0 +1,31 @@
package org.jeecg.modules.airag.llm.controller;
import org.jeecg.common.airag.api.IAiragBaseApi;
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* airag baseAPI Controller
*
* @author sjlei
* @date 2025-12-30
*/
@RestController("airagBaseApiController")
public class AiragBaseApiController implements IAiragBaseApi {
@Autowired
AiragBaseApiImpl airagBaseApi;
@PostMapping("/airag/api/knowledgeWriteTextDocument")
public String knowledgeWriteTextDocument(
@RequestParam("knowledgeId") String knowledgeId,
@RequestParam("title") String title,
@RequestParam("content") String content
) {
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
}
}

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