Compare commits

..

73 Commits

Author SHA1 Message Date
c8e33d43cb 体验地址更改 2025-06-13 13:56:47 +08:00
6c6af870ae jeecgboot vs 敲敲云 2025-06-13 13:53:33 +08:00
79107599a6 增加jeecgboot vs 敲敲云 说明 2025-06-13 13:51:56 +08:00
b6e8b37aee 禁用不好使 2025-06-12 17:00:35 +08:00
195ddd0421 降低shiro版本号,导致不兼容jdk8 2025-06-11 22:18:48 +08:00
9ddad931ff 【issues/8374】分页始终显示在底部 2025-06-10 09:52:27 +08:00
fc05fe1aff 补充.npmrc 2025-06-09 09:22:21 +08:00
7e325f68ca Merge pull request #8389 from 1298191366/master
open api功能优化
2025-06-06 15:22:11 +08:00
f74da23d06 升级积木报表到最新版2.0.0 2025-06-06 11:21:50 +08:00
e279915ba2 --author:wangshuai---date:2025-06-06---for:【QQYUN-12667】open api功能优化--- 2025-06-06 09:54:55 +08:00
bb6f077a95 Revert "//update---author:wangshuai---date:2025-06-05---for:open api功能优化:ak接口返回未明确提示错误---"
This reverts commit 7a031e6135.
2025-06-06 09:35:43 +08:00
7a031e6135 //update---author:wangshuai---date:2025-06-05---for:open api功能优化:ak接口返回未明确提示错误--- 2025-06-06 09:35:27 +08:00
157877f9a6 【issues/8233】resetFields时无法重置 2025-06-05 09:34:54 +08:00
25a71fa66c 【issues/8232】代码设置JSelectDept组件值没翻译 2025-06-05 09:34:11 +08:00
fdc02fa68a [-- author: liusq---date:20250604--for: issue【日志管理】的异常日志列表显示不正常 #8295 --] 2025-06-05 09:33:24 +08:00
ec3c34969a ---author:chenrui---date:2025/6/4-----for:[issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309 --- 2025-06-05 09:31:32 +08:00
5a215525d5 调整aksk测试数据 2025-05-29 16:07:30 +08:00
450b93d916 Merge pull request #8358 from xlh12306/master
使用原生方式对接口进行测试 修复授权管理页面重置AK,SK未写入到数据库中的异常
2025-05-29 15:56:13 +08:00
2d62bad2a9 使用原生方式对接口进行测试 修复授权管理页面重置AK,SK未写入到数据库中的异常
使用原生方式对接口进行测试
修复授权管理页面重置AK,SK未写入到数据库中的异常
2025-05-28 22:22:39 +08:00
0b10096f1c 修复微服务启动失败报错 2025-05-28 15:49:56 +08:00
431ddb8fcb 系统监控的头两个tab不好使,接口404--- 2025-05-27 22:51:25 +08:00
ddf0f61ae5 修复v3.8.0 存在绕过sql黑名单限制sql注入漏洞 #8335
修复minidao `getQueryTableInfo`无法解析带括号的表名、增加数据库名解析能力
2025-05-27 18:40:34 +08:00
4042579167 提供更新的数据库脚本,加入openapi的表和放开AI流程设计菜单 2025-05-27 09:51:52 +08:00
bd5fda5968 访问Swagger接口不带doc.html后缀,会丢失项目前缀/jeecg-boot/导致测试接口,返回下载文件 2025-05-26 19:03:29 +08:00
fdbd9c30ac 解决Swagger3Config 接口文档参数显示问题 #8325 2025-05-25 22:49:07 +08:00
b8b4d3f29d JVxetable升级到最新版 2025-05-25 18:09:19 +08:00
78212aa7c0 通讯录手机号码没有显示出来 #8282 2025-05-25 16:53:33 +08:00
6dc3c6af2a nacos增加aigc配置 2025-05-22 11:01:06 +08:00
b7a6812140 更新pom.xml,添加jeecg-boot-module-airag依赖 2025-05-22 09:25:39 +08:00
7efc51e30e 更新README.md,修改新手指南和在线体验部分的描述 2025-05-19 14:14:18 +08:00
bd83b994bc Merge pull request #8298 from xlh12306/master
openapi相关测试项修改
2025-05-18 17:01:28 +08:00
8fb81f331c 提交openAPiModel 2025-05-16 23:13:11 +08:00
fb188a83a1 提交openapi单元测试代码
调整接口管理的字段位置
授权管理删掉无用配置
授权管理编辑改为重置生成ak sk功能
授权页面应该改成跟角色授权类型的效果
2025-05-16 23:12:28 +08:00
9aea5de668 基于安全认证AK和SK鉴权调用OPEN API功能 2025-05-15 10:23:27 +08:00
8979dd7ae9 Merge branch 'master' into master 2025-05-14 22:17:00 +08:00
a56bd05389 将请求方式修改为大写,修复openapi回调执行调用接口失败的问题 2025-05-14 22:04:06 +08:00
9cf3328ea4 uniapp3代码生成器模板 2025-05-14 18:33:15 +08:00
37c593e1d4 删除jsqlparse代码 2025-05-14 15:04:57 +08:00
49ba40e98a 提交api接口设计前端页面以及后端更改 2025-05-10 23:56:06 +08:00
0e184eaa64 升级shiro到2.0.4 2025-05-09 10:17:14 +08:00
86a3ed9dae 更新OpenApiController,使用创建用户生成token签名,简化用户关联逻辑 2025-05-09 10:11:33 +08:00
94bff11eb1 移除sqlparse代码改调minidao方法、升级fastjson版本号到2.0.57 2025-05-07 13:37:54 +08:00
590d73dfe3 修改版本号 2025-05-06 16:01:31 +08:00
fe9630d15c 【issues/8211】判断逻辑写的有问题--
org/jeecg/config/firewall/interceptor/LowCodeModeInterceptor.java
2025-05-06 09:24:20 +08:00
77ae25b86a 解决jeecg-boot-V3.8.0使用下拉树组件时高频率报错UT005071: Undertow request failed HttpServerExchange{ GET /sys/dict/loadTreeData} #8217 2025-05-04 17:28:30 +08:00
3b34276cf8 新增一个UI组件,支持通过部门选人,更加便捷 2025-05-04 16:34:12 +08:00
cffba084fc 【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错 2025-05-04 16:27:28 +08:00
d1589acc41 回滚swagger配置 2025-05-03 17:14:18 +08:00
0d18e536f0 解决swagger生成文档,参数乱码问题 2025-04-29 16:14:51 +08:00
21392c44f8 issues/8181】同步部门(将钉钉部门同步到本地) #8181 2025-04-29 09:54:21 +08:00
2730d8e06f 流程编排脚本节点不好使问题 2025-04-28 17:36:57 +08:00
0002606d41 流程编排脚本节点不好使问题 2025-04-28 12:54:38 +08:00
8bd19484ee 升级jeecg-aiflow依赖版本至1.0.3 2025-04-28 11:51:55 +08:00
5d2db92613 默认切jdk17 2025-04-27 10:21:56 +08:00
c1b39d21dd 升级AI引擎,底层接口开放 2025-04-26 16:16:50 +08:00
e83c9b8190 【jvxeTable】expandConfig属性无用 2025-04-26 16:09:49 +08:00
9bd03f467d 【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效。 2025-04-25 16:59:03 +08:00
e032591366 • 【AI】兼容jdk21,让程序可以启动,但是提示AI流程编排无法使用
• 【AI】流程,入参中的必填没有校验
• 【AI】开启多租户,导致流程接口调用提示流程不存在
• 【AI】流程调用流程接口改成无需登录
2025-04-24 14:02:32 +08:00
de767e07b4 【issues/8093】删除后会先变成编码再显示label文字 2025-04-24 09:27:58 +08:00
b77d3e36ab 【issues/8075】可编辑行无法获取最新的值 2025-04-24 09:27:16 +08:00
7885aaed3b 【issues/8137】vxetable表格禁用后分页隐藏了 2025-04-24 09:26:27 +08:00
6f4c2eb77c 【pull/8013】修复 BasicTable position 属性类型配置 --- 2025-04-24 09:25:22 +08:00
3f0597a0f6 【issues/7986】插槽方式弹窗中取消该数据checkbox的选中状态,需要点击第二次才生效 2025-04-24 09:24:48 +08:00
04a3764f00 弹窗中勾选,再点取消,值被选中了
弹窗勾选了值,点击取消再次打开弹窗遗留了上次的勾选的值
2025-04-24 09:24:00 +08:00
68464109de Merge remote-tracking branch 'origin/master' 2025-04-23 19:10:59 +08:00
79866c5823 AI大模型使用本地部署的deepseek报错 #8164 2025-04-23 19:10:44 +08:00
d64b8ecaef Merge pull request #8160 from EightMonth/master
修复 #8134
2025-04-23 18:42:45 +08:00
ec9f2b146a 敲敲云支持在线免费搭建AI知识库 2025-04-23 15:32:37 +08:00
69fd2888a1 Update application.yml 2025-04-23 11:12:59 +08:00
cb1d8e3527 修复 #8134 2025-04-22 10:05:04 +08:00
a89b299a4b 邮件发送失败,修改yml配置方式 2025-04-21 10:19:23 +08:00
7a15bfc161 错别字修复 2025-04-20 16:51:56 +08:00
65f7eb9542 支持全信创环境 2025-04-18 14:54:53 +08:00
103 changed files with 6000 additions and 1491 deletions

View File

@ -25,7 +25,7 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染。
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
| 功能 | Dify | Jeecg AI |
@ -63,11 +63,17 @@ JeecgBoot平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
## 产品体验
- 使用手册https://help.jeecg.com/aigc
- 演示地址https://boot3.jeecg.com
#### 在线体验
- JeecgBoot演示 https://boot3.jeecg.com
- 敲敲云在线搭建AI知识库https://app.qiaoqiaoyun.com
## 技术交流
- 开发文档https://help.jeecg.com/aigc
- QQ群716488839
## 功能列表

View File

@ -41,10 +41,22 @@ JeecgBoot 提供了一系列 `低代码能力`,实现`真正的零代码`在
适用项目
-----------------------------------
JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中支持信创国产化(默认适配达梦和人大金仓)。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM其半智能手工Merge的开发方式可以显著提高开发效率70%以上,极大降低开发成本。
JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中支持信创国产化。尤其适合SAAS项目、企业信息管理系统MIS、内部办公系统OA、企业资源计划系统ERP、客户关系管理系统CRM其半智能手工Merge的开发方式可以显著提高开发效率70%以上,极大降低开发成本。
又是一个全栈式 AI 开发平台,快速帮助企业构建和部署个性化的 AI 应用。
信创国产化
-----------------------------------
JeecgBoot 是一个开源低代码开发平台,支持全信创环境。它兼容多种国产操作系统和数据库,包括:
- 操作系统:国产麒麟、银河麒麟等国产系统几乎都是基于 Linux 内核,因此它们具有良好的兼容性。
- 数据库达梦、人大金仓、TiDB , [转库文档](https://my.oschina.net/jeecg/blog/4905722)
- 中间件:东方通 TongWeb、TongRDS宝兰德 AppServer、CacheDB, [信创配置文档](https://help.jeecg.com/java/tongweb-deploy/)
通过这些适配JeecgBoot 为使用国产软件和硬件的用户提供了高效的开发解决方案。
项目说明
-----------------------------------
@ -56,14 +68,6 @@ JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 在线演示 [平台演示](http://boot3.jeecg.com) | [APP演示](http://jeecg.com/appIndex) | [体验低代码](https://jeecg.blog.csdn.net/article/details/106079007) | [体验零代码](https://app.qiaoqiaoyun.com/myapps/index)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [如何反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
@ -75,6 +79,29 @@ JeecgBoot AI低代码平台可以应用在任何J2EE项目的开发中
在线体验
-----------------------------------
> JeecgBoot vs 敲敲云
> - JeecgBoot是低代码产品拥有很多低代码能力比如流程设计、表单设计、大屏设计代码生成器适合半开发模式开发+低代码结合),也可以集成零代码的应用管理模块;
> - 敲敲云是零代码产品完全不写代码通过配置搭建业务系统其在jeecgboot基础上研发而成删除了online、代码生成、OA等很多需要编码的功能只保留了应用管理和聊天、流程、日程、文件四个标准OA功能
- JeecgBoot低代码 https://boot3.jeecg.com
- 敲敲云零代码https://app.qiaoqiaoyun.com
- APP演示: http://jeecg.com/appIndex
技术文档
-----------------------------------
- 官方网站: [http://www.jeecg.com](http://www.jeecg.com)
- 开发文档: [文档中心](https://help.jeecg.com) | [AIGC大模块](https://help.jeecg.com/aigc) | [低代码初体验一分钟](https://jeecg.blog.csdn.net/article/details/106079007)
- 新手指南: [快速入门](http://www.jeecg.com/doc/quickstart) | [入门视频](http://jeecg.com/doc/video) | [反馈问题](https://github.com/jeecgboot/JeecgBoot/issues/new?template=bug_report.md)
- QQ交流群 ⑩716488839、⑨808791225(满)、其他(满)
AIGC应用平台介绍
-----------------------------------
@ -93,6 +120,8 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
[![](https://jeecgos.oss-cn-beijing.aliyuncs.com/files/jeecg_aivideo.png)](https://www.bilibili.com/video/BV1zmd7YFE4w)
##### Dify `VS` JEECG AI
> JEECG AI与Dify相比在多个方面展现出显著的优势特别是在文档处理、格式和图片保持方面。以下是一些具体的优点
@ -101,7 +130,7 @@ JeecgBoot 平台的AIGC功能模块是一套类似`Dify`的`AIGC应用开发
> - 对话回复格式美观:
> 在对话过程中JEECG AI能够保持回复内容的原格式也不丢失图片使得输出的文章更加美观不会出现格式错乱的情况还支持图片的渲染。
> - PDF文档导入与格式转换
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
> JEECG AI在处理PDF文档时能够更好地保持原始格式和图片确保转换后的内容与原始文档一致。这功能在许多AI产品中表现不佳而JEECG AI在这方面做出了显著的优化
##### 功能大模块

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,170 @@
/*
Navicat Premium Data Transfer
Source Server : mysql5.7
Source Server Type : MySQL
Source Server Version : 50738 (5.7.38)
Source Host : 127.0.0.1:3306
Source Schema : jeecg-boot
Target Server Type : MySQL
Target Server Version : 50738 (5.7.38)
File Encoding : 65001
Date: 15/05/2025 10:18:36
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for open_api
-- ----------------------------
DROP TABLE IF EXISTS `open_api`;
CREATE TABLE `open_api` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口名称',
`request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求方法',
`request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口地址',
`black_list` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP 黑名单',
`body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '请求体内容',
`origin_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原始地址',
`status` int(10) NULL DEFAULT NULL COMMENT '状态',
`del_flag` int(10) NULL DEFAULT NULL COMMENT '删除标识',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`headers_json` json NULL COMMENT '请求头json',
`params_json` json NULL COMMENT '请求参数json',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '接口表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of open_api
-- ----------------------------
INSERT INTO `open_api` VALUES ('1922132683346649090', '根据部门查询用户', 'GET', 'TEwcXBlr', NULL, NULL, '/sys/user/queryUserByDepId', 1, 0, 'admin', '2025-05-13 11:31:58', 'admin', '2025-05-15 10:10:01', '[]', '[{\"id\": \"row_24\", \"note\": \"\", \"paramKey\": \"id\", \"required\": \"1\", \"defaultValue\": \"\"}]');
-- ----------------------------
-- Table structure for open_api_auth
-- ----------------------------
DROP TABLE IF EXISTS `open_api_auth`;
CREATE TABLE `open_api_auth` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权名称',
`ak` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'AK',
`sk` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'SK',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
`system_user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联系统用户名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '权限表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of open_api_auth
-- ----------------------------
INSERT INTO `open_api_auth` VALUES ('1922164194775056386', 'scott', 'ak-pFjyNHWRsJEFWlu6', '4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS', 'admin', '2025-05-13 13:37:11', NULL, NULL, 'e9ca23d68d884d4ebb19d07889727dae');
-- ----------------------------
-- Table structure for open_api_log
-- ----------------------------
DROP TABLE IF EXISTS `open_api_log`;
CREATE TABLE `open_api_log` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`call_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '调用ID',
`call_time` datetime NULL DEFAULT NULL COMMENT '调用时间',
`used_time` bigint(20) NULL DEFAULT NULL COMMENT '耗时',
`response_time` datetime NULL DEFAULT NULL COMMENT '响应时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '调用记录表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of open_api_log
-- ----------------------------
INSERT INTO `open_api_log` VALUES ('1922175238557913090', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:04', 94, '2025-05-13 14:21:04');
INSERT INTO `open_api_log` VALUES ('1922175436256432130', '1922132683346649090', '1922164194775056386', '2025-05-13 14:21:51', 38, '2025-05-13 14:21:51');
INSERT INTO `open_api_log` VALUES ('1922175487921868802', '1922132683346649090', '1922164194775056386', '2025-05-13 14:22:03', 31, '2025-05-13 14:22:03');
INSERT INTO `open_api_log` VALUES ('1922176033789562883', '1922132683346649090', '1922164194775056386', '2025-05-13 14:24:13', 27, '2025-05-13 14:24:13');
INSERT INTO `open_api_log` VALUES ('1922176583943835650', '1922132683346649090', '1922164194775056386', '2025-05-13 14:26:25', 39, '2025-05-13 14:26:25');
INSERT INTO `open_api_log` VALUES ('1922177249969934337', '1922132683346649090', '1922164194775056386', '2025-05-13 14:28:08', 55250, '2025-05-13 14:29:03');
INSERT INTO `open_api_log` VALUES ('1922180212645941249', '1922132683346649090', '1922164194775056386', '2025-05-13 14:40:46', 4162, '2025-05-13 14:40:50');
INSERT INTO `open_api_log` VALUES ('1922180441692688385', '1922132683346649090', '1922164194775056386', '2025-05-13 14:41:11', 33346, '2025-05-13 14:41:44');
INSERT INTO `open_api_log` VALUES ('1922180521686454273', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:00', 3570, '2025-05-13 14:42:03');
INSERT INTO `open_api_log` VALUES ('1922180965825499138', '1922132683346649090', '1922164194775056386', '2025-05-13 14:42:10', 99211, '2025-05-13 14:43:49');
INSERT INTO `open_api_log` VALUES ('1922181034515615746', '1922132683346649090', '1922164194775056386', '2025-05-13 14:43:52', 14005, '2025-05-13 14:44:06');
INSERT INTO `open_api_log` VALUES ('1922183171307982850', '1922132683346649090', '1922164194775056386', '2025-05-13 14:52:15', 19834, '2025-05-13 14:52:35');
INSERT INTO `open_api_log` VALUES ('1922184177068523521', '1922132683346649090', '1922164194775056386', '2025-05-13 14:56:34', 748, '2025-05-13 14:56:35');
INSERT INTO `open_api_log` VALUES ('1922184729043107841', '1922132683346649090', '1922164194775056386', '2025-05-13 14:58:46', 1031, '2025-05-13 14:58:47');
INSERT INTO `open_api_log` VALUES ('1922184806453182465', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:05', 68, '2025-05-13 14:59:05');
INSERT INTO `open_api_log` VALUES ('1922184918382379009', '1922132683346649090', '1922164194775056386', '2025-05-13 14:59:10', 22155, '2025-05-13 14:59:32');
INSERT INTO `open_api_log` VALUES ('1922185292635844610', '1922132683346649090', '1922164194775056386', '2025-05-13 15:00:55', 6267, '2025-05-13 15:01:01');
INSERT INTO `open_api_log` VALUES ('1922186002672791554', '1922132683346649090', '1922164194775056386', '2025-05-13 15:03:23', 27554, '2025-05-13 15:03:50');
INSERT INTO `open_api_log` VALUES ('1922187506582425601', '1922132683346649090', '1922164194775056386', '2025-05-13 15:09:45', 3464, '2025-05-13 15:09:49');
INSERT INTO `open_api_log` VALUES ('1922187586597163011', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:08', 82, '2025-05-13 15:10:08');
INSERT INTO `open_api_log` VALUES ('1922187924741951490', '1922132683346649090', '1922164194775056386', '2025-05-13 15:10:49', 39590, '2025-05-13 15:11:28');
INSERT INTO `open_api_log` VALUES ('1922188138710261761', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:19', 758, '2025-05-13 15:12:19');
INSERT INTO `open_api_log` VALUES ('1922188290661507073', '1922132683346649090', '1922164194775056386', '2025-05-13 15:12:29', 26527, '2025-05-13 15:12:56');
INSERT INTO `open_api_log` VALUES ('1922189701755424769', '1922132683346649090', '1922164194775056386', '2025-05-13 15:18:28', 3619, '2025-05-13 15:18:32');
INSERT INTO `open_api_log` VALUES ('1922190076784803841', '1922132683346649090', '1922164194775056386', '2025-05-13 15:20:01', 741, '2025-05-13 15:20:02');
INSERT INTO `open_api_log` VALUES ('1922836671113101313', '1922132683346649090', '1922164194775056386', '2025-05-15 10:09:21', 186, '2025-05-15 10:09:22');
INSERT INTO `open_api_log` VALUES ('1922836856287428610', '1922132683346649090', '1922164194775056386', '2025-05-15 10:10:06', 145, '2025-05-15 10:10:06');
-- ----------------------------
-- Table structure for open_api_permission
-- ----------------------------
DROP TABLE IF EXISTS `open_api_permission`;
CREATE TABLE `open_api_permission` (
`id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`api_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '接口ID',
`api_auth_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '认证ID',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'openapi授权' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of open_api_permission
-- ----------------------------
INSERT INTO `open_api_permission` VALUES ('1922164225875820545', '1922132683346649090', '1922164194775056386', 'admin', '2025-05-13 13:37:18', NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1917957565728198657', '1922109301837606914', '接口文档', '/openapi/SwaggerUI', 'openapi/SwaggerUI', 1, '', null, 1, null, '0', 1, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 23:01:32', 'admin', '2025-05-13 09:59:46', 0, 0, null, 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('1922109301837606914', '', 'OpenApi管理', '/openapi', 'layouts/RouteView', 1, '', null, 0, null, '0', 12.1, 0, 'ant-design:swap-outlined', 0, 0, 0, 0, null, 'admin', '2025-05-13 09:59:03', 'admin', '2025-05-13 10:02:43', 0, 0, null, 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193340030', '1922109301837606914', '接口管理', '/openapi/openApiList', 'openapi/OpenApiList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', 'admin', '2025-05-13 09:59:24', 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350031', '2025050104193340030', '添加接口管理', null, null, 0, null, null, 2, 'openapi:open_api:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350032', '2025050104193340030', '编辑接口管理', null, null, 0, null, null, 2, 'openapi:open_api:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350033', '2025050104193340030', '删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350034', '2025050104193340030', '批量删除接口管理', null, null, 0, null, null, 2, 'openapi:open_api:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350035', '2025050104193340030', '导出excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050104193350036', '2025050104193340030', '导入excel_接口管理', null, null, 0, null, null, 2, 'openapi:open_api:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 16:19:03', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940200', '1922109301837606914', '授权管理', '/openapi/openApiAuthList', 'openapi/OpenApiAuthList', 1, null, null, 1, null, '1', 0, 0, null, 0, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', 'admin', '2025-05-13 09:59:35', 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940201', '2025050105554940200', '添加授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:add', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940202', '2025050105554940200', '编辑授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:edit', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940203', '2025050105554940200', '删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:delete', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940204', '2025050105554940200', '批量删除授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:deleteBatch', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940205', '2025050105554940200', '导出excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:exportXls', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_permission (id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) VALUES ('2025050105554940206', '2025050105554940200', '导入excel_授权管理', null, null, 0, null, null, 2, 'openapi:open_api_auth:importExcel', '1', null, 0, null, 1, 0, 0, 0, null, 'admin', '2025-05-01 17:55:20', null, null, 0, 0, '1', 0);
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071739539457', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193340030', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648321', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350031', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648322', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350032', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648323', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350033', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648324', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350034', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648325', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350035', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917857071806648326', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050104193350036', null, '2025-05-01 16:22:13', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149426864129', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940200', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058436', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940203', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058437', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940204', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058438', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940205', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917881149431058439', 'f6817f48af4fb3af11b9e8bf182f618b', '2025050105554940206', null, '2025-05-01 17:57:53', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1917957659860963330', 'f6817f48af4fb3af11b9e8bf182f618b', '1917957565728198657', null, '2025-05-01 23:01:55', '0:0:0:0:0:0:0:1');
INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES ('1922109760551858178', 'f6817f48af4fb3af11b9e8bf182f618b', '1922109301837606914', null, '2025-05-13 10:00:53', '0:0:0:0:0:0:0:1');

View File

@ -314,5 +314,10 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,17 +11,16 @@ import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.event.EventListener;
import org.springframework.http.CacheControl;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@ -59,6 +58,14 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
@Autowired(required = false)
private PrometheusMeterRegistry prometheusMeterRegistry;
/**
* meterRegistryPostProcessor
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
*/
@Autowired(required = false)
@Qualifier("meterRegistryPostProcessor")
private BeanPostProcessor meterRegistryPostProcessor;
/**
* 静态资源的配置 - 使得可以从磁盘中读取 Html、图片、视频、音频等
*/
@ -147,12 +154,17 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
/**
* 解决metrics端点不显示jvm信息的问题(zyf)
* 监听应用启动完成事件,确保 PrometheusMeterRegistry 已经初始化
* for [QQYUN-12558]【监控】系统监控的头两个tab不好使接口404
* @param event
* @author chenrui
* @date 2025/5/26 16:46
*/
@Bean
@ConditionalOnBean(name = "meterRegistryPostProcessor")
InitializingBean forcePrometheusPostProcessor(BeanPostProcessor meterRegistryPostProcessor) {
return () -> meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
if(null != meterRegistryPostProcessor){
meterRegistryPostProcessor.postProcessAfterInitialization(prometheusMeterRegistry, "");
}
}
// /**

View File

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

View File

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

View File

@ -1,75 +0,0 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestInjectWithSqlParser {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
assertFalse(isExistSqlInject("select * from test"));
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
assertFalse(isExistSqlInject("WITH SUB1 AS (SELECT user FROM t1) SELECT * FROM T2 WHERE id > 123 "));
//存在sql注入
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
assertTrue(isExistSqlInject("select * from users;show databases;"));
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
assertTrue(isExistSqlInject("update user set name = '123'"));
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertTrue(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertTrue(isExistSqlInject("select * from dc_device where 2=2.0 or 2 != 4"));
assertTrue(isExistSqlInject("select * from dc_device where 1!=2.0"));
assertTrue(isExistSqlInject("select * from dc_device where id=floor(2.0)"));
assertTrue(isExistSqlInject("select * from dc_device where not true"));
assertTrue(isExistSqlInject("select * from dc_device where 1 or id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where 'tom' or id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where '-2.3' "));
assertTrue(isExistSqlInject("select * from dc_device where 2 "));
assertTrue(isExistSqlInject("select * from dc_device where (3+2) "));
assertTrue(isExistSqlInject("select * from dc_device where -1 IS TRUE"));
assertTrue(isExistSqlInject("select * from dc_device where 'hello' is null "));
assertTrue(isExistSqlInject("select * from dc_device where '2022-10-31' and id > 0"));
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1!=2.0 "));
assertTrue(isExistSqlInject("select * from dc_device where id > 0 or 1 in (1,3,4) "));
assertTrue(isExistSqlInject("select * from dc_device UNION select name from other"));
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
}
}

View File

@ -1,50 +0,0 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestSqlInjectForDict {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForDictSql(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("sys_user,realname,id"));
assertFalse(isExistSqlInject("oa_officialdoc_organcode,organ_name,id"));
assertFalse(isExistSqlInject("onl_cgform_head where table_type!=3 and copy_type=0,table_txt,table_name"));
assertFalse(isExistSqlInject("onl_cgform_head where copy_type = 0,table_txt,table_name"));
//存在sql注入
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
}
}

View File

@ -1,60 +0,0 @@
package org.jeecg.test.sqlinjection;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* SQL注入攻击检查测试
* @author: liusq
* @date: 2023年09月08日
*/
@Slf4j
public class TestSqlInjectForOnlineReport {
/**
* 注入测试
*
* @param sql
* @return
*/
private boolean isExistSqlInject(String sql) {
try {
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
return false;
} catch (Exception e) {
log.info("===================================================");
return true;
}
}
@Test
public void test() throws JSQLParserException {
//不存在sql注入
assertFalse(isExistSqlInject("select * from fm_time where dept_id=:sqlparamsmap.id and time=:sqlparamsmap.time"));
assertFalse(isExistSqlInject("select * from test"));
assertFalse(isExistSqlInject("select load_file(\"C:\\\\benben.txt\")"));
assertFalse(isExistSqlInject("select * from dc_device where id in (select id from other)"));
assertFalse(isExistSqlInject("select * from dc_device UNION select name from other"));
//存在sql注入
assertTrue(isExistSqlInject("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)"));
assertTrue(isExistSqlInject("or 1= 1 --"));
assertTrue(isExistSqlInject("select * from test where sleep(%23)"));
assertTrue(isExistSqlInject("select * from test where SLEEP(3)"));
assertTrue(isExistSqlInject("select * from test where id=1 and multipoint((select * from(select * from(select user())a)b));"));
assertTrue(isExistSqlInject("select * from users;show databases;"));
assertTrue(isExistSqlInject("select * from dc_device where id=1 and length((select group_concat(table_name) from information_schema.tables where table_schema=database()))>13"));
assertTrue(isExistSqlInject("update user set name = '123'"));
assertTrue(isExistSqlInject("SELECT * FROM users WHERE username = 'admin' AND password = '123456' OR 1=1;--"));
assertTrue(isExistSqlInject("select * from users where id=1 and (select count(*) from information_schema.tables where table_schema='数据库名')>4 %23"));
assertTrue(isExistSqlInject("select * from dc_device where sleep(5) %23"));
}
}

View File

@ -1,103 +0,0 @@
package org.jeecg.test.sqlinjection;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import org.jeecg.common.util.SqlInjectionUtil;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* @Description: SQL注入测试类
* @author: scott
* @date: 2023年08月14日 9:55
*/
public class TestSqlInjection {
/**
* 表名带别名同时有html编码字符
*/
@Test
public void testSpecialSQL() {
String tableName = "sys_user t";
//解决使用参数tableName=sys_user t&复测,漏洞仍然存在
if (tableName.contains(" ")) {
tableName = tableName.substring(0, tableName.indexOf(" "));
}
//【issues/4393】 sys_user , (sys_user), sys_user%20, %60sys_user%60
String reg = "\\s+|\\(|\\)|`";
tableName = tableName.replaceAll(reg, "");
System.out.println(tableName);
}
/**
* 测试sql是否含sql注入风险
* <p>
* mybatis plus的方法
*/
@Test
public void sqlInjectionCheck() {
String sql = "select * from sys_user";
System.out.println(SqlInjectionUtils.check(sql));
}
/**
* 测试sql是否有SLEEP风险
* <p>
* mybatisPlus的方法
*/
@Test
public void sqlSleepCheck() {
SqlInjectionUtil.checkSqlAnnotation("(SELECT 6240 FROM (SELECT(SLEEP(5))and 1=2)vidl)");
}
/**
* 测试sql是否含sql注入风险
* <p>
* 自定义方法
*/
@Test
public void sqlInjectionCheck2() {
String sql = "select * from sys_user";
SqlInjectionUtil.specialFilterContentForOnlineReport(sql);
}
/**
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
* <p>
* 判断字段名是否符合规范
*/
@Test
public void testFieldSpecification() {
List<String> list = new ArrayList();
list.add("Hello World!");
list.add("Hello%20World!");
list.add("HelloWorld!");
list.add("Hello World");
list.add("age");
list.add("user_name");
list.add("user_name%20");
list.add("user_name%20 ");
for (String input : list) {
boolean containsSpecialChars = isValidString(input);
System.out.println("input:" + input + " ,包含空格和特殊字符: " + containsSpecialChars);
}
}
/**
* 字段定义只能是是字母 数字 下划线的组合(不允许有空格、转义字符串等)
*
* @param input
* @return
*/
private static boolean isValidString(String input) {
Pattern pattern = Pattern.compile("^[a-zA-Z0-9_]+$");
return pattern.matcher(input).matches();
}
}

View File

@ -1,109 +0,0 @@
package org.jeecg.test.sqlparse;
import net.sf.jsqlparser.JSQLParserException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import org.junit.jupiter.api.Test;
import java.util.Map;
/**
* 针对 JSqlParserUtils 的单元测试
*/
public class JSqlParserUtilsTest {
private static final String[] sqlList = new String[]{
"select * from sys_user",
"select u.* from sys_user u",
"select u.*, c.name from sys_user u, demo c",
"select u.age, c.name from sys_user u, demo c",
"select sex, age, c.name from sys_user, demo c",
// 别名测试
"select username as realname from sys_user",
"select username as realname, u.realname as aaa, u.id bbb from sys_user u",
// 不存在真实地查询字段
"select count(1) from sys_user",
// 函数式字段
"select max(sex), id from sys_user",
// 复杂嵌套函数式字段
"select CONCAT(CONCAT(' _ ', sex), ' - ' , birthday) as info, id from sys_user",
// 更复杂的嵌套函数式字段
"select CONCAT(CONCAT(101,'_',NULL, DATE(create_time),'_',sex),' - ',birthday) as info, id from sys_user",
// 子查询SQL
"select u.name1 as name2 from (select username as name1 from sys_user) u",
// 多层嵌套子查询SQL
"select u2.name2 as name3 from (select u1.name1 as name2 from (select username as name1 from sys_user) u1) u2",
// 字段子查询SQL
"select id, (select username as name1 from sys_user u2 where u1.id = u2.id) as name2 from sys_user u1",
// 带条件的SQL不解析where条件里的字段但不影响解析查询字段
"select username as name1 from sys_user where realname LIKE '%张%'",
// 多重复杂关联表查询解析包含的表为sys_user, sys_depart, sys_dict_item, demo
"" +
"SELECT " +
" u.*, d.age, sd.item_text AS sex, (SELECT count(sd.id) FROM sys_depart sd) AS count " +
"FROM " +
" (SELECT sd.username AS foo, sd.realname FROM sys_user sd) u, " +
" demo d " +
"LEFT JOIN sys_dict_item AS sd ON d.sex = sd.item_value " +
"WHERE sd.dict_id = '3d9a351be3436fbefb1307d4cfb49bf2'",
};
@Test
public void testParseSelectSql() {
System.out.println("-----------------------------------------");
for (String sql : sqlList) {
System.out.println("待测试的sql" + sql);
try {
// 解析所有的表名key=表名value=解析后的sql信息
Map<String, SelectSqlInfo> parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
assert parsedMap != null;
for (Map.Entry<String, SelectSqlInfo> entry : parsedMap.entrySet()) {
System.out.println("表名:" + entry.getKey());
this.printSqlInfo(entry.getValue(), 1);
}
} catch (JSQLParserException e) {
System.out.println("SQL解析出现异常" + e.getMessage());
}
System.out.println("-----------------------------------------");
}
}
private void printSqlInfo(SelectSqlInfo sqlInfo, int level) {
String beforeStr = this.getBeforeStr(level);
if (sqlInfo.getFromTableName() == null) {
// 子查询
System.out.println(beforeStr + "子查询:" + sqlInfo.getFromSubSelect().getParsedSql());
this.printSqlInfo(sqlInfo.getFromSubSelect(), level + 1);
} else {
// 非子查询
System.out.println(beforeStr + "查询的表名:" + sqlInfo.getFromTableName());
}
if (oConvertUtils.isNotEmpty(sqlInfo.getFromTableAliasName())) {
System.out.println(beforeStr + "查询的表别名:" + sqlInfo.getFromTableAliasName());
}
if (sqlInfo.isSelectAll()) {
System.out.println(beforeStr + "查询的字段:*");
} else {
System.out.println(beforeStr + "查询的字段:" + sqlInfo.getSelectFields());
System.out.println(beforeStr + "真实的字段:" + sqlInfo.getRealSelectFields());
if (sqlInfo.getFromTableName() == null) {
System.out.println(beforeStr + "所有的字段(包括子查询):" + sqlInfo.getAllRealSelectFields());
}
}
}
// 打印前缀,根据层级来打印
private String getBeforeStr(int level) {
if (level == 0) {
return "";
}
StringBuilder beforeStr = new StringBuilder();
for (int i = 0; i < level; i++) {
beforeStr.append(" ");
}
beforeStr.append("- ");
return beforeStr.toString();
}
}

View File

@ -55,9 +55,48 @@
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-aiflow-jdk8</artifactId>
<version>1.0.0</version>
<artifactId>jeecg-aiflow</artifactId>
<version>1.0.4</version>
</dependency>
<!-- aiflow 脚本依赖 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-kotlin</artifactId>
<version>${liteflow.version}</version>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-aviator</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<artifactId>aviator</artifactId>
<groupId>com.googlecode.aviator</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- aiflow 脚本依赖 -->
<!-- langChain4j model support -->
<dependency>

View File

@ -24,6 +24,12 @@
<artifactId>hibernate-re</artifactId>
</dependency>
<!-- AI大模型管理 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module-airag</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- 企业微信/钉钉 api -->
<dependency>
<groupId>org.jeecgframework</groupId>

View File

@ -4,14 +4,14 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgSqlInjectionException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.util.sqlparse.JSqlParserUtils;
import org.jeecg.common.util.sqlparse.vo.SelectSqlInfo;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.firewall.SqlInjection.IDictTableWhiteListHandler;
import org.jeecg.config.firewall.interceptor.LowCodeModeInterceptor;
import org.jeecg.modules.system.entity.SysTableWhiteList;
import org.jeecg.modules.system.security.DictQueryBlackListHandler;
import org.jeecg.modules.system.service.ISysTableWhiteListService;
import org.jeecgframework.minidao.sqlparser.impl.vo.SelectSqlInfo;
import org.jeecgframework.minidao.util.MiniDaoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -65,7 +65,7 @@ public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler
public boolean isPassBySql(String sql) {
Map<String, SelectSqlInfo> parsedMap = null;
try {
parsedMap = JSqlParserUtils.parseAllSelectTable(sql);
parsedMap = MiniDaoUtil.parseAllSelectTable(sql);
} catch (Exception e) {
log.warn("校验sql语句解析报错{}", e.getMessage());
}
@ -127,7 +127,7 @@ public class DictTableWhiteListHandlerImpl implements IDictTableWhiteListHandler
log.info("字典拼接的查询SQL{}", sql);
try {
// 进行SQL解析
JSqlParserUtils.parseSelectSqlInfo(sql);
MiniDaoUtil.parseSelectSqlInfo(sql);
} catch (Exception e) {
// 如果SQL解析失败则通过字段名和表名进行校验
return checkWhiteList(tableName, new HashSet<>(Arrays.asList(fields)));

View File

@ -19,6 +19,21 @@ public class CustomInMemoryHttpTraceRepository extends InMemoryHttpTraceReposito
return super.findAll();
}
/**
* for [issues/8309]系统监控>请求追踪,列表每刷新一下,总数据就减一#8309
* @param trace
* @author chenrui
* @date 2025/6/4 19:38
*/
@Override
public void add(HttpTrace trace) {
// 只有当请求不是OPTIONS方法并且URI不包含httptrace时才记录数据
if (!"OPTIONS".equals(trace.getRequest().getMethod()) &&
!trace.getRequest().getUri().toString().contains("httptrace")) {
super.add(trace);
}
}
public List<HttpTrace> findAll(String query) {
List<HttpTrace> allTrace = super.findAll();
if (null != allTrace && !allTrace.isEmpty()) {

View File

@ -78,7 +78,9 @@ public class CustomUndertowMetricsHandler {
// 获取当前 session如果不存在则创建
Session session = sessionManager.getSession(exchange, sessionConfig);
if (session == null) {
sessionManager.createSession(exchange, sessionConfig);
try {
sessionManager.createSession(exchange, sessionConfig);
} catch (Exception e) {}
}
// 执行下一个 Handler

View File

@ -1,5 +1,7 @@
package org.jeecg.modules.openapi.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -10,14 +12,14 @@ import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.RestUtil;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiAuth;
import org.jeecg.modules.openapi.entity.OpenApiHeader;
import org.jeecg.modules.openapi.entity.OpenApiParam;
import org.jeecg.modules.openapi.generator.PathGenerator;
import org.jeecg.modules.openapi.service.OpenApiAuthService;
import org.jeecg.modules.openapi.service.OpenApiHeaderService;
import org.jeecg.modules.openapi.service.OpenApiParamService;
import org.jeecg.modules.openapi.service.OpenApiService;
import org.jeecg.modules.openapi.swagger.*;
import org.jeecg.modules.system.entity.SysUser;
@ -26,13 +28,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
/**
* @date 2024/12/10 9:11
@ -44,9 +48,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
@Autowired
private RestTemplate restTemplate;
@Autowired
private OpenApiParamService openApiParamService;
@Autowired
private OpenApiHeaderService openApiHeaderService;
private RedisUtil redisUtil;
@Autowired
private ISysUserService sysUserService;
@Autowired
@ -67,10 +69,6 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
QueryWrapper<OpenApi> queryWrapper = QueryGenerator.initQueryWrapper(openApi, req.getParameterMap());
Page<OpenApi> page = new Page<>(pageNo, pageSize);
IPage<OpenApi> pageList = service.page(page, queryWrapper);
for (OpenApi api : pageList.getRecords()) {
api.setParams(openApiParamService.findByApiId(api.getId()));
api.setHeaders(openApiHeaderService.findByApiId(api.getId()));
}
return Result.ok(pageList);
}
@ -82,16 +80,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
*/
@PostMapping(value = "/add")
public Result<?> add(@RequestBody OpenApi openApi) {
if (service.save(openApi)) {
if (!CollectionUtils.isEmpty(openApi.getHeaders())) {
openApiHeaderService.saveBatch(openApi.getHeaders());
}
if (!CollectionUtils.isEmpty(openApi.getParams())) {
openApiParamService.saveBatch(openApi.getParams());
}
}
service.save(openApi);
return Result.ok("添加成功!");
}
@ -103,18 +92,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
*/
@PutMapping(value = "/edit")
public Result<?> edit(@RequestBody OpenApi openApi) {
if (service.updateById(openApi)) {
openApiHeaderService.deleteByApiId(openApi.getId());
openApiParamService.deleteByApiId(openApi.getId());
if (!CollectionUtils.isEmpty(openApi.getHeaders())) {
openApiHeaderService.saveBatch(openApi.getHeaders());
}
if (!CollectionUtils.isEmpty(openApi.getParams())) {
openApiParamService.saveBatch(openApi.getParams());
}
}
service.updateById(openApi);
return Result.ok("修改成功!");
}
@ -170,27 +148,72 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
result.put("data", null);
return Result.error("失败", result);
}
List<OpenApiHeader> headers = openApiHeaderService.findByApiId(openApi.getId());
HttpHeaders httpHeaders = new HttpHeaders();
if (StrUtil.isNotEmpty(openApi.getHeadersJson())) {
List<OpenApiHeader> headers = JSON.parseArray(openApi.getHeadersJson(),OpenApiHeader.class);
if (headers.size()>0) {
for (OpenApiHeader header : headers) {
httpHeaders.put(header.getHeaderKey(), Lists.newArrayList(request.getHeader(header.getHeaderKey())));
}
}
}
String url = openApi.getOriginUrl();
String method = openApi.getRequestMethod();
HttpHeaders httpHeaders = new HttpHeaders();
for (OpenApiHeader header : headers) {
httpHeaders.put(header.getHeaderKey(), Lists.newArrayList(request.getHeader(header.getHeaderKey())));
}
String appkey = request.getHeader("appkey");
OpenApiAuth openApiAuth = openApiAuthService.getByAppkey(appkey);
SysUser systemUser = sysUserService.getById(openApiAuth.getSystemUserId());
String token = JwtUtil.sign(systemUser.getUsername(), systemUser.getPassword());
SysUser systemUser = sysUserService.getUserByName(openApiAuth.getCreateBy());
String token = this.getToken(systemUser.getUsername(), systemUser.getPassword());
httpHeaders.put("X-Access-Token", Lists.newArrayList(token));
httpHeaders.put("Content-Type",Lists.newArrayList("application/json"));
HttpEntity<String> httpEntity = new HttpEntity<>(json, httpHeaders);
url = RestUtil.getBaseUrl() + url;
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (HttpMethod.GET.matches(method)
|| HttpMethod.DELETE.matches(method)
|| HttpMethod.OPTIONS.matches(method)
|| HttpMethod.TRACE.matches(method)) {
//拼接参数
if (!request.getParameterMap().isEmpty()) {
if (StrUtil.isNotEmpty(openApi.getParamsJson())) {
List<OpenApiParam> params = JSON.parseArray(openApi.getParamsJson(),OpenApiParam.class);
if (params.size()>0) {
Map<String, OpenApiParam> openApiParamMap = params.stream().collect(Collectors.toMap(p -> p.getParamKey(), p -> p, (e, r) -> e));
request.getParameterMap().forEach((k, v) -> {
OpenApiParam openApiParam = openApiParamMap.get(k);
if (Objects.nonNull(openApiParam)) {
if(v==null&&StrUtil.isNotEmpty(openApiParam.getDefaultValue())){
builder.queryParam(openApiParam.getParamKey(), openApiParam.getDefaultValue());
}
if (v!=null){
builder.queryParam(openApiParam.getParamKey(), v);
}
}
});
}
}
return restTemplate.exchange(url, HttpMethod.resolve(method), httpEntity, Result.class, request.getParameterMap()).getBody();
}
}
URI targetUrl = builder.build().encode().toUri();
return restTemplate.exchange(targetUrl.toString(), Objects.requireNonNull(HttpMethod.resolve(method)), httpEntity, Result.class, request.getParameterMap()).getBody();
}
/**
* 生成接口访问令牌 Token
*
* @param USERNAME
* @param PASSWORD
* @return
*/
private String getToken(String USERNAME, String PASSWORD) {
String token = JwtUtil.sign(USERNAME, PASSWORD);
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, 60);
return token;
}
@GetMapping("/json")
public SwaggerModel swaggerModel() {
@ -230,13 +253,16 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
definition.setType("object");
Map<String, SwaggerDefinitionProperties> definitionProperties = new HashMap<>();
definition.setProperties(definitionProperties);
JSONObject jsonObject = JSONObject.parseObject(openApi.getBody());
for (Map.Entry<String, Object> properties : jsonObject.entrySet()) {
SwaggerDefinitionProperties swaggerDefinitionProperties = new SwaggerDefinitionProperties();
swaggerDefinitionProperties.setType("string");
swaggerDefinitionProperties.setDescription(properties.getValue()+"");
definitionProperties.put(properties.getKey(), swaggerDefinitionProperties);
if (openApi.getBody()!=null){
JSONObject jsonObject = JSONObject.parseObject(openApi.getBody());
if (jsonObject.size()>0){
for (Map.Entry<String, Object> properties : jsonObject.entrySet()) {
SwaggerDefinitionProperties swaggerDefinitionProperties = new SwaggerDefinitionProperties();
swaggerDefinitionProperties.setType("string");
swaggerDefinitionProperties.setDescription(properties.getValue()+"");
definitionProperties.put(properties.getKey(), swaggerDefinitionProperties);
}
}
}
// body的definition构建完成
definitions.put(openApi.getRequestUrl()+"Using"+openApi.getRequestMethod()+"body", definition);
@ -327,25 +353,28 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
private void parameters(SwaggerOperation operation, OpenApi openApi) {
List<SwaggerOperationParameter> parameters = new ArrayList<>();
for (OpenApiParam openApiParam : openApiParamService.findByApiId(openApi.getId())) {
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
parameter.setIn("path");
parameter.setName(openApiParam.getParamKey());
parameter.setRequired(openApiParam.getRequired() == 1);
parameter.setDescription(openApiParam.getNote());
parameters.add(parameter);
if (openApi.getParamsJson()!=null) {
List<OpenApiParam> openApiParams = JSON.parseArray(openApi.getParamsJson(), OpenApiParam.class);
for (OpenApiParam openApiParam : openApiParams) {
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
parameter.setIn("path");
parameter.setName(openApiParam.getParamKey());
parameter.setRequired(openApiParam.getRequired() == 1);
parameter.setDescription(openApiParam.getNote());
parameters.add(parameter);
}
}
for (OpenApiHeader openApiHeader : openApiHeaderService.findByApiId(openApi.getId())) {
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
parameter.setIn("header");
parameter.setName(openApiHeader.getHeaderKey());
parameter.setRequired(openApiHeader.getRequired() == 1);
parameter.setDescription(openApiHeader.getNote());
parameters.add(parameter);
if (openApi.getHeadersJson()!=null) {
List<OpenApiHeader> openApiHeaders = JSON.parseArray(openApi.getHeadersJson(), OpenApiHeader.class);
for (OpenApiHeader openApiHeader : openApiHeaders) {
SwaggerOperationParameter parameter = new SwaggerOperationParameter();
parameter.setIn("header");
parameter.setName(openApiHeader.getHeaderKey());
parameter.setRequired(openApiHeader.getRequired() == 1);
parameter.setDescription(openApiHeader.getNote());
parameters.add(parameter);
}
}
operation.setParameters(parameters);
}
@ -353,7 +382,7 @@ public class OpenApiController extends JeecgController<OpenApi, OpenApiService>
SwaggerInfo info = new SwaggerInfo();
info.setDescription("OpenAPI 接口列表");
info.setVersion("3.7.1");
info.setVersion("3.8.0");
info.setTitle("OpenAPI 接口列表");
info.setTermsOfService("https://jeecg.com");

View File

@ -6,8 +6,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.openapi.entity.OpenApiRecord;
import org.jeecg.modules.openapi.service.OpenApiRecordService;
import org.jeecg.modules.openapi.entity.OpenApiLog;
import org.jeecg.modules.openapi.service.OpenApiLogService;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
@ -18,47 +18,47 @@ import java.util.Arrays;
*/
@RestController
@RequestMapping("/openapi/record")
public class OpenApiRecordController extends JeecgController<OpenApiRecord, OpenApiRecordService> {
public class OpenApiLogController extends JeecgController<OpenApiLog, OpenApiLogService> {
/**
* 分页列表查询
*
* @param openApiRecord
* @param OpenApiLog
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@GetMapping(value = "/list")
public Result<?> queryPageList(OpenApiRecord openApiRecord, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
public Result<?> queryPageList(OpenApiLog OpenApiLog, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
QueryWrapper<OpenApiRecord> queryWrapper = QueryGenerator.initQueryWrapper(openApiRecord, req.getParameterMap());
Page<OpenApiRecord> page = new Page<>(pageNo, pageSize);
IPage<OpenApiRecord> pageList = service.page(page, queryWrapper);
QueryWrapper<OpenApiLog> queryWrapper = QueryGenerator.initQueryWrapper(OpenApiLog, req.getParameterMap());
Page<OpenApiLog> page = new Page<>(pageNo, pageSize);
IPage<OpenApiLog> pageList = service.page(page, queryWrapper);
return Result.ok(pageList);
}
/**
* 添加
*
* @param openApiRecord
* @param OpenApiLog
* @return
*/
@PostMapping(value = "/add")
public Result<?> add(@RequestBody OpenApiRecord openApiRecord) {
service.save(openApiRecord);
public Result<?> add(@RequestBody OpenApiLog OpenApiLog) {
service.save(OpenApiLog);
return Result.ok("添加成功!");
}
/**
* 编辑
*
* @param openApiRecord
* @param OpenApiLog
* @return
*/
@PutMapping(value = "/edit")
public Result<?> edit(@RequestBody OpenApiRecord openApiRecord) {
service.updateById(openApiRecord);
public Result<?> edit(@RequestBody OpenApiLog OpenApiLog) {
service.updateById(OpenApiLog);
return Result.ok("修改成功!");
}
@ -96,7 +96,7 @@ public class OpenApiRecordController extends JeecgController<OpenApiRecord, Open
*/
@GetMapping(value = "/queryById")
public Result<?> queryById(@RequestParam(name = "id", required = true) String id) {
OpenApiRecord openApiRecord = service.getById(id);
return Result.ok(openApiRecord);
OpenApiLog OpenApiLog = service.getById(id);
return Result.ok(OpenApiLog);
}
}

View File

@ -4,10 +4,7 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/openapi/permission")
@ -15,6 +12,11 @@ public class OpenApiPermissionController extends JeecgController<OpenApiPermissi
@PostMapping("add")
public Result add(@RequestBody OpenApiPermission openApiPermission) {
return Result.ok(service.save(openApiPermission));
service.add(openApiPermission);
return Result.ok("保存成功");
}
@GetMapping("/getOpenApi")
public Result<?> getOpenApi( String apiAuthId) {
return service.getOpenApi(apiAuthId);
}
}

View File

@ -47,18 +47,15 @@ public class OpenApi implements Serializable {
* IP 黑名单
*/
private String blackList;
/**
* 请求头列表
* 请求头json
*/
@TableField(exist = false)
private List<OpenApiHeader> headers;
private String headersJson;
/**
* 请求参数列表
* 请求参数json
*/
@TableField(exist = false)
private List<OpenApiParam> params;
private String paramsJson;
/**
* 目前仅支持json
@ -100,4 +97,9 @@ public class OpenApi implements Serializable {
* 更新时间
*/
private Date updateTime;
/**
* 历史已选接口
*/
@TableField(exist = false)
private String ifCheckBox = "0";
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import java.io.Serializable;
import java.util.Date;
@ -44,6 +45,7 @@ public class OpenApiAuth implements Serializable {
/**
* 系统用户ID
*/
@Dict(dictTable = "sys_user",dicCode = "id",dicText = "username")
private String systemUserId;
/**

View File

@ -18,16 +18,6 @@ import java.io.Serializable;
public class OpenApiHeader implements Serializable {
private static final long serialVersionUID = 5032708503120184683L;
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**
* 接口ID
*/
private String apiId;
/**
* key

View File

@ -16,7 +16,7 @@ import java.util.Date;
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class OpenApiRecord implements Serializable {
public class OpenApiLog implements Serializable {
private static final long serialVersionUID = -5870384488947863579L;
/**

View File

@ -17,16 +17,6 @@ import java.io.Serializable;
@Accessors(chain = true)
public class OpenApiParam implements Serializable {
private static final long serialVersionUID = -6174831468578022357L;
/**
* id
*/
@TableId(type = IdType.ASSIGN_ID)
private String id;
/**
* 接口ID
*/
private String apiId;
/**
* key

View File

@ -4,11 +4,11 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiAuth;
import org.jeecg.modules.openapi.entity.OpenApiLog;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import org.jeecg.modules.openapi.entity.OpenApiRecord;
import org.jeecg.modules.openapi.service.OpenApiAuthService;
import org.jeecg.modules.openapi.service.OpenApiLogService;
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
import org.jeecg.modules.openapi.service.OpenApiRecordService;
import org.jeecg.modules.openapi.service.OpenApiService;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext;
@ -27,7 +27,7 @@ import java.util.List;
@Slf4j
public class ApiAuthFilter implements Filter {
private OpenApiRecordService openApiRecordService;
private OpenApiLogService openApiLogService;
private OpenApiAuthService openApiAuthService;
private OpenApiPermissionService openApiPermissionService;
private OpenApiService openApiService;
@ -61,13 +61,13 @@ public class ApiAuthFilter implements Filter {
filterChain.doFilter(servletRequest, servletResponse);
long endTime = System.currentTimeMillis();
OpenApiRecord record = new OpenApiRecord();
record.setApiId(openApi.getId());
record.setCallAuthId(openApiAuth.getId());
record.setCallTime(callTime);
record.setUsedTime(endTime - startTime);
record.setResponseTime(new Date());
openApiRecordService.save(record);
OpenApiLog openApiLog = new OpenApiLog();
openApiLog.setApiId(openApi.getId());
openApiLog.setCallAuthId(openApiAuth.getId());
openApiLog.setCallTime(callTime);
openApiLog.setUsedTime(endTime - startTime);
openApiLog.setResponseTime(new Date());
openApiLogService.save(openApiLog);
}
@Override
@ -75,7 +75,7 @@ public class ApiAuthFilter implements Filter {
ServletContext servletContext = filterConfig.getServletContext();
WebApplicationContext applicationContext = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
this.openApiService = applicationContext.getBean(OpenApiService.class);
this.openApiRecordService = applicationContext.getBean(OpenApiRecordService.class);
this.openApiLogService = applicationContext.getBean(OpenApiLogService.class);
this.openApiAuthService = applicationContext.getBean(OpenApiAuthService.class);
this.openApiPermissionService = applicationContext.getBean(OpenApiPermissionService.class);
}

View File

@ -1,12 +0,0 @@
package org.jeecg.modules.openapi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.openapi.entity.OpenApiHeader;
/**
* @date 2024/12/10 14:47
*/
@Mapper
public interface OpenApiHeaderMapper extends BaseMapper<OpenApiHeader> {
}

View File

@ -2,11 +2,11 @@ package org.jeecg.modules.openapi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.openapi.entity.OpenApiRecord;
import org.jeecg.modules.openapi.entity.OpenApiLog;
/**
* @date 2024/12/10 9:50
*/
@Mapper
public interface OpenApiRecordMapper extends BaseMapper<OpenApiRecord> {
public interface OpenApiLogMapper extends BaseMapper<OpenApiLog> {
}

View File

@ -1,12 +0,0 @@
package org.jeecg.modules.openapi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.openapi.entity.OpenApiParam;
/**
* @date 2024/12/10 14:48
*/
@Mapper
public interface OpenApiParamMapper extends BaseMapper<OpenApiParam> {
}

View File

@ -1,16 +0,0 @@
package org.jeecg.modules.openapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.openapi.entity.OpenApiHeader;
import java.util.List;
/**
* @date 2024/12/10 14:48
*/
public interface OpenApiHeaderService extends IService<OpenApiHeader> {
boolean deleteByApiId(String apiId);
List<OpenApiHeader> findByApiId(String apiId);
}

View File

@ -1,10 +1,10 @@
package org.jeecg.modules.openapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.openapi.entity.OpenApiRecord;
import org.jeecg.modules.openapi.entity.OpenApiLog;
/**
* @date 2024/12/10 9:51
*/
public interface OpenApiRecordService extends IService<OpenApiRecord> {
public interface OpenApiLogService extends IService<OpenApiLog> {
}

View File

@ -1,15 +0,0 @@
package org.jeecg.modules.openapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.openapi.entity.OpenApiParam;
import java.util.List;
/**
* @date 2024/12/10 14:49
*/
public interface OpenApiParamService extends IService<OpenApiParam> {
boolean deleteByApiId(String apiId);
List<OpenApiParam> findByApiId(String apiId);
}

View File

@ -1,6 +1,8 @@
package org.jeecg.modules.openapi.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import java.util.List;
@ -10,4 +12,8 @@ import java.util.List;
*/
public interface OpenApiPermissionService extends IService<OpenApiPermission> {
List<OpenApiPermission> findByAuthId(String authId);
Result<?> getOpenApi(String apiAuthId);
void add(OpenApiPermission openApiPermission);
}

View File

@ -1,26 +0,0 @@
package org.jeecg.modules.openapi.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.openapi.entity.OpenApiHeader;
import org.jeecg.modules.openapi.mapper.OpenApiHeaderMapper;
import org.jeecg.modules.openapi.service.OpenApiHeaderService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @date 2024/12/10 14:49
*/
@Service
public class OpenApiHeaderServiceImpl extends ServiceImpl<OpenApiHeaderMapper, OpenApiHeader> implements OpenApiHeaderService {
@Override
public boolean deleteByApiId(String apiId) {
return baseMapper.delete(Wrappers.lambdaUpdate(OpenApiHeader.class).eq(OpenApiHeader::getApiId, apiId)) > 0;
}
@Override
public List<OpenApiHeader> findByApiId(String apiId) {
return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiHeader.class).eq(OpenApiHeader::getApiId, apiId));
}
}

View File

@ -0,0 +1,14 @@
package org.jeecg.modules.openapi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.openapi.entity.OpenApiLog;
import org.jeecg.modules.openapi.mapper.OpenApiLogMapper;
import org.jeecg.modules.openapi.service.OpenApiLogService;
import org.springframework.stereotype.Service;
/**
* @date 2024/12/10 9:53
*/
@Service
public class OpenApiLogServiceImpl extends ServiceImpl<OpenApiLogMapper, OpenApiLog> implements OpenApiLogService {
}

View File

@ -1,27 +0,0 @@
package org.jeecg.modules.openapi.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.openapi.entity.OpenApiParam;
import org.jeecg.modules.openapi.mapper.OpenApiParamMapper;
import org.jeecg.modules.openapi.service.OpenApiParamService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @date 2024/12/10 14:50
*/
@Service
public class OpenApiParamServiceImpl extends ServiceImpl<OpenApiParamMapper, OpenApiParam> implements OpenApiParamService {
@Override
public boolean deleteByApiId(String apiId) {
return baseMapper.delete(Wrappers.lambdaUpdate(OpenApiParam.class).eq(OpenApiParam::getApiId, apiId)) > 0;
}
@Override
public List<OpenApiParam> findByApiId(String apiId) {
return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiParam.class).eq(OpenApiParam::getApiId, apiId));
}
}

View File

@ -1,22 +1,67 @@
package org.jeecg.modules.openapi.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.openapi.entity.OpenApi;
import org.jeecg.modules.openapi.entity.OpenApiPermission;
import org.jeecg.modules.openapi.mapper.OpenApiPermissionMapper;
import org.jeecg.modules.openapi.service.OpenApiPermissionService;
import org.jeecg.modules.openapi.service.OpenApiService;
import org.springframework.stereotype.Service;
import java.util.Collections;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @date 2024/12/19 17:44
*/
@Service
public class OpenApiPermissionServiceImpl extends ServiceImpl<OpenApiPermissionMapper, OpenApiPermission> implements OpenApiPermissionService {
@Resource
private OpenApiService openApiService;
@Override
public List<OpenApiPermission> findByAuthId(String authId) {
return baseMapper.selectList(Wrappers.lambdaQuery(OpenApiPermission.class).eq(OpenApiPermission::getApiAuthId, authId));
}
@Override
public Result<?> getOpenApi(String apiAuthId) {
List<OpenApi> openApis = openApiService.list();
if (CollectionUtil.isEmpty(openApis)) {
return Result.error("接口不存在");
}
List<OpenApiPermission> openApiPermissions = baseMapper.selectList(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, apiAuthId));
if (CollectionUtil.isNotEmpty(openApiPermissions)) {
Map<String, OpenApi> openApiMap = openApis.stream().collect(Collectors.toMap(OpenApi::getId, o -> o));
for (OpenApiPermission openApiPermission : openApiPermissions) {
OpenApi openApi = openApiMap.get(openApiPermission.getApiId());
if (openApi!=null) {
openApi.setIfCheckBox("1");
}
}
}
return Result.ok(openApis);
}
@Override
public void add(OpenApiPermission openApiPermission) {
this.remove(Wrappers.<OpenApiPermission>lambdaQuery().eq(OpenApiPermission::getApiAuthId, openApiPermission.getApiAuthId()));
List<String> list = Arrays.asList(openApiPermission.getApiId().split(","));
if (CollectionUtil.isNotEmpty(list)) {
list.forEach(l->{
if (StrUtil.isNotEmpty(l)){
OpenApiPermission saveApiPermission = new OpenApiPermission();
saveApiPermission.setApiId(l);
saveApiPermission.setApiAuthId(openApiPermission.getApiAuthId());
this.save(saveApiPermission);
}
});
}
}
}

View File

@ -1,14 +0,0 @@
package org.jeecg.modules.openapi.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.openapi.entity.OpenApiRecord;
import org.jeecg.modules.openapi.mapper.OpenApiRecordMapper;
import org.jeecg.modules.openapi.service.OpenApiRecordService;
import org.springframework.stereotype.Service;
/**
* @date 2024/12/10 9:53
*/
@Service
public class OpenApiRecordServiceImpl extends ServiceImpl<OpenApiRecordMapper, OpenApiRecord> implements OpenApiRecordService {
}

View File

@ -32,7 +32,10 @@ import org.jeecg.common.constant.enums.MessageTypeEnum;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.*;
import org.jeecg.common.util.PasswordUtil;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.JeecgBaseConfig;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.modules.system.entity.*;
@ -741,6 +744,7 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
}
sysDepart.setDepartName(department.getName());
sysDepart.setDepartOrder(department.getOrder());
sysDepart.setDingIdentifier(department.getSource_identifier());
return sysDepart;
}

View File

@ -0,0 +1,49 @@
import { render } from '@/common/renderUtils';
//列表数据
export const columns = [
<#list columns as po>
<#if po.isShowList =='Y' && po.fieldName !='id' && po.fieldName !='delFlag'>
{
title: '${po.filedComment}',
align:"center",
<#if po.sort=='Y'>
sorter: true,
</#if>
<#if po.classType=='date'>
dataIndex: '${po.fieldName}',
<#elseif po.fieldDbType=='Blob'>
dataIndex: '${po.fieldName}String'
<#elseif po.classType=='umeditor'>
dataIndex: '${po.fieldName}',
<#elseif po.classType=='pca'>
dataIndex: '${po.fieldName}',
<#elseif po.classType=='file'>
dataIndex: '${po.fieldName}',
<#elseif po.classType=='image'>
dataIndex: '${po.fieldName}',
customRender:render.renderImage,
<#elseif po.classType=='switch'>
dataIndex: '${po.fieldName}',
<#assign switch_extend_arr=['Y','N']>
<#if po.dictField?default("")?contains("[")>
<#assign switch_extend_arr=po.dictField?eval>
</#if>
<#list switch_extend_arr as a>
<#if a_index == 0>
<#assign switch_extend_arr1=a>
<#else>
<#assign switch_extend_arr2=a>
</#if>
</#list>
customRender:({text}) => {
return render.renderSwitch(text, [{text:'是',value:'${switch_extend_arr1}'},{text:'否',value:'${switch_extend_arr2}'}])
},
<#elseif po.classType == 'sel_tree' || po.classType=='list' || po.classType=='list_multi' || po.classType=='sel_search' || po.classType=='radio' || po.classType=='checkbox' || po.classType=='sel_depart' || po.classType=='sel_user' || po.classType=='popup_dict'>
dataIndex: '${po.fieldName}_dictText'
<#else>
dataIndex: '${po.fieldName}'
</#if>
},
</#if>
</#list>
];

View File

@ -0,0 +1,512 @@
<#include "/common/utils.ftl">
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '${tableVo.ftlDescription}',
},
}
</route>
<template>
<PageLayout :navTitle="navTitle" :backRouteName="backRouteName">
<scroll-view class="scrollArea" scroll-y>
<view class="form-container">
<wd-form ref="form" :model="myFormData">
<wd-cell-group border>
<#list columns as po><#rt/>
<#assign form_field_dictCode="">
<#if po.dictTable?default("")?trim?length gt 1 && po.dictText?default("")?trim?length gt 1 && po.dictField?default("")?trim?length gt 1>
<#assign form_field_dictCode="${po.dictTable},${po.dictText},${po.dictField}">
<#elseif po.dictField?default("")?trim?length gt 1>
<#assign form_field_dictCode="${po.dictField}">
</#if>
<view class="{ 'mt-14px': ${po_index % 2} == 0 }">
<#if po.fieldName !='id' && po.isShow =='Y' && po.fieldName !='delFlag'><#rt/>
<#if po.classType =='image'>
<!-- 图片 -->
<wd-cell
:title="get4Label('${po.filedComment}')"
title-width="100px"
<#if po.nullable == 'N'>
:required="true"
</#if>
>
<online-image
v-model:value="myFormData[${autoStringSuffix(po)}]"
name=${autoStringSuffix(po)}
<#if po.uploadnum??>
:maxNum="${po.uploadnum}"
</#if>
/>
</wd-cell>
<#elseif po.classType =='file'>
<wd-cell
:title="get4Label('${po.filedComment}')"
title-width="100px"
<#if po.nullable == 'N'>
:required="true"
</#if>
>
<!-- #ifndef APP-PLUS -->
<online-file
v-model:value="myFormData[${autoStringSuffix(po)}]"
name=${autoStringSuffix(po)}
></online-file>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<online-file-custom
v-model:value="myFormData[${autoStringSuffix(po)}]"
name=${autoStringSuffix(po)}
></online-file-custom>
<!-- #endif -->
</wd-cell>
<#elseif po.classType =='datetime' || po.classType =='time'>
<DateTime
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
<#if po.classType =='datetime'>
format="YYYY-MM-DD HH:mm:ss"
<#else>
format="HH:mm:ss"
</#if>
name=${autoStringSuffix(po)}
v-model="myFormData[${autoStringSuffix(po)}]"
></DateTime>
<#elseif po.classType =='date'>
<online-date
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
<#if po.extendParams?exists && po.extendParams.picker?exists>
:type="getDateExtendType('${po.extendParams.picker}')"
<#else>
type="${po.classType}"
</#if>
name=${autoStringSuffix(po)}
v-model:value="myFormData[${autoStringSuffix(po)}]"
></online-date>
<#elseif po.classType =='switch'>
<#assign switch_extend_arr=['Y','N']>
<#if po.dictField?default("")?contains("[")>
<#assign switch_extend_arr=po.dictField?eval>
</#if>
<!-- 开关 -->
<wd-cell
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
title-width="100px"
center
>
<wd-switch
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
size="18px"
v-model="myFormData[${autoStringSuffix(po)}]"
active-value="${switch_extend_arr[0]}"
inactive-value="${switch_extend_arr[1]}"
/>
</wd-cell>
<#elseif po.classType =='list' || po.classType =='sel_search'>
<online-select
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
name=${autoStringSuffix(po)}
dict="${form_field_dictCode}"
v-model="myFormData[${autoStringSuffix(po)}]"
></online-select>
<#elseif po.classType =='checkbox'>
<online-checkbox
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
name=${autoStringSuffix(po)}
dict="${form_field_dictCode}"
v-model="myFormData[${autoStringSuffix(po)}]"
></online-checkbox>
<#elseif po.classType =='radio'>
<online-radio
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
name=${autoStringSuffix(po)}
dict="${form_field_dictCode}"
v-model="myFormData[${autoStringSuffix(po)}]"
></online-radio>
<#elseif po.classType =='list_multi'>
<online-multi
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
name=${autoStringSuffix(po)}
dict="${form_field_dictCode}"
v-model="myFormData[${autoStringSuffix(po)}]"
></online-multi>
<#elseif po.classType =='textarea' || po.classType =='markdown' || po.classType =='umeditor'>
<wd-textarea
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
type="${po.classType}"
name=${autoStringSuffix(po)}
prop=${autoStringSuffix(po)}
clearable
:maxlength="300"
v-model="myFormData[${autoStringSuffix(po)}]"
></wd-textarea>
<#elseif po.classType =='password'>
<wd-input
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
show-password
name=${autoStringSuffix(po)}
prop=${autoStringSuffix(po)}
clearable
v-model="myFormData[${autoStringSuffix(po)}]"
></wd-input>
<#elseif po.classType =='popup_dict'>
<PopupDict
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
v-model="myFormData[${autoStringSuffix(po)}]"
:multi="${po.extendParams.popupMulti?c}"
dictCode="${po.dictTable},${po.dictText},${po.dictField}"
></PopupDict>
<#elseif po.classType =='popup'>
<#assign sourceFields = po.dictField?default("")?trim?split(",")/>
<#assign targetFields = po.dictText?default("")?trim?split(",")/>
<Popup
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
v-model="myFormData[${autoStringSuffix(po)}]"
:multi="${po.extendParams.popupMulti?c}"
code="${po.dictTable}"
:setFieldsValue="setFieldsValue"
:fieldConfig="[
<#list sourceFields as fieldName>
{ source: '${fieldName}', target: '${dashedToCamel(targetFields[fieldName_index])}' },
</#list>
]"
></Popup>
<#elseif po.classType =='link_table'>
<online-popup-link-record
:label="get4Label('${po.filedComment}')"
labelWidth="100px"
name=${autoStringSuffix(po)}
:formSchema="getFormSchema('${po.dictTable}','${po.dictField}','${po.dictText}')"
v-model:value="myFormData[${autoStringSuffix(po)}]"
></online-popup-link-record>
<#elseif po.classType =='sel_user'>
<select-user
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
v-model="myFormData[${autoStringSuffix(po)}]"
<#if po.extendParams?exists && po.extendParams.text?exists>labelKey="${po.extendParams.text}"</#if>
<#if po.extendParams?exists && po.extendParams.store?exists>rowKey="${po.extendParams.store}"</#if>
></select-user>
<#elseif po.classType =='sel_depart'>
<select-dept
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
v-model="myFormData[${autoStringSuffix(po)}]"
<#if po.extendParams?exists && po.extendParams.text?exists>labelKey="${po.extendParams.text}"</#if>
<#if po.extendParams?exists && po.extendParams.store?exists>rowKey="${po.extendParams.store}"</#if>
:multiple="${po.extendParams.multi?default('true')}"
></select-dept>
<#elseif po.classType =='cat_tree'>
<CategorySelect
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
v-model="myFormData[${autoStringSuffix(po)}]"
pcode="${po.dictField?default("")}"
></CategorySelect>
<#elseif po.classType =='sel_tree'>
<TreeSelect
labelWidth="100px"
:label="get4Label('${po.filedComment}')"
v-model="myFormData[${autoStringSuffix(po)}]"
<#if po.dictText??>
<#if po.dictText?split(',')[2]?? && po.dictText?split(',')[0]??>
dict="${po.dictTable},${po.dictText?split(',')[2]},${po.dictText?split(',')[0]}"
<#elseif po.dictText?split(',')[1]??>
pidField="${po.dictText?split(',')[1]}"
<#elseif po.dictText?split(',')[3]??>
hasChildField="${po.dictText?split(',')[3]}"
</#if>
</#if>
:pidValue="`${po.dictField}`"
></TreeSelect>
<#elseif po.fieldDbType=='int' || po.fieldDbType=='double' || po.fieldDbType=='BigDecimal'>
<wd-input
label-width="100px"
v-model="myFormData[${autoStringSuffix(po)}]"
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
prop=${autoStringSuffix(po)}
placeholder="请选择${po.filedComment}"
inputMode="numeric"
:rules="[
<#if po.fieldName != 'id'>
<#assign fieldValidType = po.fieldValidType!''>
<#-- 非空校验 -->
<#if po.nullable == 'N' || fieldValidType == '*'>
{ required: true, message: '请输入${po.filedComment}!'},
<#elseif fieldValidType!=''>
{ required: false},
</#if>
<#-- 6到16位数字 -->
<#if fieldValidType == 'n6-16'>
{ pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'},
<#-- 6到16位任意字符 -->
<#elseif fieldValidType == '*6-16'>
{ pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'},
<#-- 6到18位字母 -->
<#elseif fieldValidType == 's6-18'>
{ pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'},
<#-- 网址 -->
<#elseif fieldValidType == 'url'>
{ pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'},
<#-- 电子邮件 -->
<#elseif fieldValidType == 'e'>
{ pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'},
<#-- 手机号码 -->
<#elseif fieldValidType == 'm'>
{ pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'},
<#-- 邮政编码 -->
<#elseif fieldValidType == 'p'>
{ pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'},
<#-- 字母 -->
<#elseif fieldValidType == 's'>
{ pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'},
<#-- 数字 -->
<#elseif fieldValidType == 'n'>
{ pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'},
<#-- 整数 -->
<#elseif fieldValidType == 'z'>
{ pattern: /^-?\d+$/, message: '请输入整数!'},
<#-- 金额 -->
<#elseif fieldValidType == 'money'>
{ pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'},
<#-- 正则校验 -->
<#elseif fieldValidType != '' && fieldValidType != '*'>
{ pattern: '${fieldValidType}', message: '不符合校验规则!'},
<#-- 无校验 -->
<#else>
<#t>
</#if>
</#if>
]"
clearable
/>
<#else>
<wd-input
label-width="100px"
v-model="myFormData[${autoStringSuffix(po)}]"
:label="get4Label('${po.filedComment}')"
name=${autoStringSuffix(po)}
prop=${autoStringSuffix(po)}
placeholder="请选择${po.filedComment}"
:rules="[
<#if po.fieldName != 'id'>
<#assign fieldValidType = po.fieldValidType!''>
<#-- 非空校验 -->
<#if po.nullable == 'N' || fieldValidType == '*'>
{ required: true, message: '请输入${po.filedComment}!'},
<#elseif fieldValidType!=''>
{ required: false},
</#if>
<#-- 6到16位数字 -->
<#if fieldValidType == 'n6-16'>
{ pattern: /^\d{6,16}$|^(?=\d+\.\d+)[\d.]{7,17}$/, message: '请输入6到16位数字!'},
<#-- 6到16位任意字符 -->
<#elseif fieldValidType == '*6-16'>
{ pattern: /^.{6,16}$/, message: '请输入6到16位任意字符!'},
<#-- 6到18位字母 -->
<#elseif fieldValidType == 's6-18'>
{ pattern: /^[a-z|A-Z]{6,18}$/, message: '请输入6到18位字母!'},
<#-- 网址 -->
<#elseif fieldValidType == 'url'>
{ pattern: /^((ht|f)tps?):\/\/[\w\-]+(\.[\w\-]+)+([\w\-.,@?^=%&:\/~+#]*[\w\-@?^=%&\/~+#])?$/, message: '请输入正确的网址!'},
<#-- 电子邮件 -->
<#elseif fieldValidType == 'e'>
{ pattern: /^([\w]+\.*)([\w]+)@[\w]+\.\w{3}(\.\w{2}|)$/, message: '请输入正确的电子邮件!'},
<#-- 手机号码 -->
<#elseif fieldValidType == 'm'>
{ pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号码!'},
<#-- 邮政编码 -->
<#elseif fieldValidType == 'p'>
{ pattern: /^[0-9]\d{5}$/, message: '请输入正确的邮政编码!'},
<#-- 字母 -->
<#elseif fieldValidType == 's'>
{ pattern: /^[A-Z|a-z]+$/, message: '请输入字母!'},
<#-- 数字 -->
<#elseif fieldValidType == 'n'>
{ pattern: /^-?\d+\.?\d*$/, message: '请输入数字!'},
<#-- 整数 -->
<#elseif fieldValidType == 'z'>
{ pattern: /^-?\d+$/, message: '请输入整数!'},
<#-- 金额 -->
<#elseif fieldValidType == 'money'>
{ pattern: /^(([1-9][0-9]*)|([0]\.\d{0,2}|[1-9][0-9]*\.\d{0,2}))$/, message: '请输入正确的金额!'},
<#-- 正则校验 -->
<#elseif fieldValidType != '' && fieldValidType != '*'>
{ pattern: '${fieldValidType}', message: '不符合校验规则!'},
<#-- 无校验 -->
<#else>
<#t>
</#if>
</#if>
]"
clearable
/>
</#if>
</#if>
</view>
</#list>
</wd-cell-group>
</wd-form>
</view>
</scroll-view>
<view class="footer">
<wd-button :disabled="loading" block :loading="loading" @click="handleSubmit">提交</wd-button>
</view>
</PageLayout>
</template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { http } from '@/utils/http'
import { useToast } from 'wot-design-uni'
import { useRouter } from '@/plugin/uni-mini-router'
import { ref, onMounted, computed,reactive } from 'vue'
import OnlineImage from '@/components/online/view/online-image.vue'
import OnlineFile from '@/components/online/view/online-file.vue'
import OnlineFileCustom from '@/components/online/view/online-file-custom.vue'
import OnlineSelect from '@/components/online/view/online-select.vue'
import OnlineTime from '@/components/online/view/online-time.vue'
import OnlineDate from '@/components/online/view/online-date.vue'
import OnlineRadio from '@/components/online/view/online-radio.vue'
import OnlineCheckbox from '@/components/online/view/online-checkbox.vue'
import OnlineMulti from '@/components/online/view/online-multi.vue'
import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue'
import SelectDept from '@/components/SelectDept/SelectDept.vue'
import SelectUser from '@/components/SelectUser/SelectUser.vue'
defineOptions({
name: '${entityName}Form',
options: {
styleIsolation: 'shared',
},
})
const toast = useToast()
const router = useRouter()
const form = ref(null)
// 定义响应式数据
const myFormData = reactive({})
const loading = ref(false)
const navTitle = ref('新增')
const dataId = ref('')
const backRouteName = ref('${entityName}List')
// 定义 initForm 方法
const initForm = (item) => {
console.log('initForm item', item)
if(item?.dataId){
dataId.value = item.dataId;
navTitle.value = item.dataId?'编辑':'新增';
initData();
}
}
// 初始化数据
const initData = () => {
http.get("/${entityPackagePath}/${entityName?uncap_first}/queryById",{id:dataId.value}).then((res) => {
if (res.success) {
let obj = res.result
Object.assign(myFormData, { ...obj })
}else{
toast.error(res?.message || '表单加载失败!')
}
})
}
const handleSuccess = () => {
uni.$emit('refreshList');
router.back()
}
// 提交表单
const handleSubmit = () => {
let url = dataId.value?'/${entityPackagePath}/${entityName?uncap_first}/edit':'/${entityPackagePath}/${entityName?uncap_first}/add';
form.value
.validate()
.then(({ valid, errors }) => {
if (valid) {
loading.value = true;
http.post(url,myFormData).then((res) => {
loading.value = false;
if (res.success) {
toast.success('保存成功');
handleSuccess()
}else{
toast.error(res?.message || '表单保存失败!')
}
})
}
})
.catch((error) => {
console.log(error, 'error')
loading.value = false;
})
}
// 标题
const get4Label = computed(() => {
return (label) => {
return label && label.length > 4 ? label.substring(0, 4) : label;
}
})
// 标题
const getFormSchema = computed(() => {
return (dictTable,dictCode,dictText) => {
return {
dictCode,
dictTable,
dictText
};
}
})
/**
* 获取日期控件的扩展类型
* @param picker
* @returns {string}
*/
const getDateExtendType = (picker: string) => {
let mapField = {
month: 'year-month',
year: 'year',
quarter: 'quarter',
week: 'week',
day: 'date',
}
return picker && mapField[picker]
? mapField[picker]
: 'date'
}
//设置pop返回值
const setFieldsValue = (data) => {
Object.assign(myFormData, {...data })
}
// onLoad 生命周期钩子
onLoad((option) => {
initForm(option)
})
</script>
<style lang="scss" scoped>
.footer {
width: 100%;
padding: 10px 20px;
padding-bottom: calc(constant(safe-area-inset-bottom) + 10px);
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
}
</style>

View File

@ -0,0 +1,148 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '${tableVo.ftlDescription}',
navigationStyle: 'custom',
},
}
</route>
<template>
<PageLayout navTitle="${tableVo.ftlDescription}" backRouteName="index" routeMethod="pushTab">
<view class="wrap">
<z-paging
ref="paging"
:fixed="false"
v-model="dataList"
@query="queryList"
:default-page-size="15"
>
<template v-for="item in dataList" :key="item.id">
<wd-swipe-action>
<view class="list" @click="handleEdit(item)">
<template v-for="(cItem, cIndex) in columns" :key="cIndex">
<view v-if="cIndex < 3" class="box" :style="getBoxStyle">
<view class="field ellipsis">{{ cItem.title }}</view>
<view class="value cu-text-grey">{{ item[cItem.dataIndex] }}</view>
</view>
</template>
</view>
<template #right>
<view class="action">
<view class="button" @click="handleAction('del', item)">删除</view>
</view>
</template>
</wd-swipe-action>
</template>
</z-paging>
<view class="add u-iconfont u-icon-add" @click="handleAdd"></view>
</view>
</PageLayout>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { http } from '@/utils/http'
import usePageList from '@/hooks/usePageList'
import {columns} from './${entityName}Data';
defineOptions({
name: '${entityName}List',
options: {
styleIsolation: 'shared',
}
})
//分页加载配置
let { toast, router, paging, dataList, queryList } = usePageList('/${entityPackagePath}/${entityName?uncap_first}/list');
//样式
const getBoxStyle = computed(() => {
return { width: "calc(33% - 5px)" }
})
// 其他操作
const handleAction = (val, item) => {
if (val == 'del') {
http.delete("/${entityPackagePath}/${entityName?uncap_first}/delete?id="+item.id,{id:item.id}).then((res) => {
toast.success('删除成功~')
paging.value.reload()
})
}
}
// go 新增页
const handleAdd = () => {
router.push({
name: '${entityName}Form'
})
}
//go 编辑页
const handleEdit = (record) => {
router.push({
name: '${entityName}Form',
params: {dataId: record.id},
})
}
onMounted(() => {
// 监听刷新列表事件
uni.$on('refreshList', () => {
queryList(1,10)
})
})
</script>
<style lang="scss" scoped>
.wrap {
height: 100%;
}
:deep(.wd-swipe-action) {
margin-top: 10px;
background-color: #fff;
}
.list {
padding: 10px 10px;
width: 100%;
text-align: left;
display: flex;
justify-content: space-between;
.box {
width: 33%;
.field {
margin-bottom: 10px;
line-height: 20px;
}
}
}
.action {
width: 60px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.button {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100%;
color: #fff;
&:first-child {
background-color: #fa4350;
}
}
}
.add {
height: 70upx;
width: 70upx;
text-align: center;
line-height: 70upx;
background-color: #fff;
border-radius: 50%;
position: fixed;
bottom: 80upx;
right: 30upx;
box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1);
color: #666;
}
</style>

View File

@ -25,13 +25,6 @@
<version>${jeecgboot.version}</version>
</dependency>
<!-- AI大模型管理 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module-airag</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- flyway 数据库自动升级 -->
<dependency>
<groupId>org.flywaydb</groupId>

View File

@ -40,8 +40,8 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
String path = oConvertUtils.getString(env.getProperty("server.servlet.context-path"));
log.info("\n----------------------------------------------------------\n\t" +
"Application Jeecg-Boot is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:" + port + path + "/\n\t" +
"External: \thttp://" + ip + ":" + port + path + "/\n\t" +
"Local: \t\thttp://localhost:" + port + path + "/doc.html\n\t" +
"External: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" +
"Swagger文档: \thttp://" + ip + ":" + port + path + "/doc.html\n" +
"----------------------------------------------------------");

View File

@ -40,12 +40,12 @@ spring:
username: jeecgos@163.com
password: ??
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
mail.smtp.timeout: 10000 # 连接超时(毫秒)
mail.smtp.connectiontimeout: 10000 # 连接超时(毫秒)
mail.smtp.writetimeout: 10000 # 写入超时(毫秒)
mail.smtp.auth: true
smtp.ssl.enable: true
mail.debug: true # 启用调试模式(查看详细日志)
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc

View File

@ -38,12 +38,8 @@ spring:
username: jeecgos@163.com
password: ??
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
mail.smtp.auth: true
smtp.ssl.enable: true
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc

View File

@ -38,12 +38,8 @@ spring:
username: jeecgos@163.com
password: ??
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
mail.smtp.auth: true
smtp.ssl.enable: true
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc

View File

@ -36,12 +36,8 @@ spring:
username: jeecgos@163.com
password: ??
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
mail.smtp.auth: true
smtp.ssl.enable: true
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc

View File

@ -38,12 +38,9 @@ spring:
username: jeecgos@163.com
password: ??
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
mail.smtp.auth: true
smtp.ssl.enable: true
mail.debug: true # 启用调试模式(查看详细日志)
## quartz定时任务,采用数据库方式
quartz:
job-store-type: jdbc

View File

@ -0,0 +1,95 @@
package org.jeecg.modules.openapi.test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.*;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import java.security.MessageDigest;
public class SampleOpenApiTest {
private final String base_url = "http://localhost:8080/jeecg-boot";
private final String appKey = "ak-pFjyNHWRsJEFWlu6";
private final String searchKey = "4hV5dBrZtmGAtPdbA5yseaeKRYNpzGsS";
@Test
public void test() throws Exception {
// 根据部门ID查询用户
String url = base_url+"/openapi/call/TEwcXBlr?id=6d35e179cd814e3299bd588ea7daed3f";
JSONObject header = genTimestampAndSignature();
HttpGet httpGet = new HttpGet(url);
// 设置请求头
httpGet.setHeader("Content-Type", "application/json");
httpGet.setHeader("appkey",appKey);
httpGet.setHeader("signature",header.get("signature").toString());
httpGet.setHeader("timestamp",header.get("timestamp").toString());
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(httpGet);) {
// 获取响应状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("[debug] 响应状态码: " + statusCode);
HttpEntity entity = response.getEntity();
System.out.println(entity);
// 获取响应内容
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("[debug] 响应内容: " + responseBody);
// 解析JSON响应
JSONObject res = JSON.parseObject(responseBody);
//错误日志判断
if(res.containsKey("success")){
Boolean success = res.getBoolean("success");
if(success){
System.out.println("[info] 调用成功: " + res.toJSONString());
}else{
System.out.println("[error] 调用失败: " + res.getString("message"));
}
}else{
System.out.println("[error] 调用失败: " + res.getString("message"));
}
}
}
private JSONObject genTimestampAndSignature(){
JSONObject jsonObject = new JSONObject();
long timestamp = System.currentTimeMillis();
jsonObject.put("timestamp",timestamp);
jsonObject.put("signature", md5(appKey + searchKey + timestamp));
return jsonObject;
}
/**
* 生成md5
* @param sourceStr
* @return
*/
protected String md5(String sourceStr) {
String result = "";
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(sourceStr.getBytes("utf-8"));
byte[] hash = md.digest();
int i;
StringBuffer buf = new StringBuffer(32);
for (int offset = 0; offset < hash.length; offset++) {
i = hash[offset];
if (i < 0) {
i += 256;
}
if (i < 16) {
buf.append("0");
}
buf.append(Integer.toHexString(i));
}
result = buf.toString();
} catch (Exception e) {
throw new RuntimeException("sign签名错误", e);
}
return result;
}
}

View File

@ -1,12 +1,14 @@
package org.jeecg.handler.swagger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.swagger.web.*;
import java.util.ArrayList;
import java.util.List;
/** 已使用knife4j-gateway支持该功能
@ -19,6 +21,9 @@ import java.util.List;
public class SwaggerResourceController {
private MySwaggerResourceProvider swaggerResourceProvider;
@Value("${knife4j.gateway.enabled:true}")
private Boolean enableSwagger;
@Autowired
public SwaggerResourceController(MySwaggerResourceProvider swaggerResourceProvider) {
this.swaggerResourceProvider = swaggerResourceProvider;
@ -36,6 +41,9 @@ public class SwaggerResourceController {
@RequestMapping
public ResponseEntity<List<SwaggerResource>> swaggerResources() {
if (!enableSwagger) {
return new ResponseEntity<>(new ArrayList<>(), HttpStatus.OK);
}
return new ResponseEntity<>(swaggerResourceProvider.get(), HttpStatus.OK);
}
}

View File

@ -4,11 +4,6 @@ server:
knife4j:
gateway:
enabled: true
strategy: discover
discover:
excluded-services: ${spring.application.name}
enabled: true
version: OpenAPI3
spring:
application:

View File

@ -30,7 +30,7 @@
<properties>
<jeecgboot.version>3.8.0</jeecgboot.version>
<!-- JDK版本支持17和1.8 -->
<java.version>1.8</java.version>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 微服务 -->
@ -40,7 +40,7 @@
<seata.version>1.5.2</seata.version>
<xxl-job-core.version>2.4.1</xxl-job-core.version>
<fastjson.version>2.0.56</fastjson.version>
<fastjson.version>2.0.57</fastjson.version>
<aviator.version>5.2.6</aviator.version>
<pegdown.version>1.6.0</pegdown.version>
<commonmark.version>0.17.0</commonmark.version>
@ -56,7 +56,8 @@
<dm8.version>8.1.1.49</dm8.version>
<!-- 积木报表-->
<jimureport-spring-boot-starter.version>1.9.5</jimureport-spring-boot-starter.version>
<jimureport-spring-boot-starter.version>2.0.0</jimureport-spring-boot-starter.version>
<minidao.version>1.10.10</minidao.version>
<!-- 持久层 -->
<mybatis-plus.version>3.5.3.2</mybatis-plus.version>
<dynamic-datasource-spring-boot-starter.version>4.1.3</dynamic-datasource-spring-boot-starter.version>
@ -68,8 +69,8 @@
<aliyun.oss.version>3.11.2</aliyun.oss.version>
<!-- shiro -->
<shiro.version>1.13.0</shiro.version>
<java-jwt.version>4.5.0</java-jwt.version>
<shiro-redis.version>3.2.3</shiro-redis.version>
<java-jwt.version>4.5.0</java-jwt.version>
<codegenerate.version>1.4.9</codegenerate.version>
<autopoi-web.version>1.4.11</autopoi-web.version>
<minio.version>8.0.3</minio.version>
@ -254,7 +255,7 @@
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>hibernate-re</artifactId>
<version>3.8.0-GA</version>
<version>3.8.0.2</version>
</dependency>
<!--mongon db-->
@ -359,7 +360,7 @@
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>weixin4j</artifactId>
<version>2.0.2</version>
<version>2.0.4</version>
<exclusions>
<exclusion>
<artifactId>commons-beanutils</artifactId>
@ -383,6 +384,22 @@
</exclusion>
</exclusions>
</dependency>
<!-- minidao -->
<dependency>
<groupId>org.jeecgframework</groupId>
<artifactId>minidao-spring-boot-starter</artifactId>
<version>${minidao.version}</version>
<exclusions>
<exclusion>
<artifactId>druid</artifactId>
<groupId>com.alibaba</groupId>
</exclusion>
<exclusion>
<artifactId>jsqlparser</artifactId>
<groupId>com.github.jsqlparser</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
@ -418,7 +435,7 @@
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-nosql-starter</artifactId>
<version>${jimureport-spring-boot-starter.version}</version>
<version>1.9.5.2</version>
<exclusions>
<exclusion>
<groupId>org.apache.calcite</groupId>
@ -430,13 +447,13 @@
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>1.9.4</version>
<version>1.9.5</version>
</dependency>
<!-- AI集成 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
<version>3.8.0</version>
<version>3.8.0.2</version>
</dependency>
<!--flyway 支持 mysql5.7+、MariaDB10.3.16-->
<!--mysql5.6需要把版本号改成5.2.1-->

View File

@ -2,7 +2,6 @@ node_modules
.DS_Store
.github
dist
.npmrc
.cache
tests/server/static

2
jeecgboot-vue3/.npmrc Normal file
View File

@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

View File

@ -66,6 +66,7 @@
"resize-observer-polyfill": "^1.5.1",
"showdown": "^2.1.0",
"sortablejs": "^1.15.6",
"swagger-ui-dist": "^5.21.0",
"tinymce": "6.6.2",
"vditor": "^3.10.8",
"vue": "^3.5.13",
@ -77,8 +78,9 @@
"vue-router": "^4.5.0",
"vue-types": "^5.1.3",
"vuedraggable": "^4.1.0",
"vxe-table": "4.6.17",
"vxe-table-plugin-antd": "4.0.7",
"vxe-table": "4.13.31",
"vxe-table-plugin-antd": "4.0.8",
"vxe-pc-ui": "4.6.12",
"xe-utils": "3.5.26",
"xss": "^1.0.15"
},

View File

@ -1,6 +1,6 @@
<template>
<a-upload name="file" :showUploadList="false" :customRequest="(file) => onClick(file)">
<Button :type="type" :class="getButtonClass">
<Button :type="type" :class="getButtonClass" :disabled="props.disabled">
<template #default="data">
<Icon :icon="preIcon" v-if="preIcon" :size="iconSize" />
<slot v-bind="data || {}"></slot>

View File

@ -27,6 +27,7 @@ export { default as JDictSelectTag } from './src/jeecg/components/JDictSelectTag
export { default as JTreeSelect } from './src/jeecg/components/JTreeSelect.vue';
export { default as JSearchSelect } from './src/jeecg/components/JSearchSelect.vue';
export { default as JSelectUserByDept } from './src/jeecg/components/JSelectUserByDept.vue';
export { default as JSelectUserByDepartment } from './src/jeecg/components/JSelectUserByDepartment.vue';
export { default as JEditor } from './src/jeecg/components/JEditor.vue';
export { default as JImageUpload } from './src/jeecg/components/JImageUpload.vue';
// Jeecg自定义校验

View File

@ -64,6 +64,7 @@ import JInput from './jeecg/components/JInput.vue';
import JTreeSelect from './jeecg/components/JTreeSelect.vue';
import JEllipsis from './jeecg/components/JEllipsis.vue';
import JSelectUserByDept from './jeecg/components/JSelectUserByDept.vue';
import JSelectUserByDepartment from './jeecg/components/JSelectUserByDepartment.vue';
import JUpload from './jeecg/components/JUpload/JUpload.vue';
import JSearchSelect from './jeecg/components/JSearchSelect.vue';
import JAddInput from './jeecg/components/JAddInput.vue';
@ -159,6 +160,7 @@ componentMap.set('JInput', JInput);
componentMap.set('JTreeSelect', JTreeSelect);
componentMap.set('JEllipsis', JEllipsis);
componentMap.set('JSelectUserByDept', JSelectUserByDept);
componentMap.set('JSelectUserByDepartment', JSelectUserByDepartment);
componentMap.set('JUpload', JUpload);
componentMap.set('JSearchSelect', JSearchSelect);
componentMap.set('JAddInput', JAddInput);

View File

@ -1,7 +1,7 @@
<!--职务选择组件-->
<template>
<div class="JSelectPosition">
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="(changeValue) => $emit('update:value', changeValue)"></JSelectBiz>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<PositionSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></PositionSelectModal>
@ -51,7 +51,7 @@
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<object>({
let selectValues = reactive<any>({
value: [],
change: false,
});
@ -70,9 +70,23 @@
/**
* 监听组件值
*/
watchEffect(() => {
props.value && initValue();
});
// update-begin--author:liaozhiyang---date:20250423---for【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态需要点击第二次才生效。
watch(
() => props.value,
() => {
if (props.value) {
initValue();
} else {
// update-begin--author:liaozhiyang---date:20250604---for【issues/8233】resetFields时无法重置
if (selectValues.value?.length) {
selectValues.value = [];
}
// update-end--author:liaozhiyang---date:20250604---for【issues/8233】resetFields时无法重置
}
},
{ deep: true, immediate: true }
);
// update-end--author:liaozhiyang---date:20250423---for【pull/8014】插槽方式弹窗中取消该数据checkbox的选中状态需要点击第二次才生效。
/**
* 监听selectValues变化

View File

@ -0,0 +1,176 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" @change="handleSelectChange"></JSelectBiz>
<JSelectUserByDepartmentModal
v-if="modalShow"
:selectedUser="selectOptions"
:modalTitle="modalTitle"
:rowKey="rowKey"
:labelKey="labelKey"
:isRadioSelection="isRadioSelection"
:params="params"
@register="regModal"
@change="setValue"
@close="() => (modalShow = false)"
v-bind="attrs"
></JSelectUserByDepartmentModal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, provide } from 'vue';
import JSelectUserByDepartmentModal from './modal/JSelectUserByDepartmentModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { isArray, isString, isObject } from '/@/utils/is';
import { getTableList as getTableListOrigin } from '/@/api/common/api';
import { useMessage } from '/@/hooks/web/useMessage';
defineOptions({ name: 'JSelectUserByDepartment' });
const props = defineProps({
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
modalTitle: {
type: String,
default: '部门用户选择',
},
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
//查询参数
params: {
type: Object,
default: () => {},
},
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['options-change', 'change', 'update:value']);
const { createMessage } = useMessage();
//注册model
const [regModal, { openModal }] = useModal();
// 是否显示弹窗
const modalShow = ref(false);
//下拉框选项值
const selectOptions: any = ref<SelectValue>([]);
//下拉框选中值
let selectValues: any = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const attrs: any = useAttrs();
// 打开弹窗
function handleOpen() {
modalShow.value = true;
setTimeout(() => {
openModal(true, {
isUpdate: false,
});
}, 0);
}
const handleSelectChange = (data) => {
selectOptions.value = selectOptions.value.filter((item) => data.includes(item[props.rowKey]));
setValue(selectOptions.value);
};
// 设置下拉框的值
const setValue = (data) => {
selectOptions.value = data.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
selectValues.value = data.map((item) => item[props.rowKey]);
// 更新value
emit('update:value', selectValues.value);
// 触发change事件不转是因为basicForm提交时会自动将字符串转化为数组
emit('change', selectValues.value);
// 触发options-change事件
emit('options-change', selectOptions.value);
};
// 翻译
const transform = () => {
let value = props.value;
let len;
if (isArray(value) || isString(value)) {
if (isArray(value)) {
len = value.length;
value = value.join(',');
} else {
len = value.split(',').length;
}
value = value.trim();
if (value) {
// 如果value的值在selectedUser中存在则不请求翻译
let isNotRequestTransform = false;
isNotRequestTransform = value.split(',').every((value) => !!selectOptions.value.find((item) => item[props.rowKey] === value));
if (isNotRequestTransform) {
selectValues.value = value.split(',')
return;
}
const params = { isMultiTranslate: true, pageSize: len, [props.rowKey]: value };
if (isObject(attrs.params)) {
Object.assign(params, attrs.params);
}
getTableListOrigin(params).then((result: any) => {
const records = result.records ?? [];
selectValues.value = records.map((item) => item[props.rowKey]);
selectOptions.value = records.map((item) => {
return {
...item,
label: item[props.labelKey],
value: item[props.rowKey],
};
});
});
}
} else {
selectValues.value = [];
}
};
// 监听value变化
watch(
() => props.value,
() => {
transform();
},
{ deep: true, immediate: true }
);
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>

View File

@ -32,7 +32,7 @@
import { useAttrs } from '/@/hooks/core/useAttrs';
import { TreeSelect } from 'ant-design-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { isObject } from '/@/utils/is';
import { isObject, isArray } from '/@/utils/is';
import { useI18n } from '/@/hooks/web/useI18n';
enum Api {
url = '/sys/dict/loadTreeData',
@ -143,6 +143,23 @@
if(props.url){
getItemFromTreeData();
}else{
// update-begin--author:liaozhiyang---date:20250423---for【issues/8093】选择节点后会先变成编码再显示label文字
if (props.value) {
if (isArray(treeValue.value)) {
let isNotRequestTransform = false;
const value = isArray(props.value) ? props.value : props.value.split(',');
isNotRequestTransform = value.every((value) => !!treeValue.value.find((item) => item.value === value));
if (isNotRequestTransform) {
return;
}
} else if (isObject(treeValue.value) && unref(treeValue).label != null) {
if (props.value == unref(treeValue).value) {
// 不需要再去请求翻译
return;
}
}
}
// update-end--author:liaozhiyang---date:20250423---for【issues/8093】选择节点后会先变成编码再显示label文字
let params = { key: props.value };
let result = await defHttp.get({ url: `${Api.view}${props.dict}`, params }, { isTransformResponse: false });
if (result.success) {
@ -291,7 +308,22 @@
} else {
emitValue(value.value);
}
treeValue.value = value;
// update-begin--author:liaozhiyang---date:20250423---for【issues/8093】删除后会先变成编码再显示label文字
if (isArray(value)) {
// 编辑删除时有选中的值是异步第二级以上的会不显示label
value.forEach((item) => {
if (item.label === undefined && item.value != null) {
const findItem = treeValue.value.find((o) => o.value === item.value);
if (findItem) {
item.label = findItem.label;
}
}
});
treeValue.value = value;
} else {
treeValue.value = value;
}
// update-end--author:liaozhiyang---date:20250423---for【issues/8093】删除后会先变成编码再显示label文字
}
function emitValue(value) {

View File

@ -0,0 +1,833 @@
<template>
<BasicModal
wrapClassName="JSelectUserByDepartmentModal"
v-bind="$attrs"
@register="register"
:title="modalTitle"
width="800px"
@ok="handleOk"
destroyOnClose
@visible-change="visibleChange"
>
<div class="j-select-user-by-dept">
<div class="modal-content">
<!-- 左侧搜索和组织列表 -->
<div class="left-content">
<!-- 搜索框 -->
<div class="search-box">
<a-input v-model:value.trim="searchText" placeholder="搜索" @change="handleSearch" @pressEnter="handleSearch" allowClear />
</div>
<!-- 组织架构 -->
<div class="tree-box">
<template v-if="searchText.length">
<template v-if="searchResult.depart.length || searchResult.user.length">
<div class="search-result">
<template v-if="searchResult.user.length">
<div class="search-user">
<p class="search-user-title">人员</p>
<template v-for="item in searchResult.user" :key="item.id">
<div class="search-user-item" @click="handleSearchUserCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="search-user-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="search-user-item-info">
<div class="search-user-item-name">{{ item.realname }}</div>
<div class="search-user-item-org">{{ item.orgCodeTxt }}</div>
</div>
</div>
</div>
</template>
</div>
</template>
<template v-if="searchResult.depart.length">
<div class="search-depart">
<p class="search-depart-title">部门</p>
<template v-for="item in searchResult.depart" :key="item.id">
<div class="search-depart-item" @click="handleSearchDepartClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleSearchDepartCheck($event, item)" />
<div class="search-depart-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
</template>
</div>
</template>
<template v-else>
<div class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</template>
<template v-else>
<a-breadcrumb v-if="breadcrumb.length">
<a-breadcrumb-item @click="handleBreadcrumbClick()">
<HomeOutlined />
</a-breadcrumb-item>
<template v-for="item in breadcrumb" :key="item?.id">
<a-breadcrumb-item @click="handleBreadcrumbClick(item)">
<span>{{ item.departName }}</span>
</a-breadcrumb-item>
</template>
</a-breadcrumb>
<div v-if="currentDepartUsers.length">
<!-- 当前部门用户树 -->
<div class="depart-users-tree">
<div v-if="!currentDepartTree.length" class="allChecked">
<a-checkbox v-model:checked="currentDepartAllUsers" @change="handleAllUsers">全选</a-checkbox>
</div>
<template v-for="item in currentDepartUsers" :key="item.id">
<div class="depart-users-tree-item" @click="handleDepartUsersTreeCheck(item)">
<a-checkbox v-model:checked="item.checked" />
<div class="right">
<div class="depart-users-tree-item-circle">
<img v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" alt="avatar" />
</div>
<div class="depart-users-tree-item-name">{{ item.realname }}</div>
</div>
</div>
</template>
</div>
</div>
<!-- 部门树 -->
<div v-if="currentDepartTree.length" class="depart-tree">
<template v-for="item in currentDepartTree" :key="item.id">
<div class="depart-tree-item" @click="handleDepartTreeClick(item)">
<a-checkbox v-model:checked="item.checked" @click.stop @change="($event) => handleDepartTreeCheck($event, item)" />
<div class="depart-tree-item-name">{{ item.departName }}</div>
<RightOutlined />
</div>
</template>
</div>
<div v-if="currentDepartTree.length === 0 && currentDepartUsers.length === 0" class="no-data">
<a-empty description="暂无数据" />
</div>
</template>
</div>
</div>
<!-- 右侧已选人员展示 -->
<div class="right-content">
<div class="selected-header"> 已选人员{{ selectedUsers.length }} </div>
<div class="selected-users">
<div class="content">
<div v-for="user in selectedUsers" :key="user.id" class="user-avatar" @click="handleDelUser(user)">
<div class="avatar-circle">
<img v-if="user.avatar" :src="getFileAccessHttpUrl(user.avatar)" alt="avatar" />
<div class="mask">
<CloseOutlined></CloseOutlined>
</div>
</div>
<div class="user-name">{{ user.realname }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</BasicModal>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { RightOutlined, HomeOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryTreeList, getTableList as getTableListOrigin } from '/@/api/common/api';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { isArray } from '/@/utils/is';
import { defHttp } from '/@/utils/http/axios';
defineOptions({ name: 'JSelectUserByDepartmentModal' });
const props = defineProps({
// ...selectProps,
//回传value字段名
rowKey: {
type: String,
default: 'id',
},
//回传文本字段名
labelKey: {
type: String,
default: 'name',
},
modalTitle: {
type: String,
default: '部门用户选择',
},
selectedUser: {
type: Array,
default: () => [],
},
//查询参数
params: {
type: Object,
default: () => {},
},
//最大选择数量
maxSelectCount: {
type: Number,
default: 0,
},
// 是否单选
isRadioSelection: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['close', 'register', 'change']);
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 搜索文本
const searchText = ref('');
const breadcrumb = ref<any[]>([]);
// 部门树(整颗树)
const departTree = ref([]);
// 当前部门树
const currentDepartTree = ref<any[]>([]);
// 选中的部门节点
const checkedDepartIds = ref<string[]>([]);
// 当前部门用户
const currentDepartUsers = ref([]);
// 已选用户
const selectedUsers = ref<any[]>([]);
// 全选
const currentDepartAllUsers = ref(false);
// 搜索结构
const searchResult: any = reactive({
depart: [],
user: [],
});
// 映射部门和人员的关系
const cacheDepartUser = {};
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
// 初始化
if (props.selectedUser.length) {
// 编辑时,传进来已选中的数据
selectedUsers.value = props.selectedUser;
}
getQueryTreeList();
});
const visibleChange = (visible) => {
if (visible === false) {
setTimeout(() => {
emit('close');
}, 300);
}
};
const handleOk = () => {
if (selectedUsers.value.length == 0) {
createMessage.warning('请选择人员');
return;
}
if (props.isRadioSelection && selectedUsers.value.length > 1) {
createMessage.warning('只允许选择一个用户');
return;
}
if (props.maxSelectCount && selectedUsers.value.length > props.maxSelectCount) {
createMessage.warning(`最多只能选择${props.maxSelectCount}个用户`);
return;
}
emit('change', selectedUsers.value);
closeModal();
};
// 搜索人员/部门
const handleSearch = () => {
if (searchText.value) {
defHttp
.get({
url: `/sys/user/listAll`,
params: {
column: 'createTime',
order: 'desc',
pageNo: 1,
pageSize: 100,
realname: `*${searchText.value}*`,
},
})
.then((result: any) => {
result.records?.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
}
});
searchResult.user = result.records ?? [];
});
searchResult.depart = getDepartByName(searchText.value) ?? [];
} else {
searchResult.user = [];
searchResult.depart = [];
}
};
// 面包屑
const handleBreadcrumbClick = (item?) => {
// 先清空
currentDepartUsers.value = [];
if (item) {
const findIndex = breadcrumb.value.findIndex((o) => o.id === item.id);
if (findIndex != -1) {
breadcrumb.value = breadcrumb.value.filter((item, index) => {
console.log(item);
return index <= findIndex;
});
}
const data = getDepartTreeNodeById(item.id, departTree.value);
currentDepartTree.value = data.children;
} else {
// 根节点
currentDepartTree.value = departTree.value;
breadcrumb.value = [];
}
};
// 点击部门树复选框触发
const handleDepartTreeCheck = (e, item) => {
const { target } = e;
if (target.checked) {
// 选中
getUsersByDeptId(item['id']).then((users) => {
addUsers(users);
});
checkedDepartIds.value.push((item as any).id);
// 检查父节点下所有子节点是否选中
const parentItem = getDepartTreeParentById(item.id);
if (parentItem?.children) {
const isChildAllChecked = parentItem.children.every((item) => item.checked);
if (isChildAllChecked) {
parentItem.checked = true;
} else {
parentItem.checked = false;
}
}
} else {
// 取消选中
const findIndex = checkedDepartIds.value.findIndex((o: any) => o.id === item.id);
if (findIndex != -1) {
checkedDepartIds.value.splice(findIndex, 1);
}
// 如果父节点是选中,则需要取消
const parentItem = getDepartTreeParentById(item.id);
if (parentItem) {
parentItem.checked = false;
}
getUsersByDeptId(item['id']).then((users) => {
users.forEach((item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
});
});
}
};
// 点击部门树节点触发
const handleDepartTreeClick = (item) => {
breadcrumb.value = [...breadcrumb.value, item];
if (item.children) {
// 有子节点,则显示部门
if (item.checked) {
// 父节点勾选,则子节点全部勾选
item.children.forEach((item) => {
item.checked = true;
});
}
currentDepartTree.value = item.children;
defHttp
.get({
url: '/sys/sysDepart/getUsersByDepartId',
params: {
id: item['id'],
},
})
.then((res: any) => {
const result = res ?? [];
if (item.checked) {
// 父节点勾选,则默认勾选
result.forEach((item) => {
item.checked = true;
});
}
// 右侧勾选了,则默认勾选(用户存在多部门,在别的部门被选中了)
if (selectedUsers.value.length) {
result.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id === item.id);
if (findItem) {
// 说明在selectedUsers中被找到
item.checked = true;
}
});
}
currentDepartUsers.value = result;
});
} else {
// 没有子节点,则显示用户
currentDepartTree.value = [];
getTableList({
departId: item['id'],
}).then((res: any) => {
if (res?.records) {
let checked = true;
res.records.forEach((item) => {
const findItem = selectedUsers.value.find((user) => user.id == item.id);
if (findItem) {
// 能在右侧找到说明选中了,左侧同样需要选中。
item.checked = true;
} else {
item.checked = false;
checked = false;
}
});
currentDepartAllUsers.value = checked;
currentDepartUsers.value = res.records;
}
});
}
};
// 点击部门用户树复选框触发
const handleDepartUsersTreeCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
if (item.checked == false) {
// 有一个是false则全选false
currentDepartAllUsers.value = false;
}
};
// 全选
const handleAllUsers = ({ target }) => {
const { checked } = target;
if (checked) {
currentDepartUsers.value.forEach((item: any) => (item.checked = true));
addUsers(currentDepartUsers.value);
} else {
currentDepartUsers.value.forEach((item: any) => (item.checked = false));
selectedUsers.value = selectedUsers.value.filter((user) => {
const userId = user.id;
const findItem = currentDepartUsers.value.find((item: any) => item.id === userId);
if (findItem) {
return false;
} else {
return true;
}
});
}
};
// 删除人员
const handleDelUser = (item) => {
const findIndex = selectedUsers.value.findIndex((user) => user.id === item.id);
if (findIndex != -1) {
selectedUsers.value.splice(findIndex, 1);
}
const findItem: any = currentDepartUsers.value.find((user: any) => user.id === item.id);
if (findItem) {
findItem.checked = false;
currentDepartAllUsers.value = false;
}
};
// 点击搜索用户复选框
const handleSearchUserCheck = (item) => {
item.checked = !item.checked;
if (item.checked) {
addUsers(item);
} else {
selectedUsers.value = selectedUsers.value.filter((user) => user.id !== item.id);
}
};
// 点击搜索部门复选框
const handleSearchDepartCheck = (e, item) => {
handleDepartTreeCheck(e, item);
};
// 点击搜索部门
const handleSearchDepartClick = (item) => {
searchResult.depart = [];
searchResult.user = [];
breadcrumb.value = getPathToNodeById(item.id);
handleDepartTreeClick(item);
};
// 添加人员到右侧
const addUsers = (users) => {
let newUsers: any = [];
if (isArray(users)) {
// selectedUsers里面没有才添加防止重复
newUsers = users.filter((user: any) => !selectedUsers.value.find((item) => item.id === user.id));
} else {
if (!selectedUsers.value.find((user) => user.id === users.id)) {
// selectedUsers里面没有才添加防止重复
newUsers = [users];
}
}
selectedUsers.value = [...selectedUsers.value, ...newUsers];
const result = currentDepartUsers.value.every((item: any) => !!item.checked);
currentDepartAllUsers.value = result;
};
// 解析参数
const parseParams = (params) => {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
};
const getQueryTreeList = (params?) => {
params = parseParams(params);
queryTreeList({ ...params }).then((res) => {
if (res) {
departTree.value = res;
currentDepartTree.value = res;
}
});
};
// 根据部门id获取用户
const getTableList = (params) => {
params = parseParams(params);
return getTableListOrigin({ ...params });
};
const getUsersByDeptId = (id) => {
return new Promise<any[]>((resolve) => {
if (cacheDepartUser[id]) {
resolve(cacheDepartUser[id]);
} else {
getTableList({
departId: id,
}).then((res: any) => {
cacheDepartUser[id] = res.records ?? [];
if (res?.records?.length) {
resolve(res.records ?? []);
}
});
}
});
};
// 根据id获取根节点到当前节点路径
const getPathToNodeById = (id: string, tree = departTree.value, path = []): any[] => {
for (const node of tree) {
if ((node as any).id === id) {
return [...path];
}
if ((node as any).children) {
const foundPath = getPathToNodeById(id, (node as any).children, [...path, node]);
if (foundPath.length) {
return foundPath;
}
}
}
return [];
};
// 根据id获取部门树父节点数据
const getDepartTreeParentById = (id: string, tree = departTree.value, parent = null): any => {
for (const node of tree) {
if ((node as any).id === id) {
return parent;
}
if ((node as any).children) {
const found = getDepartTreeParentById(id, (node as any).children, node);
if (found) {
return found;
}
}
}
return null;
};
// 通过名称搜索部门支持模糊
const getDepartByName = (name: string, tree = departTree.value): any[] => {
const result: any[] = [];
const search = (nodes: any[]) => {
for (const node of nodes) {
if (node.departName?.toLowerCase().includes(name.toLowerCase())) {
result.push(node);
}
if (node.children?.length) {
search(node.children);
}
}
};
search(tree);
return result;
};
// 根据id获取部门树当前节点数据
const getDepartTreeNodeById = (id: string, tree = departTree.value): any => {
for (const node of tree) {
if ((node as any).id === id) {
return node;
}
if ((node as any).children) {
const found = getDepartTreeNodeById(id, (node as any).children);
if (found) {
return found;
}
}
}
return null;
};
</script>
<style lang="less">
.JSelectUserByDepartmentModal {
.scroll-container {
padding: 0;
}
}
</style>
<style lang="less" scoped>
.j-select-user-by-dept {
background: #fff;
border-radius: 4px;
}
.modal-content {
display: flex;
padding: 20px;
padding-bottom: 0;
padding-left: 0;
height: 400px;
font-size: 12px;
}
.left-content {
display: flex;
flex-direction: column;
flex: 1;
border-right: 1px solid #e8e8e8;
.search-box {
margin: 0 16px 16px 16px;
}
:deep(.ant-breadcrumb) {
font-size: 12px;
margin-left: 16px;
color: inherit;
cursor: pointer;
li {
.ant-breadcrumb-link {
cursor: pointer;
&:hover {
color: @primary-color;
}
}
&:last-child {
.ant-breadcrumb-link {
pointer-events: none;
}
}
}
}
.tree-box {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
.no-data {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
}
.depart-tree {
.depart-tree-item {
padding: 0 16px;
line-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
}
.depart-tree-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 8px;
}
}
}
.depart-users-tree {
.allChecked {
padding: 0 16px;
margin-bottom: 16px;
padding-top: 12px;
:deep(.ant-checkbox-wrapper) {
font-size: 12px;
}
}
.depart-users-tree-item {
line-height: 50px;
padding: 0 16px;
display: flex;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.depart-users-tree-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background-color: #aaa;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
}
}
.depart-users-tree-item-name {
margin-left: 8px;
}
}
}
.search-depart {
margin-bottom: 8px;
.search-depart-title {
padding-left: 16px;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.search-depart-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.search-depart-item-name {
margin-left: 8px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.search-user {
margin-bottom: 8px;
.search-user-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
padding-left: 16px;
}
.search-user-item {
display: flex;
align-items: center;
padding: 8px 16px;
cursor: pointer;
&:hover {
background-color: #f4f6fa;
}
.right {
flex: 1;
display: flex;
align-items: center;
margin: 0 8px;
}
.search-user-item-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 8px;
}
.search-user-item-circle {
width: 36px;
height: 36px;
border-radius: 50%;
overflow: hidden;
background-color: #aaa;
}
.search-user-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-user-item-org {
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.right-content {
width: 400px;
display: flex;
flex-direction: column;
padding-left: 16px;
.selected-header {
margin-bottom: 16px;
}
.selected-users {
flex: 1;
overflow-y: auto;
}
.content {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.user-avatar {
text-align: center;
width: 70px;
cursor: pointer;
}
.avatar-circle {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: white;
margin: 0 auto 8px;
position: relative;
background-color: rgba(0, 0, 0, 0.5);
img {
width: 100%;
height: 100%;
}
&:hover {
.mask {
opacity: 1;
}
}
.mask {
opacity: 0;
transition: opacity;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
}
.user-name {
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
</style>

View File

@ -45,7 +45,9 @@ export function useSelectBiz(getList, props, emit?) {
});
}
//设置列表默认选中
checkedKeys['value'] = selectValues['value'];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
},
{ immediate: true }
);
@ -109,7 +111,9 @@ export function useSelectBiz(getList, props, emit?) {
code: selectValues['value'].join(','),
pageSize: selectValues['value'].length,
});
checkedKeys['value'] = selectValues['value'];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
checkedKeys['value'] = [...selectValues['value']];
// update-end--author:liaozhiyang---date:20250423---for【QQYUN-12155】弹窗中勾选再点取消值被选中了
selectRows['value'] = records;
}
@ -118,6 +122,9 @@ export function useSelectBiz(getList, props, emit?) {
*/
async function visibleChange(visible) {
if (visible) {
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12179】弹窗勾选了值点击取消再次打开弹窗遗留了上次的勾选的值
checkedKeys['value'] = [...selectValues['value']];
// update-begin--author:liaozhiyang---date:20250423---for【QQYUN-12179】弹窗勾选了值点击取消再次打开弹窗遗留了上次的勾选的值
//设置列表默认选中
props.showSelected && initSelectRows();
} else {

View File

@ -2,6 +2,7 @@ import type { Ref } from 'vue';
import { inject, reactive, ref, computed, unref, watch, nextTick } from 'vue';
import { TreeActionType } from '/@/components/Tree';
import { listToTree } from '/@/utils/common/compUtils';
import { isEqual } from 'lodash-es';
export function useTreeBiz(treeRef, getList, props, realProps, emit) {
//接收下拉框选项
@ -22,7 +23,7 @@ export function useTreeBiz(treeRef, getList, props, realProps, emit) {
const getCheckStrictly = computed(() => (realProps.multiple ? props.checkStrictly : true));
// 是否是首次加载回显,只有首次加载,才会显示 loading
let isFirstLoadEcho = true;
let prevSelectValues = [];
/**
* 监听selectValues变化
*/
@ -32,12 +33,17 @@ export function useTreeBiz(treeRef, getList, props, realProps, emit) {
if(!values){
return;
}
if (openModal.value == false && values.length > 0) {
// update-begin--author:liaozhiyang---date:20250604---for【issues/8232】代码设置JSelectDept组件值没翻译
if (values.length > 0) {
// 防止多次请求
if (isEqual(values, prevSelectValues)) return;
prevSelectValues = values;
loadingEcho.value = isFirstLoadEcho;
isFirstLoadEcho = false;
onLoadData(null, values.join(',')).finally(() => {
loadingEcho.value = false;
});
// update-end--author:liaozhiyang---date:20250604---for【issues/8232】代码设置JSelectDept组件值没翻译
}
},
{ immediate: true }

View File

@ -139,6 +139,7 @@ export type ComponentType =
| 'JTreeSelect'
| 'JEllipsis'
| 'JSelectUserByDept'
| 'JSelectUserByDepartment'
| 'JUpload'
| 'JSearchSelect'
| 'JAddInput'

View File

@ -222,7 +222,7 @@
// update-end--author:sunjianlei---date:220230630---for【QQYUN-5571】自封装选择列解决数据行选择卡顿问题
);
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef, slots);
const { getScrollRef, redoHeight } = useTableScroll(getProps, tableElRef, getColumnsRef, getRowSelectionRef, getDataSourceRef, slots, getPaginationInfo);
const { customRow } = useCustomRow(getProps, {
setSelectedRowKeys,

View File

@ -371,9 +371,7 @@
if (props.column.dataIndex) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
// update-begin--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
props.record.editValueRefs[props.column.dataIndex] = unref(currentValueRef);
// update-end--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {

View File

@ -73,8 +73,6 @@ export type EditRecordRow<T = Recordable> = Partial<
submitCbs: Cbs[];
cancelCbs: Cbs[];
validCbs: Cbs[];
// update-begin--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
editValueRefs: Recordable;
// update-end--author:liaozhiyang---date:20250206---for【issues/7709】当dataSource是响应式时单元格编辑输入会自动关闭
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@ -15,7 +15,8 @@ export function useTableScroll(
columnsRef: ComputedRef<BasicColumn[]>,
rowSelectionRef: ComputedRef<TableRowSelection<any> | null>,
getDataSourceRef: ComputedRef<Recordable[]>,
slots: Slots
slots: Slots,
getPaginationInfo: ComputedRef<any>
) {
const tableHeightRef: Ref<Nullable<number>> = ref(null);
@ -138,12 +139,33 @@ export function useTableScroll(
// 10+6(外层边距padding:10 + 内层padding-bottom:6)
height -= 16;
// update-end--author:liaozhiyang---date:20240603---for【TV360X-861】列表查询区域不可往上滚动
height = (height < minHeight! ? (minHeight as number) : height) ?? height;
height = (height > maxHeight! ? (maxHeight as number) : height) ?? height;
setHeight(height);
bodyEl!.style.height = `${height}px`;
// update-begin--author:liaozhiyang---date:20240609---for【issues/8374】分页始终显示在底部
if (maxHeight === undefined) {
if (unref(getPaginationInfo) && unref(getDataSourceRef).length) {
const pageSize = unref(getPaginationInfo)?.pageSize;
const current = unref(getPaginationInfo)?.current;
const total = unref(getPaginationInfo)?.total;
const tableBody = tableEl.querySelector('.ant-table-body') as HTMLElement;
const tr = tableEl.querySelector('.ant-table-tbody')?.children ?? [];
const lastrEl = tr[tr.length - 1] as HTMLElement;
const trHeight = lastrEl.offsetHeight;
const dataHeight = trHeight * pageSize;
if (tableBody && lastrEl) {
if (current === 1 && pageSize > unref(getDataSourceRef).length && total <= pageSize) {
tableBody.style.height = `${height}px`;
} else {
tableBody.style.height = `${dataHeight < height ? dataHeight : height}px`;
}
}
}
}
// update-end--author:liaozhiyang---date:20240609---for【issues/8374】分页始终显示在底部
}
useWindowSizeFn(calcTableHeight, 280);
onMountedOrActivated(() => {

View File

@ -7,6 +7,8 @@ interface PaginationRenderProps {
originalElement: any;
}
type Position = 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight';
export declare class PaginationConfig extends Pagination {
position?: 'top' | 'bottom' | 'both';
}
@ -96,4 +98,11 @@ export interface PaginationProps {
* @type Function
*/
itemRender?: (props: PaginationRenderProps) => VNodeChild | JSX.Element;
// update-begin--author:liaozhiyang---date:20250423---for【pull/8013】修复 BasicTable position 属性类型配置
/**
* specify the position of Pagination
* @type Position[]
*/
position?: Position[];
// update-end--author:liaozhiyang---date:20250423---for【pull/8013】修复 BasicTable position 属性类型配置
}

View File

@ -28,7 +28,9 @@ export function useColumns(props: JVxeTableProps, data: JVxeDataProps, methods:
// update-begin--author:liaozhiyang---date:20250403---for【issues/7812】linkageConfig改变了vxetable没更新
// linkageConfig变化时也需要执行
const linkageConfig = toRaw(props.linkageConfig);
console.log(linkageConfig);
if (linkageConfig) {
// console.log(linkageConfig);
}
// update-end--author:liaozhiyang---date:20250403---for【issues/7812】linkageConfig改变了vxetable没更新
let columns: JVxeColumn[] = [];
if (isArray(props.columns)) {

View File

@ -18,10 +18,10 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
// rowId: props.rowKey,
rowConfig: {
keyField: props.rowKey,
// 高亮hover的行
isHover: true,
},
// update-end--author:liaozhiyang---date:20240607---for【TV360X-327】vxetable警告
// 高亮hover的行
highlightHoverRow: true,
// --- 【issues/209】自带的tooltip会错位所以替换成原生的title ---
// 溢出隐藏并显示tooltip
@ -41,8 +41,9 @@ export function useData(props: JVxeTableProps): JVxeDataProps {
// update-end--author:liaozhiyang---date:20231013---for【QQYUN-5133】JVxeTable 行编辑升级
},
expandConfig: {
iconClose: 'ant-table-row-expand-icon ant-table-row-expand-icon-collapsed',
iconOpen: 'ant-table-row-expand-icon ant-table-row-expand-icon-expanded',
iconClose: 'vxe-icon-arrow-right',
iconOpen: 'vxe-icon-arrow-down',
...props.expandConfig,
},
// 虚拟滚动配置y轴大于xx条数据时启用虚拟滚动
scrollY: {

View File

@ -23,7 +23,12 @@ export function useDragSort(props: JVxeTableProps, methods: JVxeTableMethods) {
function createSortable() {
let xTable = methods.getXTable();
// let dom = xTable.$el.querySelector('.vxe-table--fixed-wrapper .vxe-table--body tbody')
let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
// let dom = xTable.$el.querySelector('.body--wrapper>.vxe-table--body tbody');
let dom = xTable.$el.querySelector('.vxe-table--body-inner-wrapper > .vxe-table--body tbody');
if (!dom) {
console.warn('[JVxeTable] 拖拽排序初始化失败可能是vxe-table升级导致的版本不兼容。');
return;
}
let startChildren = [];
sortable2 = Sortable.create(dom as HTMLElement, {
handle: '.drag-btn',

View File

@ -424,12 +424,13 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
let xTable = getXTable();
let { setActive, index } = options;
index = index === -1 ? index : xTable.internalData.tableFullData[index];
index = index == null ? -1 : index;
// 插入行
let result = await xTable.insertAt(rows, index);
if (setActive) {
// -update-begin--author:liaozhiyang---date:20240619---for【TV360X-1404】vxetable警告
// 激活最后一行的编辑模式
xTable.setEditRow(result.rows[result.rows.length - 1]);
xTable.setEditRow(result.rows[result.rows.length - 1], true);
// -update-end--author:liaozhiyang---date:20240619---for【TV360X-1404】vxetable警告
}
await recalcSortNumber();
@ -763,7 +764,7 @@ export function useMethods(props: JVxeTableProps, { emit }, data: JVxeDataProps,
// 4.1.0
//await xTable.updateCache();
// 4.1.1
await xTable.cacheRowMap()
await xTable.cacheRowMap(true)
// update-end--author:liaozhiyang---date:20231011---for【QQYUN-5133】JVxeTable 行编辑升级
return await xTable.updateData();
}

View File

@ -52,7 +52,9 @@ export function usePagination(props: JVxeTableProps, methods: JVxeTableMethods)
[
h(Pagination, {
...bindProps.value,
disabled: props.disabled,
// update-begin--author:liaozhiyang---date:20250423---for【issues/8137】vxetable表格禁用后分页隐藏了
disabled: false,
// update-end--author:liaozhiyang---date:20250423---for【issues/8137】vxetable表格禁用后分页隐藏了
onChange: handleChange,
onShowSizeChange: handleShowSizeChange,
}),

View File

@ -1,8 +1,10 @@
import type { App } from 'vue';
// 引入 vxe-table
import 'xe-utils';
import VxeUIAll from 'vxe-pc-ui';
import VXETable /*Grid*/ from 'vxe-table';
import VXETablePluginAntd from 'vxe-table-plugin-antd';
import 'vxe-pc-ui/lib/style.css';
import 'vxe-table/lib/style.css';
import JVxeTable from './JVxeTable';
@ -27,6 +29,7 @@ export function registerJVxeTable(app: App) {
// 注册自定义组件
registerAllComponent();
// 执行注册方法
app.use(VxeUIAll);
app.use(VXETable, VXETableSettings);
app.component('JVxeTable', JVxeTable);
}
@ -39,6 +42,12 @@ export function registerJVxeTable(app: App) {
function preventClosingPopUp(this: any, params) {
// 获取组件增强
let col = params.column.params;
// update-begin--author:liaozhiyang---date:20250429---for【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错
if (col === undefined) {
// 说明使用的是纯原生的vxe-table
return;
}
// update-end--author:liaozhiyang---date:20250429---for【issues/8178】使用原生vxe-table组件编辑模式下失去焦点报错
let { $event } = params;
const interceptor = getEnhanced(col.type).interceptor;
// 执行增强

View File

@ -258,6 +258,20 @@ export const schemas: FormSchema[] = [
label: '选中用户',
colProps: { span: 12 },
},
{
field: 'user4',
component: 'JSelectUserByDepartment',
label: '部门选择用户',
helpMessage: ['component模式'],
defaultValue: '',
componentProps: {
labelKey: 'realname',
rowKey: 'username',
},
colProps: {
span: 12,
},
},
{
field: 'role2',
component: 'JSelectRole',

View File

@ -43,7 +43,7 @@
const { createMessage } = useMessage();
const checkedKeys = ref<Array<string | number>>([]);
const logColumns = ref<any>(columns);
const logColumns = ref<any>(exceptionColumns);
const searchSchema = ref<any>(searchFormSchema);
const searchInfo = { logType: '4' };
// 列表页面公共参数、方法

View File

@ -0,0 +1,118 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/openapi/list',
save='/openapi/add',
edit='/openapi/edit',
deleteOne = '/openapi/delete',
deleteBatch = '/openapi/deleteBatch',
genPath = '/openapi/genPath',
importExcel = '/openapi/importExcel',
exportXls = '/openapi/exportXls',
openApiHeaderList = '/openapi/list',
openApiParamList = '/openapi/list',
openApiJson = '/openapi/json',
}
/**
* 子表单查询接口
* @param params
*/
export const genPath = Api.genPath
/**
* swagger文档json
* @param params
*/
export const openApiJson = Api.openApiJson
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 子表单查询接口
* @param params
*/
export const queryOpenApiHeader = Api.openApiHeaderList
/**
* 子表单查询接口
* @param params
*/
export const queryOpenApiParam = Api.openApiParamList
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({url: Api.edit, params});
} else {
return defHttp.post({url: Api.save, params});
}
}
/**
* 获取接口地址
* @param params
*/
export const getGenPath = (params) =>
defHttp.get({url: Api.genPath, params},{isTransformResponse:false});
/**
* 子表列表接口
* @param params
*/
export const openApiHeaderList = (params) =>
defHttp.get({url: Api.openApiHeaderList, params},{isTransformResponse:false});
/**
* 子表列表接口
* @param params
*/
export const openApiParamList = (params) =>
defHttp.get({url: Api.openApiParamList, params},{isTransformResponse:false});
/**
* swagger文档json
* @param params
*/
export const getOpenApiJson = (params) =>
defHttp.get({url: Api.openApiJson, params},{isTransformResponse:false});

View File

@ -0,0 +1,345 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types'
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '接口名称',
align:"center",
dataIndex: 'name'
},
{
title: '请求方法',
align:"center",
dataIndex: 'requestMethod'
},
{
title: '接口地址',
align:"center",
dataIndex: 'requestUrl'
},
{
title: 'IP 黑名单',
align:"center",
dataIndex: 'blackList'
},
// {
// title: '状态',
// align:"center",
// dataIndex: 'status'
// },
{
title: '创建人',
align:"center",
dataIndex: 'createBy'
},
{
title: '创建时间',
align:"center",
dataIndex: 'createTime'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
{
label: "接口名称",
field: "name",
component: 'JInput',
},
{
label: "创建人",
field: "createBy",
component: 'JInput',
},
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: '接口名称',
field: 'name',
component: 'Input',
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入接口名称!'},
];
},
},
{
label: '原始地址',
field: 'originUrl',
component: 'Input',
},
{
label: '请求方法',
field: 'requestMethod',
component: 'JSearchSelect',
componentProps:{
dictOptions: [
{
text: 'POST',
value: 'POST',
},
{
text: 'GET',
value: 'GET',
},
{
text: 'HEAD',
value: 'HEAD',
},
{
text: 'PUT',
value: 'PUT',
},
{
text: 'PATCH',
value: 'PATCH',
},
{
text: 'DELETE',
value: 'DELETE',
},{
text: 'OPTIONS',
value: 'OPTIONS',
},{
text: 'TRACE',
value: 'TRACE',
},
]
},
dynamicRules: ({model,schema}) => {
return [
{ required: true, message: '请输入请求方法!'},
];
},
},
{
label: '接口地址',
field: 'requestUrl',
component: 'Input',
dynamicDisabled:true
},
{
label: 'IP 黑名单',
field: 'blackList',
component: 'Input',
},
{
label: '请求体内容',
component:"Input",
field: 'body'
},
{
label: '删除标识',
field: 'delFlag',
component: 'Input',
defaultValue:0,
show:false
},
{
label: '状态',
field: 'status',
component: 'Input',
defaultValue:"1",
show:false
},
// TODO 主键隐藏字段目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false
},
];
//子表单数据
//子表列表数据
export const openApiHeaderColumns: BasicColumn[] = [
// {
// title: 'apiId',
// align:"center",
// dataIndex: 'apiId'
// },
{
title: '请求头Key',
align:"center",
dataIndex: 'headerKey'
},
{
title: '是否必填',
align:"center",
dataIndex: 'required_dictText'
},
{
title: '默认值',
align:"center",
dataIndex: 'defaultValue'
},
{
title: '备注',
align:"center",
dataIndex: 'note'
},
];
//子表列表数据
export const openApiParamColumns: BasicColumn[] = [
// {
// title: 'apiId',
// align:"center",
// dataIndex: 'apiId'
// },
{
title: '参数Key',
align:"center",
dataIndex: 'paramKey'
},
{
title: '是否必填',
align:"center",
dataIndex: 'required_dictText'
},
{
title: '默认值',
align:"center",
dataIndex: 'defaultValue'
},
{
title: '备注',
align:"center",
dataIndex: 'note'
},
];
//子表表格配置
export const openApiHeaderJVxeColumns: JVxeColumn[] = [
// {
// title: 'apiId',
// key: 'apiId',
// type: JVxeTypes.input,
// width:"200px",
// placeholder: '请输入${title}',
// defaultValue:'',
// },
{
title: '请求头Key',
key: 'headerKey',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '是否必填',
key: 'required',
type: JVxeTypes.checkbox,
options:[],
// dictCode:"yn",
width:"100px",
placeholder: '请输入${title}',
defaultValue:'',
customValue: ['1','0']
},
{
title: '默认值',
key: 'defaultValue',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '备注',
key: 'note',
type: JVxeTypes.input,
placeholder: '请输入${title}',
defaultValue:'',
},
]
export const openApiParamJVxeColumns: JVxeColumn[] = [
// {
// title: 'apiId',
// key: 'apiId',
// type: JVxeTypes.input,
// width:"200px",
// placeholder: '请输入${title}',
// defaultValue:'',
// },
{
title: '参数Key',
key: 'paramKey',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '是否必填',
key: 'required',
type: JVxeTypes.checkbox,
options:[],
// dictCode:"yn",
width:"100px",
placeholder: '请输入${title}',
defaultValue:'',
customValue: ['1','0']
},
{
title: '默认值',
key: 'defaultValue',
type: JVxeTypes.input,
width:"200px",
placeholder: '请输入${title}',
defaultValue:'',
},
{
title: '备注',
key: 'note',
type: JVxeTypes.input,
placeholder: '请输入${title}',
defaultValue:'',
},
]
// 高级查询数据
export const superQuerySchema = {
name: {title: '接口名称',order: 0,view: 'text', type: 'string',},
requestMethod: {title: '请求方法',order: 1,view: 'list', type: 'string',dictCode: '',},
requestUrl: {title: '接口地址',order: 2,view: 'text', type: 'string',},
blackList: {title: 'IP 黑名单',order: 3,view: 'text', type: 'string',},
status: {title: '状态',order: 5,view: 'number', type: 'number',},
createBy: {title: '创建人',order: 6,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 7,view: 'datetime', type: 'string',},
//子表高级查询
openApiHeader: {
title: '请求头表',
view: 'table',
fields: {
// apiId: {title: 'apiId',order: 0,view: 'text', type: 'string',},
headerKey: {title: '请求头Key',order: 1,view: 'text', type: 'string',},
required: {title: '是否必填',order: 2,view: 'number', type: 'number',dictCode: 'yn',},
defaultValue: {title: '默认值',order: 3,view: 'text', type: 'string',},
note: {title: '备注',order: 4,view: 'text', type: 'string',},
}
},
openApiParam: {
title: '请求参数部分',
view: 'table',
fields: {
// apiId: {title: 'apiId',order: 0,view: 'text', type: 'string',},
paramKey: {title: '参数Key',order: 1,view: 'text', type: 'string',},
required: {title: '是否必填',order: 2,view: 'number', type: 'number',dictCode: 'yn',},
defaultValue: {title: '默认值',order: 3,view: 'text', type: 'string',},
note: {title: '备注',order: 4,view: 'text', type: 'string',},
}
},
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[]{
// 默认和原始表单保持一致 如果流程中配置了权限数据这里需要单独处理formSchema
return formSchema;
}

View File

@ -0,0 +1,122 @@
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/openapi/auth/list',
save='/openapi/auth/add',
edit='/openapi/auth/edit',
apiList= '/openapi/list',
genAKSK = '/openapi/auth/genAKSK',
permissionList='/openapi/permission/getOpenApi',
permissionAdd='/openapi/permission/add',
deleteOne = '/openapi/auth/delete',
deleteBatch = '/openapi/auth/deleteBatch',
importExcel = '/openapi/auth/importExcel',
exportXls = '/openapi/auth/exportXls',
}
/**
* 获取API
* @param params
*/
export const apiList = Api.apiList;
/**
* 权限添加
* @param params
*/
export const permissionAdd = Api.permissionAdd;
/**
* 生成AKSK
* @param params
*/
export const genAKSK = Api.genAKSK;
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除单个
* @param params
* @param handleSuccess
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
* @param handleSuccess
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
* @param isUpdate
*/
export const saveOrUpdate = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({ url: Api.edit, params }, { isTransformResponse: false });
}
return defHttp.post({ url: Api.save, params }, { isTransformResponse: false });
}
/**
* 全部权限列表接口
* @param params
*/
export const getApiList = (params) => defHttp.get({ url: Api.apiList, params }, { isTransformResponse: false });
/**
* 获取已授权项目的接口
* @param params
*/
export const getPermissionList = (params) => defHttp.get({ url: Api.permissionList, params });
/**
* 授权保存方法
* @param params
* @param isUpdate
*/
export const permissionAddFunction = (params) => {
return defHttp.post({ url: Api.permissionAdd, params }, { isTransformResponse: false });
}
/**
* 授权保存方法
* @param params
* @param isUpdate
*/
export const getGenAKSK = (params) => {
return defHttp.get({ url: Api.genAKSK, params });
}

View File

@ -0,0 +1,48 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: '授权名称',
align: "center",
dataIndex: 'name'
},
{
title: 'AK',
align: "center",
dataIndex: 'ak'
},
{
title: 'SK',
align: "center",
dataIndex: 'sk'
},
{
title: '创建人',
align: "center",
dataIndex: 'createBy'
},
{
title: '创建时间',
align: "center",
dataIndex: 'createTime'
},
// {
// title: '关联系统用户名',
// align: "center",
// dataIndex: 'createBy',
// },
];
// 高级查询数据
export const superQuerySchema = {
name: {title: '授权名称',order: 0,view: 'text', type: 'string',},
ak: {title: 'AK',order: 1,view: 'text', type: 'string',},
sk: {title: 'SK',order: 2,view: 'text', type: 'string',},
createBy: {title: '关联系统用户名',order: 3,view: 'text', type: 'string',},
createTime: {title: '创建时间',order: 4,view: 'datetime', type: 'string',},
// systemUserId: {title: '关联系统用户名',order: 5,view: 'text', type: 'string',},
};

View File

@ -0,0 +1,303 @@
<template>
<div class="p-2">
<!--查询区域-->
<div class="jeecg-basic-table-form-container">
<a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item name="name">
<template #label><span title="授权名称">授权名称</span></template>
<a-input placeholder="请输入授权名称" v-model:value="queryParam.name" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item name="createBy">
<template #label><span title="关联系统用户名">关联系统用户名</span></template>
<JSearchSelect dict="sys_user,username,username" v-model:value="queryParam.createBy" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>
<!-- <a-input placeholder="请输入关联系统用户名" v-model:value="queryParam.systemUserId" allow-clear ></a-input>-->
</a-form-item>
</a-col>
<a-col :xl="6" :lg="7" :md="8" :sm="24">
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
<a @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
{{ toggleSearchStatus ? '收起' : '展开' }}
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</a>
</a-col>
</span>
</a-col>
</a-row>
</a-form>
</div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'openapi:open_api_auth:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api_auth:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api_auth:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'openapi:open_api_auth:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiAuthModal ref="registerModal" @success="handleSuccess"></OpenApiAuthModal>
<AuthModal ref="authModal" @success="handleSuccess"></AuthModal>
</div>
</template>
<script lang="ts" name="openapi-openApiAuth" setup>
import { ref, reactive } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, superQuerySchema } from './OpenApiAuth.data';
import {
list,
deleteOne,
batchDelete,
getImportUrl,
getExportUrl,
getGenAKSK, saveOrUpdate
} from "./OpenApiAuth.api";
import OpenApiAuthModal from './components/OpenApiAuthModal.vue'
import AuthModal from './components/AuthModal.vue'
import { useUserStore } from '/@/store/modules/user';
import JSearchSelect from "../../components/Form/src/jeecg/components/JSearchSelect.vue";
const formRef = ref();
const queryParam = reactive<any>({});
const toggleSearchStatus = ref<boolean>(false);
const registerModal = ref();
const authModal = ref();
const userStore = useUserStore();
//注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '授权管理',
api: list,
columns,
canResize:false,
useSearchForm: false,
actionColumn: {
width: 200,
fixed: 'right',
},
beforeFetch: async (params) => {
return Object.assign(params, queryParam);
},
},
exportConfig: {
name: "授权管理",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
});
const [registerTable, { reload, updateTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs:24,
sm:10,
xl:6,
xxl:10
});
const wrapperCol = reactive({
xs: 24,
sm: 20,
});
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
searchQuery();
}
/**
* 新增事件
*/
function handleAdd() {
registerModal.value.disableSubmit = false;
registerModal.value.add();
}
/**
* 编辑事件
*/
function handleAuth(record: Recordable) {
authModal.value.disableSubmit = false;
authModal.value.edit(record);
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
registerModal.value.disableSubmit = false;
registerModal.value.authDrawerOpen = true;
registerModal.value.edit(record);
}
/**
* 重置事件
* @param record
*/
async function handleReset(record: Recordable) {
const AKSKObj = await getGenAKSK({});
record.ak = AKSKObj[0];
record.sk = AKSKObj[1];
saveOrUpdate(record,true);
// handleSuccess;
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
registerModal.value.disableSubmit = true;
registerModal.value.edit(record);
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '授权',
onClick: handleAuth.bind(null, record),
auth: 'openapi:open_api_auth:edit'
},
{
label: '重置',
popConfirm: {
title: '是否重置AK,SK',
confirm: handleReset.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:edit'
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft',
},
auth: 'openapi:open_api_auth:delete'
}
]
}
/**
* 查询
*/
function searchQuery() {
reload();
}
/**
* 重置
*/
function searchReset() {
formRef.value.resetFields();
selectedRowKeys.value = [];
//刷新数据
reload();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
padding: 0;
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
.query-group-cust{
min-width: 100px !important;
}
.query-group-split-cust{
width: 30px;
display: inline-block;
text-align: center
}
.ant-form-item:not(.ant-form-item-with-help){
margin-bottom: 16px;
height: 32px;
}
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
}
</style>

View File

@ -0,0 +1,217 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection" @expand="handleExpand">
<!-- 内嵌table区域 begin -->
<!-- <template #expandedRowRender="{record}">-->
<!-- <a-tabs tabPosition="top">-->
<!-- <a-tab-pane tab="请求头表" key="openApiHeader" forceRender>-->
<!-- <openApiHeaderSubTable v-if="expandedRowKeys.includes(record.id)" :id="record.id" />-->
<!-- </a-tab-pane>-->
<!-- <a-tab-pane tab="请求参数部分" key="openApiParam" forceRender>-->
<!-- <openApiParamSubTable v-if="expandedRowKeys.includes(record.id)" :id="record.id" />-->
<!-- </a-tab-pane>-->
<!-- </a-tabs>-->
<!-- </template>-->
<!-- 内嵌table区域 end -->
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'openapi:open_api:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'openapi:open_api:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'openapi:open_api:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'openapi:open_api:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<OpenApiModal @register="registerModal" @success="handleSuccess"></OpenApiModal>
</div>
</template>
<script lang="ts" name="openapi-openApi" setup>
import {ref, reactive, computed, unref} from 'vue';
import {BasicTable, useTable, TableAction} from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage'
import {useModal} from '/@/components/Modal';
import OpenApiModal from './components/OpenApiModal.vue'
import OpenApiHeaderSubTable from './subTables/OpenApiHeaderSubTable.vue'
import OpenApiParamSubTable from './subTables/OpenApiParamSubTable.vue'
import {columns, searchFormSchema, superQuerySchema} from './OpenApi.data';
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './OpenApi.api';
import {downloadFile} from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
const queryParam = reactive<any>({});
// 展开key
const expandedRowKeys = ref<any[]>([]);
//注册model
const [registerModal, {openModal}] = useModal();
const userStore = useUserStore();
//注册table数据
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
title: '接口管理',
api: list,
columns,
canResize:false,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter:true,
showAdvancedButton:true,
fieldMapToNumber: [
],
fieldMapToTime: [
],
},
actionColumn: {
width: 120,
fixed:'right'
},
beforeFetch: (params) => {
return Object.assign(params, queryParam);
},
},
exportConfig: {
name:"接口管理",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
})
const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 展开事件
* */
function handleExpand(expanded, record){
expandedRowKeys.value=[];
if (expanded === true) {
expandedRowKeys.value.push(record.id)
}
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({id: record.id}, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ids: selectedRowKeys.value},handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'openapi:open_api:edit'
}
]
}
/**
* 下拉操作栏
*/
function getDropDownAction(record){
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft'
},
auth: 'openapi:open_api:delete'
}
]
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div ref="swaggerUiRef" style="height: 100%;"></div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
// 尝试直接导入 SwaggerUI 而不是使用 * as
import SwaggerUI from 'swagger-ui-dist/swagger-ui-bundle'; // 确保使用 ESM 版本
import 'swagger-ui-dist/swagger-ui.css';
import { getOpenApiJson } from './OpenApi.api';
const swaggerUiRef = ref<HTMLElement | null>(null);
const API_DOMAIN = import.meta.env.VITE_GLOB_DOMAIN_URL
onMounted(async () => {
try {
const response = await getOpenApiJson();
const openApiJson = response;
if (swaggerUiRef.value) {
SwaggerUI({
domNode: swaggerUiRef.value,
spec: openApiJson,
});
}
} catch (error) {
console.error('Failed to fetch OpenAPI JSON:', error);
}
});
</script>
<style scoped>
/* 确保容器有高度 */
.swagger-ui-container {
height: 100%;
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<a-spin :spinning="confirmLoading">
<a-row :span="24" style="margin-bottom: 10px">
<a-col :span="12" v-for="item in apiList" @click="handleSelect(item)">
<a-card :style="item.checked ? { border: '1px solid #3370ff' } : {}" hoverable class="checkbox-card" :body-style="{ width: '100%', padding: '10px' }">
<div class="checkbox-name" style="display: flex; width: 100%; justify-content: space-between">
<span>接口名称: {{ item.name }}</span>
<a-checkbox v-model:checked="item.checked" @click.stop class="quantum-checker" @change="(e) => handleChange(e, item)"> </a-checkbox>
</div>
<div class="checkbox-name" style="margin-top: 4px">
请求方式: <span>{{item.requestMethod}}</span>
</div>
</a-card>
</a-col>
</a-row>
<Pagination
v-if="apiList.length > 0"
:current="pageNo"
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:total="total"
:showQuickJumper="true"
:showSizeChanger="true"
@change="handlePageChange"
class="list-footer"
size="small"
/>
</a-spin>
</template>
<script lang="ts" setup>
import { computed, defineExpose, defineProps, nextTick, reactive, ref } from "vue";
import { useMessage } from '/@/hooks/web/useMessage';
import { getApiList, getPermissionList, permissionAddFunction } from '../OpenApiAuth.api';
import { Form, Pagination } from 'ant-design-vue';
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => ({}) },
formBpm: { type: Boolean, default: true },
});
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const { createMessage } = useMessage();
const confirmLoading = ref<boolean>(false);
//认证ID
const apiAuthId = ref<string>('');
//表单验证
const validatorRules = reactive({});
//api列表
const apiList = ref<any>([]);
//选中的值
const selectedRowKeys = ref<any>([]);
//选中的数据
const selectedRows = ref<any>([]);
//当前页数
const pageNo = ref<number>(1);
//每页条数
const pageSize = ref<number>(10);
//总条数
const total = ref<number>(0);
//可选择的页数
const pageSizeOptions = ref<any>(['10', '20', '30']);
// 表单禁用
const disabled = computed(() => {
if (props.formBpm === true) {
if (props.formData.disabled === false) {
return false;
} else {
return true;
}
}
return props.formDisabled;
});
/**
* 加载数据
*/
function reload() {
getApiList({ pageNo: pageNo.value, pageSize: pageSize.value, column: 'createTime', order: 'desc'}).then((res)=>{
if (res.success) {
for (const item of res.result.records) {
item.checked = false;
}
apiList.value = res.result.records;
total.value = res.result.total;
setChecked();
} else {
apiList.value = [];
total.value = 0;
}
});
}
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
async function edit(record) {
selectedRowKeys.value = [];
selectedRows.value = [];
pageNo.value = 1;
pageSize.value = 10;
apiAuthId.value = record.id;
await nextTick(() => {
// 获取当前已授权的项目
getPermissionList({ apiAuthId: record.id }).then((res) => {
if (res.length > 0) {
res.forEach((item) => {
if(item.ifCheckBox == "1"){
selectedRowKeys.value.push(item.id);
selectedRows.value.push(item);
}
});
//设置选中
setChecked();
}
});
reload();
});
}
/**
* 提交数据
*/
async function submitForm() {
confirmLoading.value = true;
//时间格式化
let model = {};
let apiId = ""
selectedRowKeys.value.forEach((item) => {
apiId += item +",";
})
model['apiId'] = apiId;
model['apiAuthId'] = apiAuthId.value;
await permissionAddFunction(model)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
cleanData()
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
const cleanData = () => {
selectedRows.value = []
selectedRowKeys.value = []
};
/**
* 复选框选中事件
* @param item
*/
function handleSelect(item) {
let id = item.id;
const target = apiList.value.find((item) => item.id === id);
if (target) {
target.checked = !target.checked;
}
//存放选中的知识库的id
if (!selectedRowKeys.value || selectedRowKeys.value.length == 0) {
selectedRowKeys.value.push(id);
selectedRows.value.push(item);
return;
}
let findIndex = selectedRowKeys.value.findIndex((item) => item === id);
if (findIndex === -1) {
selectedRowKeys.value.push(id);
selectedRows.value.push(item);
} else {
selectedRowKeys.value.splice(findIndex, 1);
selectedRows.value.splice(findIndex, 1);
}
}
/**
* 复选框选中事件
*
* @param e
* @param item
*/
function handleChange(e, item: any) {
if (e.target.checked) {
selectedRowKeys.value.push(item.id);
selectedRows.value.push(item);
} else {
let findIndex = selectedRowKeys.value.findIndex((val) => val === item.id);
if (findIndex != -1) {
selectedRowKeys.value.splice(findIndex, 1);
selectedRows.value.splice(findIndex, 1);
}
}
}
/**
* 分页改变事件
* @param page
* @param current
*/
function handlePageChange(page, current) {
pageNo.value = page;
pageSize.value = current;
reload();
}
/**
* 设置选装状态
*/
function setChecked() {
if (apiList.value && apiList.value.length > 0){
let value = selectedRowKeys.value.join(',');
apiList.value = apiList.value.map((item) => {
if (value.indexOf(item.id) !== -1) {
item.checked = true;
} else {
item.checked = false;
}
return item;
});
}
}
defineExpose({
add,
edit,
submitForm,
cleanData
});
</script>
<style lang="less" scoped>
.antd-modal-form {
padding: 14px;
}
.list-footer {
position: absolute;
bottom: -22px;
right: 10px;
text-align: center;
}
.checkbox-card {
margin-bottom: 10px;
margin-right: 10px;
}
.checkbox-img {
width: 30px;
height: 30px;
}
.checkbox-name {
margin-left: 4px;
font-size: 13px;
}
.use-select {
color: #646a73;
position: absolute;
bottom: 0;
left: 20px;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<!-- <j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">-->
<div style="position: relative;">
<a-modal
v-model:open="authDrawerOpen"
class="custom-class"
root-class-name="root-class-name"
:root-style="{ color: 'blue' }"
:body-style="{ padding: '20px' }"
style="color: red"
:title="title"
:width="600"
@after-open-change="authDrawerOpenChange"
@ok="handleOk"
>
<AuthForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></AuthForm>
</a-modal>
</div>
<!-- </j-modal>-->
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import AuthForm from './AuthForm.vue';
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
const authDrawerOpen = ref(false);
const authDrawerOpenChange = (val: any) => {
if(!val)
registerForm.value.cleanData()
};
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 授权
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '授权';
visible.value = true;
authDrawerOpen.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
authDrawerOpen.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style lang="less">
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
<style lang="less" scoped></style>

View File

@ -0,0 +1,175 @@
<template>
<a-spin :spinning="confirmLoading">
<JFormContainer :disabled="disabled">
<template #detail>
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="OpenApiAuthForm">
<a-row>
<a-col :span="24">
<a-form-item label="授权名称" v-bind="validateInfos.name" id="OpenApiAuthForm-name" name="name">
<a-input v-model:value="formData.name" placeholder="请输入授权名称" allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="AK" v-bind="validateInfos.ak" id="OpenApiAuthForm-ak" name="ak">
<a-input v-model:value="formData.ak" placeholder="请输入AK" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="SK" v-bind="validateInfos.sk" id="OpenApiAuthForm-sk" name="sk">
<a-input v-model:value="formData.sk" placeholder="请输入SK" disabled allow-clear ></a-input>
</a-form-item>
</a-col>
<!-- <a-col :span="24">-->
<!-- <a-form-item label="关联系统用户名" v-bind="validateInfos.systemUserId" id="OpenApiAuthForm-systemUserId" name="systemUserId">-->
<!-- <JSearchSelect dict="sys_user,username,id" v-model:value="formData.systemUserId" placeholder="请输入关联系统用户名" allow-clear ></JSearchSelect>-->
<!-- </a-form-item>-->
<!-- </a-col>-->
</a-row>
</a-form>
</template>
</JFormContainer>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed, } from 'vue';
import { USER_INFO_KEY} from '/@/enums/cacheEnum';
import { useMessage } from '/@/hooks/web/useMessage';
import { getValueType } from '/@/utils';
import { saveOrUpdate,getGenAKSK } from '../OpenApiAuth.api';
import { Form } from 'ant-design-vue';
import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
import JSearchSelect from '/@/components/Form/src/jeecg/components/JSearchSelect.vue';
import { getAuthCache } from "@/utils/auth";
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => ({})},
formBpm: { type: Boolean, default: true },
title: { type: String, default: "" },
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
id: '',
name: '',
ak: '',
sk: '',
systemUserId: '',
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
//表单验证
const validatorRules = reactive({
name:[{ required: true, message: '请输入授权名称!'},],
systemUserId:[{ required: true, message: '请输入关联系统用户名!'},],
});
const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
// 表单禁用
const disabled = computed(()=>{
if(props.formBpm === true){
if(props.formData.disabled === false){
return false;
}else{
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
async function add() {
edit({});
const AKSKObj = await getGenAKSK({});
formData.ak = AKSKObj[0];
formData.sk = AKSKObj[1];
}
/**
* 编辑
*/
function edit(record) {
const userData = getAuthCache(USER_INFO_KEY)
if(props.title == "新增"){
record.systemUserId = userData.id
}
nextTick(() => {
resetFields();
const tmpData = {};
Object.keys(formData).forEach((key) => {
if(record.hasOwnProperty(key)){
tmpData[key] = record[key]
}
})
//赋值
Object.assign(formData, tmpData);
});
}
/**
* 提交数据
*/
async function submitForm() {
try {
// 触发表单验证
await validate();
} catch ({ errorFields }) {
if (errorFields) {
const firstField = errorFields[0];
if (firstField) {
formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
return Promise.reject(errorFields);
}
confirmLoading.value = true;
const isUpdate = ref<boolean>(false);
//时间格式化
let model = formData;
if (model.id) {
isUpdate.value = true;
}
//循环数据
for (let data in model) {
//如果该数据是数组并且是字符串类型
if (model[data] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, data);
//如果是字符串类型的需要变成以逗号分割的字符串
if (valueType === 'string') {
model[data] = model[data].join(',');
}
}
}
await saveOrUpdate(model, isUpdate.value)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
defineExpose({
add,
edit,
submitForm,
});
</script>
<style lang="less" scoped>
.antd-modal-form {
padding: 14px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<j-modal :title="title" :width="width" :maxHeight="200" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<OpenApiAuthForm ref="registerForm" @ok="submitCallback" :title="title" :formDisabled="disableSubmit" :formBpm="false"></OpenApiAuthForm>
</j-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import OpenApiAuthForm from './OpenApiAuthForm.vue'
import JModal from '/@/components/Modal/src/JModal/JModal.vue';
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style lang="less">
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
<style lang="less" scoped></style>

View File

@ -0,0 +1,178 @@
<template>
<BasicModal :bodyStyle="{ padding: '20px' }" v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" width="80%" @ok="handleSubmit">
<a-row :gutter="24">
<a-col :span="10">
<BasicForm @register="registerForm" ref="formRef" name="OpenApiForm" />
</a-col>
<a-col :span="14">
<a-row :gutter="24">
<a-col :span="24" style="margin-top: -0.6em">
<JVxeTable
keep-source
ref="openApiHeader"
:loading="openApiHeaderTable.loading"
:columns="openApiHeaderTable.columns"
:dataSource="openApiHeaderTable.dataSource"
:height="240"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
size="mini"
/>
</a-col>
<a-col :span="24">
<JVxeTable
keep-source
ref="openApiParam"
:loading="openApiParamTable.loading"
:columns="openApiParamTable.columns"
:dataSource="openApiParamTable.dataSource"
:height="240"
:disabled="formDisabled"
:rowNumber="true"
:rowSelection="true"
:toolbar="true"
size="mini"
/>
</a-col>
</a-row>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { JVxeTable } from '/@/components/jeecg/JVxeTable';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts';
import { formSchema, openApiHeaderJVxeColumns, openApiParamJVxeColumns } from '../OpenApi.data';
import { saveOrUpdate, queryOpenApiHeader, queryOpenApiParam, getGenPath } from '../OpenApi.api';
import { VALIDATE_FAILED } from '/@/utils/common/vxeUtils';
import { useMessage } from "@/hooks/web/useMessage";
// Emits声明
const $message = useMessage();
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const formDisabled = ref(false);
const refKeys = ref(['openApiHeader', 'openApiParam']);
const activeKey = ref('openApiHeader');
const openApiHeader = ref();
const openApiParam = ref();
const tableRefs = { openApiHeader, openApiParam };
const openApiHeaderTable = reactive({
loading: false,
dataSource: [],
columns: openApiHeaderJVxeColumns,
});
const openApiParamTable = reactive({
loading: false,
dataSource: [],
columns: openApiParamJVxeColumns,
});
//表单配置
const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
wrapperCol: { span: 24 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await reset();
setModalProps({ confirmLoading: false, showCancelBtn: data?.showFooter, showOkBtn: data?.showFooter });
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
// 请求后端接口获取数据
// requestSubTableData(queryOpenApiHeader, {id:data?.record?.id}, openApiHeaderTable)
// requestSubTableData(queryOpenApiParam, {id:data?.record?.id}, openApiParamTable)
openApiHeaderTable.dataSource = !!data.record.headersJson?JSON.parse(data.record.headersJson):[];
openApiParamTable.dataSource = !!data.record.paramsJson?JSON.parse(data.record.paramsJson):[];
} else {
// /openapi/genpath
const requestUrlObj = await getGenPath({});
await setFieldsValue({
requestUrl: requestUrlObj.result
});
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter });
});
//方法配置
const [handleChangeTabs, handleSubmit, requestSubTableData, formRef] = useJvxeMethod(
requestAddOrEdit,
classifyIntoFormData,
tableRefs,
activeKey,
refKeys
);
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
async function reset() {
await resetFields();
activeKey.value = 'openApiHeader';
openApiHeaderTable.dataSource = [];
openApiParamTable.dataSource = [];
}
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue);
return {
...main, // 展开
headersJson: allValues.tablesValue[0].tableData,
paramsJson: allValues.tablesValue[1].tableData,
};
}
//表单提交事件
async function requestAddOrEdit(values) {
let headersJson = !!values.headersJson?JSON.stringify(values.headersJson):null;
let paramsJson = !!values.headersJson?JSON.stringify(values.paramsJson):null;
try {
if (!!values.body){
try {
if (typeof JSON.parse(values.body)!='object'){
$message.createMessage.error("JSON格式化错误,请检查输入数据");
return;
}
} catch (e) {
$message.createMessage.error("JSON格式化错误,请检查输入数据");
return;
}
}
setModalProps({ confirmLoading: true });
values.headersJson = headersJson
values.paramsJson = paramsJson
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div>
<!--引用表格-->
<BasicTable bordered size="middle" :loading="loading" rowKey="id" :canResize="false" :columns="openApiHeaderColumns" :dataSource="dataSource" :pagination="false">
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
</div>
</template>
<script lang="ts" setup>
import {ref,watchEffect} from 'vue';
import {BasicTable} from '/@/components/Table';
import {openApiHeaderColumns} from '../OpenApi.data';
import {openApiHeaderList} from '../OpenApi.api';
import { downloadFile } from '/@/utils/common/renderUtils';
const props = defineProps({
id: {
type: String,
default: '',
},
})
const loading = ref(false);
const dataSource = ref([]);
watchEffect(() => {
props.id && loadData(props.id);
});
function loadData(id) {
dataSource.value = []
loading.value = true
openApiHeaderList({id}).then((res) => {
if (res.success) {
dataSource.value = res.result.records
}
}).finally(() => {
loading.value = false
})
}
</script>

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