【同步】BOOT 和 CLOUD 的功能

pull/254/head
YunaiV 2026-06-07 23:35:29 +08:00
parent 10035e1709
commit bccf7c79e5
154 changed files with 3611 additions and 720 deletions

View File

@ -11,7 +11,7 @@
Target Server Version : 80200 (8.2.0) Target Server Version : 80200 (8.2.0)
File Encoding : 65001 File Encoding : 65001
Date: 16/05/2026 06:24:32 Date: 31/05/2026 22:29:18
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@ -92,7 +92,7 @@ CREATE TABLE `infra_api_error_log` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE INDEX `idx_create_time`(`create_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 23883 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; ) ENGINE = InnoDB AUTO_INCREMENT = 23958 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
-- ---------------------------- -- ----------------------------
-- Records of infra_api_error_log -- Records of infra_api_error_log
@ -253,7 +253,7 @@ CREATE TABLE `infra_file` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2216 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; ) ENGINE = InnoDB AUTO_INCREMENT = 2233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
-- ---------------------------- -- ----------------------------
-- Records of infra_file -- Records of infra_file
@ -284,17 +284,17 @@ CREATE TABLE `infra_file_config` (
-- Records of infra_file_config -- Records of infra_file_config
-- ---------------------------- -- ----------------------------
BEGIN; BEGIN;
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '数据库(示例)', 1, '我是数据库', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClientConfig\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2022-03-15 23:56:24', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '数据库(示例)', 1, '我是数据库', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.db.DBFileClientConfig\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2022-03-15 23:56:24', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (22, '七牛存储器(示例)', 20, '请换成你自己的密钥!!!', b'1', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://test.yudao.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-01-13 22:11:12', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (22, '七牛存储器(示例)', 20, '请换成你自己的密钥!!!', b'1', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://test.yudao.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-01-13 22:11:12', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (24, '腾讯云存储(示例)', 20, '请换成你的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"https://cos.ap-shanghai.myqcloud.com\",\"domain\":\"http://tengxun-oss.iocoder.cn\",\"bucket\":\"aoteman-1255880240\",\"accessKey\":\"AKIDAF6WSh1uiIjwqtrOsGSN3WryqTM6cTMt\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:03:22', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (24, '腾讯云存储(示例)', 20, '请换成你的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"https://cos.ap-shanghai.myqcloud.com\",\"domain\":\"http://tengxun-oss.iocoder.cn\",\"bucket\":\"aoteman-1255880240\",\"accessKey\":\"AKIDAF6WSh1uiIjwqtrOsGSN3WryqTM6cTMt\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:03:22', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (25, '阿里云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"oss-cn-beijing.aliyuncs.com\",\"domain\":\"http://ali-oss.iocoder.cn\",\"bucket\":\"yunai-aoteman\",\"accessKey\":\"LTAI5tEQLgnDyjh3WpNcdMKA\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:47:08', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (25, '阿里云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"oss-cn-beijing.aliyuncs.com\",\"domain\":\"http://ali-oss.iocoder.cn\",\"bucket\":\"yunai-aoteman\",\"accessKey\":\"LTAI5tEQLgnDyjh3WpNcdMKA\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:47:08', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (26, '火山云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"tos-s3-cn-beijing.volces.com\",\"domain\":null,\"bucket\":\"yunai\",\"accessKey\":\"AKLTZjc3Zjc4MzZmMjU3NDk0ZTgxYmIyMmFkNTIwMDI1ZGE\",\"accessSecret\":\"X==\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:56:42', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (26, '火山云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"tos-s3-cn-beijing.volces.com\",\"domain\":null,\"bucket\":\"yunai\",\"accessKey\":\"AKLTZjc3Zjc4MzZmMjU3NDk0ZTgxYmIyMmFkNTIwMDI1ZGE\",\"accessSecret\":\"X==\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 16:56:42', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (27, '华为云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"obs.cn-east-3.myhuaweicloud.com\",\"domain\":\"\",\"bucket\":\"yudao\",\"accessKey\":\"PVDONDEIOTW88LF8DC4U\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 17:18:41', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (27, '华为云存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"obs.cn-east-3.myhuaweicloud.com\",\"domain\":\"\",\"bucket\":\"yudao\",\"accessKey\":\"PVDONDEIOTW88LF8DC4U\",\"accessSecret\":\"X\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 17:18:41', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (28, 'MinIO 存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://127.0.0.1:9000\",\"domain\":\"http://127.0.0.1:9000/yudao\",\"bucket\":\"yudao\",\"accessKey\":\"admin\",\"accessSecret\":\"password\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 17:43:10', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (28, 'MinIO 存储(示例)', 20, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://127.0.0.1:9000\",\"domain\":\"http://127.0.0.1:9000/yudao\",\"bucket\":\"yudao\",\"accessKey\":\"admin\",\"accessSecret\":\"password\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":true}', '1', '2024-11-09 17:43:10', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (29, '本地存储(示例)', 10, 'mac/linux 使用 /windows 使用 \\', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig\",\"basePath\":\"/Users/yunai/tmp/file\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2025-05-02 11:25:45', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (29, '本地存储(示例)', 10, 'mac/linux 使用 /windows 使用 \\', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig\",\"basePath\":\"/Users/yunai/tmp/file\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2025-05-02 11:25:45', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (30, 'SFTP 存储(示例)', 12, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig\",\"basePath\":\"/upload\",\"domain\":\"http://127.0.0.1:48080\",\"host\":\"127.0.0.1\",\"port\":2222,\"username\":\"foo\",\"password\":\"pass\"}', '1', '2025-05-02 16:34:10', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (30, 'SFTP 存储(示例)', 12, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig\",\"basePath\":\"/upload\",\"domain\":\"http://127.0.0.1:48080\",\"host\":\"127.0.0.1\",\"port\":2222,\"username\":\"foo\",\"password\":\"pass\"}', '1', '2025-05-02 16:34:10', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (34, '七牛云存储【私有】(示例)', 20, '请换成你自己的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://t151glocd.hn-bkt.clouddn.com\",\"bucket\":\"ruoyi-vue-pro-private\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-08-17 21:22:00', '1', '2025-11-24 20:57:14', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (34, '七牛云存储【私有】(示例)', 20, '请换成你自己的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://t151glocd.hn-bkt.clouddn.com\",\"bucket\":\"ruoyi-vue-pro-private\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-08-17 21:22:00', '1', '2026-05-17 14:05:15', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false,\"region\":\"1\"}', '1', '2025-10-02 14:32:12', '1', '2025-11-29 15:59:39', b'0'); INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false,\"region\":\"1\"}', '1', '2025-10-02 14:32:12', '1', '2026-05-17 14:05:15', b'0');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@ -313,7 +313,7 @@ CREATE TABLE `infra_file_content` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_config_id_path`(`config_id` ASC, `path` ASC) USING BTREE INDEX `idx_config_id_path`(`config_id` ASC, `path` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 286 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; ) ENGINE = InnoDB AUTO_INCREMENT = 288 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
-- ---------------------------- -- ----------------------------
-- Records of infra_file_content -- Records of infra_file_content
@ -462,7 +462,7 @@ CREATE TABLE `system_dict_data` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1061096 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; ) ENGINE = InnoDB AUTO_INCREMENT = 1061122 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
-- ---------------------------- -- ----------------------------
-- Records of system_dict_data -- Records of system_dict_data
@ -1095,6 +1095,8 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3037, 42, 'Vben5.0 Antdv Next Schema 模版', '42', 'infra_codegen_front_type', 0, '', '', '', '1', '2026-05-23 13:52:25', '1', '2026-05-23 13:52:25', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3038, 43, 'Vben5.0 Antdv Next 标准模版', '43', 'infra_codegen_front_type', 0, '', '', '', '1', '2026-05-23 13:52:25', '1', '2026-05-23 13:52:25', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
@ -1454,8 +1456,32 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061091, 1, '退货出库', '200', 'wms_shipment_order_type', 0, 'warning', '', '退货出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061091, 1, '退货出库', '200', 'wms_shipment_order_type', 0, 'warning', '', '退货出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061092, 2, '销售出库', '201', 'wms_shipment_order_type', 0, 'primary', '', '销售出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061092, 2, '销售出库', '201', 'wms_shipment_order_type', 0, 'primary', '', '销售出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061093, 3, '生产出库', '202', 'wms_shipment_order_type', 0, 'success', '', '生产出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061093, 3, '生产出库', '202', 'wms_shipment_order_type', 0, 'success', '', '生产出库', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061094, 1, '语音', '1', 'im_rtc_call_media_type', 0, '', '', '语音通话', 'admin', '2026-05-14 13:48:31', 'admin', '2026-05-14 13:48:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061096, 1, '语音', '1', 'im_rtc_call_media_type', 0, '', '', '语音通话', 'admin', '2026-05-16 11:34:50', 'admin', '2026-05-16 11:34:50', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061095, 2, '视频', '2', 'im_rtc_call_media_type', 0, '', '', '视频通话', 'admin', '2026-05-14 13:48:31', 'admin', '2026-05-14 13:48:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061097, 2, '视频', '2', 'im_rtc_call_media_type', 0, '', '', '视频通话', 'admin', '2026-05-16 11:34:50', 'admin', '2026-05-16 11:34:50', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061098, 1, '私聊', '1', 'im_rtc_call_conversation_type', 0, 'primary', '', '一对一私聊通话', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061099, 2, '群聊', '2', 'im_rtc_call_conversation_type', 0, 'success', '', '群内多人通话', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061100, 1, '创建', '10', 'im_rtc_call_status', 0, 'info', '', '通话已创建,等待接通', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061101, 2, '进行中', '20', 'im_rtc_call_status', 0, 'primary', '', '已有人接通,通话中', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061102, 3, '已结束', '30', 'im_rtc_call_status', 0, 'success', '', '通话结束', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061103, 1, '通话结束', '1', 'im_rtc_call_end_reason', 0, 'success', '', '接通后任一方主动挂断', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061104, 2, '已拒绝', '2', 'im_rtc_call_end_reason', 0, 'warning', '', '被叫接通前点拒接', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061105, 3, '已取消', '3', 'im_rtc_call_end_reason', 0, 'info', '', '主叫接通前主动取消', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061106, 4, '无人接听', '4', 'im_rtc_call_end_reason', 0, 'info', '', '振铃超时未接通', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061107, 5, '对方正忙', '5', 'im_rtc_call_end_reason', 0, 'warning', '', '对方在另一通话中', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061108, 6, '通话异常', '9', 'im_rtc_call_end_reason', 0, 'danger', '', '网络中断、设备失败等', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061109, 1, '发起人', '1', 'im_rtc_participant_role', 0, 'primary', '', '通话发起者', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061110, 2, '被邀请者', '2', 'im_rtc_participant_role', 0, 'info', '', '被邀请加入', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061111, 3, '主动加入者', '3', 'im_rtc_participant_role', 0, 'success', '', '群通话场景,旁观者主动加入', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061112, 1, '邀请中', '10', 'im_rtc_participant_status', 0, 'info', '', '已发出 invite等待响应', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061113, 2, '已加入', '20', 'im_rtc_participant_status', 0, 'primary', '', '已接通并进入房间', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061114, 3, '已拒绝', '30', 'im_rtc_participant_status', 0, 'warning', '', '接通前点拒接', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061115, 4, '未应答', '40', 'im_rtc_participant_status', 0, 'info', '', '通话已结束仍未应答', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061116, 5, '已离开', '50', 'im_rtc_participant_status', 0, 'success', '', '接通后挂断 / 离开', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061117, 1610, '通话开始', '1610', 'im_message_type', 0, 'info', '', '入消息流;私聊定向通知,群聊全员广播', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061118, 1611, '通话结束', '1611', 'im_message_type', 0, 'info', '', '入消息流;私聊准气泡,群聊系统 tip', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061119, 125, '素材', '125', 'im_message_type', 0, 'success', '', '频道运营推送的图文卡片消息', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061120, 1, '富文本', '1', 'im_channel_material_type', 0, 'primary', '', '', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061121, 2, '外链', '2', 'im_channel_material_type', 0, 'info', '', '', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@ -1475,7 +1501,7 @@ CREATE TABLE `system_dict_type` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1061092 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; ) ENGINE = InnoDB AUTO_INCREMENT = 1061099 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
-- ---------------------------- -- ----------------------------
-- Records of system_dict_type -- Records of system_dict_type
@ -1682,7 +1708,13 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061070, 'WMS 单据状态', 'wms_order_status', 0, 'WMS 单据状态', '1', '2026-05-12 13:40:29', '1', '2026-05-12 13:40:29', b'0', NULL); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061070, 'WMS 单据状态', 'wms_order_status', 0, 'WMS 单据状态', '1', '2026-05-12 13:40:29', '1', '2026-05-12 13:40:29', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061080, '入库单类型', 'wms_receipt_order_type', 0, 'WMS 入库单类型', '1', '2026-05-11 11:21:49', '1', '2026-05-12 13:40:29', b'0', NULL); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061080, '入库单类型', 'wms_receipt_order_type', 0, 'WMS 入库单类型', '1', '2026-05-11 11:21:49', '1', '2026-05-12 13:40:29', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061090, '出库单类型', 'wms_shipment_order_type', 0, 'WMS 出库单类型', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0', NULL); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061090, '出库单类型', 'wms_shipment_order_type', 0, 'WMS 出库单类型', '1', '2026-05-12 17:48:35', '1', '2026-05-12 17:48:35', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061091, 'IM 通话媒体类型', 'im_rtc_call_media_type', 0, NULL, 'admin', '2026-05-14 13:48:31', 'admin', '2026-05-14 13:48:31', b'0', NULL); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061092, 'IM 通话媒体类型', 'im_rtc_call_media_type', 0, NULL, 'admin', '2026-05-16 11:34:50', 'admin', '2026-05-16 11:34:50', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061093, 'IM 通话会话类型', 'im_rtc_call_conversation_type', 0, '1=私聊2=群聊', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061094, 'IM 通话状态', 'im_rtc_call_status', 0, '10=创建20=进行中30=已结束', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061095, 'IM 通话结束原因', 'im_rtc_call_end_reason', 0, '1=通话结束2=已拒绝3=已取消4=无人接听5=对方正忙9=通话异常', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061096, 'IM 通话参与角色', 'im_rtc_participant_role', 0, '1=发起人2=被邀请者3=主动加入者', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061097, 'IM 通话参与状态', 'im_rtc_participant_status', 0, '10=邀请中20=已加入30=已拒绝40=未应答50=已离开', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0', NULL);
INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1061098, 'IM 频道素材内容类型', 'im_channel_material_type', 0, '1=站内富文本 / 2=外链', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0', NULL);
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@ -1708,7 +1740,7 @@ CREATE TABLE `system_login_log` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_username`(`username` ASC) USING BTREE, INDEX `idx_username`(`username` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE INDEX `idx_create_time`(`create_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4557 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; ) ENGINE = InnoDB AUTO_INCREMENT = 4697 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
-- ---------------------------- -- ----------------------------
-- Records of system_login_log -- Records of system_login_log
@ -1841,7 +1873,7 @@ CREATE TABLE `system_menu` (
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6611 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; ) ENGINE = InnoDB AUTO_INCREMENT = 6735 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
-- ---------------------------- -- ----------------------------
-- Records of system_menu -- Records of system_menu
@ -2773,7 +2805,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5034, 'OTA 固件创建', 'iot:ota-firmware:create', 3, 2, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:21', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5034, 'OTA 固件创建', 'iot:ota-firmware:create', 3, 2, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:21', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5035, 'OTA 固件更新', 'iot:ota-firmware:update', 3, 3, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:29', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5035, 'OTA 固件更新', 'iot:ota-firmware:update', 3, 3, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:29', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5036, 'OTA 固件删除', 'iot:ota-firmware:delete', 3, 4, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:37', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5036, 'OTA 固件删除', 'iot:ota-firmware:delete', 3, 4, 5032, '', '', '', '', 0, b'1', b'1', b'1', '', '2025-06-30 07:50:29', '\"1\"', '2025-06-30 17:38:37', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5037, 'OTA 升级任务查询', 'iot:ota-task:create', 3, 11, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:56:56', '1', '2025-07-02 23:56:56', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5037, 'OTA 升级任务查询', 'iot:ota-task:query', 3, 11, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:56:56', '1', '2026-05-19 08:48:53', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5038, 'OTA 升级任务取消', 'iot:ota-task:cancel', 3, 13, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:57:26', '1', '2025-07-02 23:57:26', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5038, 'OTA 升级任务取消', 'iot:ota-task:cancel', 3, 13, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:57:26', '1', '2025-07-02 23:57:26', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5039, 'OTA 升级任务创建', 'iot:ota-task:create', 3, 12, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:57:52', '1', '2025-07-02 23:57:52', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5039, 'OTA 升级任务创建', 'iot:ota-task:create', 3, 12, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:57:52', '1', '2025-07-02 23:57:52', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5040, 'OTA 升级记录查询', 'iot:ota-task-record:query', 3, 21, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:58:30', '1', '2025-07-02 23:58:30', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5040, 'OTA 升级记录查询', 'iot:ota-task-record:query', 3, 21, 5032, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-07-02 23:58:30', '1', '2025-07-02 23:58:30', b'0');
@ -3270,6 +3302,24 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6582, '用户表情删除', 'im:manager:face-user-item:delete', 3, 20, 6580, '', '', '', '', 0, b'1', b'1', b'1', '', '2026-05-06 13:56:08', '', '2026-05-06 13:56:08', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6582, '用户表情删除', 'im:manager:face-user-item:delete', 3, 20, 6580, '', '', '', '', 0, b'1', b'1', b'1', '', '2026-05-06 13:56:08', '', '2026-05-06 13:56:08', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6600, '私聊管理', '', 1, 20, 6500, 'private', 'ep:chat-round', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-05-07 00:40:35', '1', '2026-05-07 00:40:35', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6600, '私聊管理', '', 1, 20, 6500, 'private', 'ep:chat-round', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-05-07 00:40:35', '1', '2026-05-07 00:40:35', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6610, '群聊管理', '', 1, 30, 6500, 'group', 'ep:chat-line-round', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-05-07 00:40:35', '1', '2026-05-07 00:40:35', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6610, '群聊管理', '', 1, 30, 6500, 'group', 'ep:chat-line-round', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-05-07 00:40:35', '1', '2026-05-07 00:40:35', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6611, '通话记录', '', 2, 40, 6500, 'rtc', 'ep:phone', 'im/manager/rtc/index', 'ImRtcCall', 0, b'1', b'1', b'1', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6612, '通话记录查询', 'im:manager:rtc:query', 3, 1, 6611, '', '', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6700, '频道管理', '', 1, 90, 6500, 'channel', 'ep:promotion', NULL, NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6710, '频道列表', '', 2, 1, 6700, 'list', 'ep:promotion', 'im/manager/channel/list/index', 'ImChannel', 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-19 09:44:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6711, '频道查询', 'im:manager:channel:query', 3, 1, 6710, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6712, '频道创建', 'im:manager:channel:create', 3, 2, 6710, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6713, '频道修改', 'im:manager:channel:update', 3, 3, 6710, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6714, '频道删除', 'im:manager:channel:delete', 3, 4, 6710, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6720, '频道素材', '', 2, 2, 6700, 'material', 'ep:document', 'im/manager/channel/material/index', 'ImChannelMaterial', 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-19 09:44:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6721, '素材查询', 'im:manager:channel-material:query', 3, 1, 6720, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6722, '素材创建', 'im:manager:channel-material:create', 3, 2, 6720, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6723, '素材修改', 'im:manager:channel-material:update', 3, 3, 6720, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6724, '素材删除', 'im:manager:channel-material:delete', 3, 4, 6720, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6730, '频道消息', '', 2, 3, 6700, 'message', 'ep:message', 'im/manager/channel/message/index', 'ImChannelMessage', 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-19 09:44:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6731, '消息查询', 'im:manager:channel-message:query', 3, 1, 6730, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6732, '立即推送', 'im:manager:channel-message:send', 3, 2, 6730, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6733, '消息删除', 'im:manager:channel-message:delete', 3, 3, 6730, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6734, '解散群', 'im:manager:group:dissolve', 3, 21, 6540, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2026-05-24 12:02:38', '1', '2026-05-24 12:02:38', b'0');
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------
@ -3392,7 +3442,7 @@ CREATE TABLE `system_oauth2_access_token` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 54457 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; ) ENGINE = InnoDB AUTO_INCREMENT = 57395 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
-- ---------------------------- -- ----------------------------
-- Records of system_oauth2_access_token -- Records of system_oauth2_access_token
@ -3518,7 +3568,7 @@ CREATE TABLE `system_oauth2_refresh_token` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2590 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; ) ENGINE = InnoDB AUTO_INCREMENT = 2728 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
-- ---------------------------- -- ----------------------------
-- Records of system_oauth2_refresh_token -- Records of system_oauth2_refresh_token
@ -3554,7 +3604,7 @@ CREATE TABLE `system_operate_log` (
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE, INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE INDEX `idx_create_time`(`create_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9194 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; ) ENGINE = InnoDB AUTO_INCREMENT = 9195 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
-- ---------------------------- -- ----------------------------
-- Records of system_operate_log -- Records of system_operate_log
@ -4952,16 +5002,16 @@ CREATE TABLE `system_users` (
INDEX `idx_mobile`(`mobile` ASC) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE,
INDEX `idx_email`(`email` ASC) USING BTREE, INDEX `idx_email`(`email` ASC) USING BTREE,
INDEX `idx_dept_id`(`dept_id` ASC) USING BTREE INDEX `idx_dept_id`(`dept_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 145 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表'; ) ENGINE = InnoDB AUTO_INCREMENT = 225 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
-- ---------------------------- -- ----------------------------
-- Records of system_users -- Records of system_users
-- ---------------------------- -- ----------------------------
BEGIN; BEGIN;
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/user/avatar/20251220/blob_1766215463801.jpg', 0, '0:0:0:0:0:0:0:1', '2026-05-15 22:24:39', 'admin', '2021-01-05 17:03:47', NULL, '2026-05-15 22:24:39', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/20260517/blob_1778998103688.png', 0, '0:0:0:0:0:0:0:1', '2026-05-31 21:54:49', 'admin', '2021-01-05 17:03:47', NULL, '2026-05-31 21:54:49', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-19 17:40:55', '', '2021-01-07 09:07:17', NULL, '2026-04-19 17:40:55', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-19 17:40:55', '', '2021-01-07 09:07:17', NULL, '2026-04-19 17:40:55', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$k/d6mc0nySN0i2udwcI8Ee8V5aM5OHixBRbQfXmPuFTUl3Zf/DBs.', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-27 13:19:27', '', '2021-01-13 23:50:35', NULL, '2026-04-27 13:19:27', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$k/d6mc0nySN0i2udwcI8Ee8V5aM5OHixBRbQfXmPuFTUl3Zf/DBs.', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-27 13:19:27', '', '2021-01-13 23:50:35', NULL, '2026-04-27 13:19:27', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-05-09 09:57:13', '', '2021-01-21 02:13:53', NULL, '2026-05-09 09:57:13', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-05-20 23:37:11', '', '2021-01-21 02:13:53', NULL, '2026-05-20 23:37:11', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2025-04-21 14:23:08', b'0', 118); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2025-04-21 14:23:08', b'0', 118);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2025-04-21 14:23:08', b'0', 119); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2025-04-21 14:23:08', b'0', 119);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2025-04-21 14:23:08', b'0', 120); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2025-04-21 14:23:08', b'0', 120);
@ -4977,7 +5027,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-04-08 13:09:07', '1', '2025-04-08 13:09:07', '1', '2025-05-14 19:11:48', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-04-08 13:09:07', '1', '2025-04-08 13:09:07', '1', '2025-05-14 19:11:48', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', NULL, '2025-12-02 13:23:11', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', NULL, '2025-12-02 13:23:11', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (143, 'a00001', '$2a$04$GhVHFviOw/SsTmiQtifHJesDYFlHMeGK7OWh7aGCCjGGVCmbHVAwa', 'a00001', NULL, 104, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-12-01 16:10:13', NULL, '2025-12-01 16:10:13', '1', '2025-12-05 21:34:05', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (143, 'a00001', '$2a$04$GhVHFviOw/SsTmiQtifHJesDYFlHMeGK7OWh7aGCCjGGVCmbHVAwa', 'a00001', NULL, 104, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-12-01 16:10:13', NULL, '2025-12-01 16:10:13', '1', '2025-12-05 21:34:05', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 116, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2025-12-15 15:55:54', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 104, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2026-05-31 21:52:48', b'0', 1);
COMMIT; COMMIT;
-- ---------------------------- -- ----------------------------

View File

@ -238,6 +238,11 @@ public class LocalDateTimeUtils {
/** /**
* N 0 * N 0
* <p>
* getLatestDays(3) [ 00:00, 00:00, 00:00]
*
* @param days
* @return LocalDateTime
*/ */
public static List<LocalDateTime> getLatestDays(int days) { public static List<LocalDateTime> getLatestDays(int days) {
LocalDateTime today = getToday(); LocalDateTime today = getToday();

View File

@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import java.net.URI; import java.net.URI;
import java.net.URLDecoder; import java.net.URLDecoder;
@ -55,11 +56,61 @@ public class HttpUtils {
* @return * @return
*/ */
public static String decodeUrlPath(String path) { public static String decodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格 // 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B"); String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8); return URLDecoder.decode(encoded, StandardCharsets.UTF_8);
} }
/**
* URL /
*
* @param path URL 20250602/xxx.pdf
* @return
*/
public static String encodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
String[] segments = path.split(StrUtil.SLASH, -1);
StringBuilder result = new StringBuilder(path.length());
for (int i = 0; i < segments.length; i++) {
if (i > 0) {
result.append(StrUtil.SLASH);
}
result.append(encodeUrlPathSegment(segments[i]));
}
return result.toString();
}
/**
* URL
*
* @param segment URL
* @return
*/
public static String encodeUrlPathSegment(String segment) {
return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8);
}
public static String removeUrlPathQueryAndFragment(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
int endIndex = path.length();
int queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
endIndex = queryIndex;
}
int fragmentIndex = path.indexOf('#');
if (fragmentIndex >= 0 && fragmentIndex < endIndex) {
endIndex = fragmentIndex;
}
return path.substring(0, endIndex);
}
public static String replaceUrlQuery(String url, String key, String value) { public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除;再添加 // 先移除;再添加
@ -202,6 +253,9 @@ public class HttpUtils {
/** /**
* WebSocket URL HTTP URLws:// → http://wss:// → https://;其它格式原样保留 * WebSocket URL HTTP URLws:// → http://wss:// → https://;其它格式原样保留
*
* @param url URL
* @return URL
*/ */
public static String wsUrlToHttp(String url) { public static String wsUrlToHttp(String url) {
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url; return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;

View File

@ -176,6 +176,9 @@ public class JsonUtils {
/** /**
* JSON Map null * JSON Map null
*
* @param text JSON
* @return Map
*/ */
public static Map<String, Object> parseMap(String text) { public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) { if (StrUtil.isEmpty(text)) {

View File

@ -81,6 +81,15 @@ public class StrUtils {
/** /**
* 便 / / * 便 / /
*
* "lao zhang"ZhangSan "zhangsan"
* / / null
*
* hutool-extra {@link PinyinUtil}
* pinyin4j / TinyPinyin / Bopomofo4j NoClassDefFoundError
*
* @param str
* @return
*/ */
public static String toPinyin(String str) { public static String toPinyin(String str) {
if (StrUtil.isBlank(str)) { if (StrUtil.isBlank(str)) {

View File

@ -9,6 +9,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
*/ */
public class HttpUtilsTest { public class HttpUtilsTest {
@Test
public void testEncodeUrlPath() {
// 准备参数
String path = "avatar/中文 100%+文件.jpg";
// 调用
String result = HttpUtils.encodeUrlPath(path);
// 断言
assertEquals("avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
}
@Test
public void testDecodeUrlPath() {
// 准备参数:+ 是路径字符,不应该按 query parameter 语义解码为空格
String path = "avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg";
// 调用
String result = HttpUtils.decodeUrlPath(path);
// 断言
assertEquals("avatar/中文 100%+文件.jpg", result);
}
@Test
public void testRemoveUrlPathQueryAndFragment() {
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg?token=1#preview"));
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg#preview?token=1"));
}
@Test @Test
public void testReplaceUrlQuery_replace() { public void testReplaceUrlQuery_replace() {
// 准备参数 // 准备参数

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.tenant.config; package cn.iocoder.yudao.framework.tenant.config;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi; import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
@ -31,7 +30,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies; import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheConfiguration;
@ -46,10 +44,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import java.util.HashSet; import java.util.*;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -63,13 +58,6 @@ public class YudaoTenantAutoConfiguration {
@Bean @Bean
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) { public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC6YZF
try {
TenantCommonApi tenantApiImpl = SpringUtil.getBean("tenantApiImpl", TenantCommonApi.class);
if (tenantApiImpl != null) {
tenantApi = tenantApiImpl;
}
} catch (Exception ignored) {}
return new TenantFrameworkServiceImpl(tenantApi); return new TenantFrameworkServiceImpl(tenantApi);
} }
@ -167,20 +155,9 @@ public class YudaoTenantAutoConfiguration {
// ========== MQ ========== // ========== MQ ==========
/** @Bean
* Redis public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
* return new TenantRedisMessageInterceptor();
* TenantRedisMessageInterceptor Bean RedisMessageInterceptor
*/
@Configuration
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate")
public static class TenantRedisMQAutoConfiguration {
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
}
} }
@Bean @Bean
@ -198,7 +175,6 @@ public class YudaoTenantAutoConfiguration {
// ========== Job ========== // ========== Job ==========
@Bean @Bean
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService); return new TenantJobAspect(tenantFrameworkService);
} }
@ -216,7 +192,12 @@ public class YudaoTenantAutoConfiguration {
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象 // 创建 TenantRedisCacheManager 对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches()); TenantRedisCacheManager cacheManager = new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration,
tenantProperties.getIgnoreCaches());
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
} }
} }

View File

@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient; import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -70,7 +69,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners, public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate, RedisMQTemplate redisTemplate,
RedissonClient redissonClient) { RedissonClient redissonClient) {
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient); return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient,
RedisPendingMessageResendJob.DEFAULT_RESEND_LOCK_KEY);
} }
/** /**
@ -81,7 +81,8 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners, public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate, RedisMQTemplate redisTemplate,
RedissonClient redissonClient) { RedissonClient redissonClient) {
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient); return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient,
RedisStreamMessageCleanupJob.DEFAULT_CLEANUP_LOCK_KEY);
} }
/** /**

View File

@ -23,7 +23,9 @@ import java.util.Objects;
@AllArgsConstructor @AllArgsConstructor
public class RedisPendingMessageResendJob { public class RedisPendingMessageResendJob {
private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock"; public static final String DEFAULT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock";
public static final String IOT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock:iot";
/** /**
* 5 * 5
@ -36,22 +38,26 @@ public class RedisPendingMessageResendJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners; private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate; private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
private final String resendLockKey;
/** /**
* , 35 * , 35
*/ */
@Scheduled(cron = "35 * * * * ?") @Scheduled(cron = "35 * * * * ?")
public void messageResend() { public void messageResend() {
RLock lock = redissonClient.getLock(LOCK_KEY); RLock lock = redissonClient.getLock(resendLockKey);
// 尝试加锁
if (lock.tryLock()) { if (lock.tryLock()) {
try { try {
execute(); execute();
} catch (Exception ex) { } catch (Exception ex) {
log.error("[messageResend][执行异常]", ex); log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex);
} finally { } finally {
lock.unlock(); if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
} }
} else {
log.debug("[messageResend][未获取到锁,跳过本轮][lockKey={}]", resendLockKey);
} }
} }

View File

@ -23,7 +23,16 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
public class RedisStreamMessageCleanupJob { public class RedisStreamMessageCleanupJob {
private static final String LOCK_KEY = "redis:stream:message-cleanup:lock"; /**
* MQSpring AbstractRedisStreamMessageListener使
*/
public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
/**
* IoT Redis 线使 {@link #DEFAULT_CLEANUP_LOCK_KEY}
* XTRIM Stream
*/
public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
/** /**
* 10000 * 10000
@ -33,22 +42,29 @@ public class RedisStreamMessageCleanupJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners; private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate; private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
/**
* Redisson Bean
*/
private final String cleanupLockKey;
/** /**
* *
*/ */
@Scheduled(cron = "0 0 * * * ?") @Scheduled(cron = "0 0 * * * ?")
public void cleanup() { public void cleanup() {
RLock lock = redissonClient.getLock(LOCK_KEY); RLock lock = redissonClient.getLock(cleanupLockKey);
// 尝试加锁
if (lock.tryLock()) { if (lock.tryLock()) {
try { try {
execute(); execute();
} catch (Exception ex) { } catch (Exception ex) {
log.error("[cleanup][执行异常]", ex); log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex);
} finally { } finally {
lock.unlock(); if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
} }
} else {
log.debug("[cleanup][未获取到锁,跳过本轮][lockKey={}]", cleanupLockKey);
} }
} }
@ -59,8 +75,8 @@ public class RedisStreamMessageCleanupJob {
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream(); StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
listeners.forEach(listener -> { listeners.forEach(listener -> {
try { try {
// 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息 // 使用 XTRIM MAXLEN 精确裁剪approximate=false避免 ~ 模式下长期明显高于上限
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true); Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, false);
if (trimCount != null && trimCount > 0) { if (trimCount != null && trimCount > 0) {
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount); log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
} }

View File

@ -20,51 +20,49 @@ public enum DbTypeEnum {
/** /**
* H2 * H2
*
* H2 find_in_set
*/ */
H2(DbType.H2, "H2", ""), H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/** /**
* MySQL * MySQL
*/ */
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"), MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/** /**
* Oracle * Oracle
*/ */
ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"), ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"),
/** /**
* PostgreSQL * PostgreSQL
* *
* openGauss 使 ProductName PostgreSQL * openGauss 使 ProductName PostgreSQL
*/ */
POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"), POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/** /**
* SQL Server * SQL Server
*/ */
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/** /**
* SQL Server 2005 * SQL Server 2005
*/ */
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"),
/** /**
* *
*/ */
DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"), DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"),
/** /**
* *
*/ */
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"), KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"),
/** /**
* OceanBase * OceanBase
*/ */
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0") OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0")
; ;
@ -95,7 +93,9 @@ public enum DbTypeEnum {
} }
public static String getFindInSetTemplate(DbType dbType) { public static String getFindInSetTemplate(DbType dbType) {
return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate()) return Optional.ofNullable(MAP_BY_MP.get(dbType))
.map(DbTypeEnum::getFindInSetTemplate)
.filter(StrUtil::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported")); .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
} }
} }

View File

@ -174,6 +174,9 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
* *
* <p> * <p>
* 使 selectOne * 使 selectOne
*
* @param queryWrapper
* @return null
*/ */
default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) { default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
return CollUtil.getLast(selectList(queryWrapper)); return CollUtil.getLast(selectList(queryWrapper));

View File

@ -34,6 +34,10 @@ public class MyBatisUtils {
private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$"); private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
public static <T> Page<T> buildPage(PageParam pageParam) { public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null); return buildPage(pageParam, null);
} }
@ -157,15 +161,43 @@ public class MyBatisUtils {
/** /**
* find_in_set * find_in_set
* *
* @param column * @param columnName
* @param value ()
* @return sql * @return sql
*/ */
public static String findInSet(String column, Object value) { public static String findInSet(String columnName) {
return findInSet(columnName, 0);
}
/**
* find_in_set apply
*
* @param columnName
* @param paramIndex apply
* @return sql
*/
public static String findInSetWithParamIndex(String columnName, int paramIndex) {
return findInSet(columnName, paramIndex);
}
private static String findInSet(String columnName, int paramIndex) {
DbType dbType = JdbcUtils.getDbType(); DbType dbType = JdbcUtils.getDbType();
return findInSet(dbType, columnName, paramIndex);
}
static String findInSet(DbType dbType, String columnName, int paramIndex) {
if (!isSafeColumnName(columnName)) {
throw new IllegalArgumentException("Invalid column name: " + columnName);
}
if (paramIndex < 0) {
throw new IllegalArgumentException("Invalid param index: " + paramIndex);
}
return DbTypeEnum.getFindInSetTemplate(dbType) return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column) .replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName)
.replace("#{value}", StrUtil.toString(value)); .replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}");
}
private static boolean isSafeColumnName(String columnName) {
return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
} }
/** /**

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.common.pojo.SortingField;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
@ -13,6 +14,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
@ -98,6 +100,71 @@ public class MyBatisUtilsTest {
assertEquals("DESC", MyBatisUtils.getOrderDirection(null)); assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
} }
@Test
public void testFindInSet() {
assertEquals("FIND_IN_SET({0}, websites) <> 0",
MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.H2, "websites", 0));
assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
}
@Test
public void testFindInSet_invalidColumnName() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
}
@Test
public void testFindInSet_invalidParamIndex() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
}
@Test
public void testFindInSet_applyBindsValue() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value = "test' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
// 断言SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value));
assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
}
@Test
public void testFindInSet_applyBindsMultipleValues() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value1 = "1' OR 1 = 1";
String value2 = "2' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
+ " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
// 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
+ " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value1));
assertFalse(query.getSqlSegment().contains(value2));
assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
}
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) { private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
assertEquals(column, orderItem.getColumn()); assertEquals(column, orderItem.getColumn());
assertEquals(asc, orderItem.isAsc()); assertEquals(asc, orderItem.isAsc());

View File

@ -75,8 +75,12 @@ public class YudaoCacheAutoConfiguration {
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象 // 创建 TimeoutRedisCacheManager 对象
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); TimeoutRedisCacheManager cacheManager = new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
} }
} }

View File

@ -76,7 +76,7 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
Long tenantId = WebSocketFrameworkUtils.getTenantId(session); Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
} catch (Throwable ex) { } catch (Throwable ex) {
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload(), ex);
} }
} }

View File

@ -27,9 +27,10 @@ public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) { WebSocketHandler wsHandler, Map<String, Object> attributes) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null) { if (loginUser == null) {
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); return false;
} }
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
return true; return true;
} }

View File

@ -84,7 +84,7 @@ public class UserProfileQueryToolFunction
request.setId(loginUser.getId()); request.setId(loginUser.getId());
} }
return TenantUtils.execute(tenantId, () -> { return TenantUtils.execute(tenantId, () -> {
AdminUserRespDTO user = adminUserApi.getUser(request.getId()).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(request.getId());
return BeanUtils.toBean(user, Response.class); return BeanUtils.toBean(user, Response.class);
}); });
} }

View File

@ -143,10 +143,10 @@ public class BpmProcessInstanceController {
processInstance.getProcessDefinitionId()); processInstance.getProcessDefinitionId());
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(
processInstance.getProcessDefinitionId()); processInstance.getProcessDefinitionId());
AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData(); AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
DeptRespDTO dept = null; DeptRespDTO dept = null;
if (startUser != null && startUser.getDeptId() != null) { if (startUser != null && startUser.getDeptId() != null) {
dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); dept = deptApi.getDept(startUser.getDeptId());
} }
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance, return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance,
processDefinition, processDefinitionInfo, startUser, dept)); processDefinition, processDefinitionInfo, startUser, dept));
@ -211,8 +211,8 @@ public class BpmProcessInstanceController {
if (historicProcessInstance == null) { if (historicProcessInstance == null) {
throw exception(PROCESS_INSTANCE_NOT_EXISTS); throw exception(PROCESS_INSTANCE_NOT_EXISTS);
} }
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())).getCheckedData(); AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId()));
DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); DeptRespDTO dept = deptApi.getDept(startUser.getDeptId());
List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId); List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap( Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(tasks, item -> Long.valueOf(item.getAssignee()))); convertSet(tasks, item -> Long.valueOf(item.getAssignee())));

View File

@ -57,7 +57,7 @@ public class BpmTaskAssignLeaderExpression {
return emptySet(); return emptySet();
} }
} else { } else {
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()).getCheckedData(); DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少 if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少
break; break;
} }
@ -68,11 +68,11 @@ public class BpmTaskAssignLeaderExpression {
} }
private DeptRespDTO getStartUserDept(Long startUserId) { private DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData(); AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则 if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则
return null; return null;
} }
return deptApi.getDept(startUser.getDeptId()).getCheckedData(); return deptApi.getDept(startUser.getDeptId());
} }
} }

View File

@ -40,7 +40,7 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
} }
DeptRespDTO currentDept = dept; DeptRespDTO currentDept = dept;
for (int i = 1; i < level; i++) { for (int i = 1; i < level; i++) {
DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId()).getCheckedData(); DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId());
if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人 if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人
break; break;
} }
@ -63,12 +63,12 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
} }
Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序 Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序
for (Long deptId : deptIds) { for (Long deptId : deptIds) {
DeptRespDTO dept = deptApi.getDept(deptId).getCheckedData(); DeptRespDTO dept = deptApi.getDept(deptId);
for (int i = 0; i < level; i++) { for (int i = 0; i < level; i++) {
if (dept.getLeaderUserId() != null) { if (dept.getLeaderUserId() != null) {
deptLeaderIds.add(dept.getLeaderUserId()); deptLeaderIds.add(dept.getLeaderUserId());
} }
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()).getCheckedData(); DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了 if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了
break; break;
} }
@ -84,11 +84,11 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
* @param startUserId Id * @param startUserId Id
*/ */
protected DeptRespDTO getStartUserDept(Long startUserId) { protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData(); AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门 if (startUser.getDeptId() == null) { // 找不到部门
return null; return null;
} }
return deptApi.getDept(startUser.getDeptId()).getCheckedData(); return deptApi.getDept(startUser.getDeptId());
} }
} }

View File

@ -30,7 +30,7 @@ public class BpmTaskCandidateDeptLeaderMultiStrategy extends AbstractBpmTaskCand
List<Long> deptIds = StrUtils.splitToLong(params[0], ","); List<Long> deptIds = StrUtils.splitToLong(params[0], ",");
int level = Integer.parseInt(params[1]); int level = Integer.parseInt(params[1]);
// 校验部门存在 // 校验部门存在
deptApi.validateDeptList(deptIds).checkError(); deptApi.validateDeptList(deptIds);
Assert.isTrue(level > 0, "部门层级必须大于 0"); Assert.isTrue(level > 0, "部门层级必须大于 0");
} }

View File

@ -32,13 +32,13 @@ public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrat
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
deptApi.validateDeptList(deptIds).checkError(); deptApi.validateDeptList(deptIds);
} }
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<DeptRespDTO> depts = deptApi.getDeptList(deptIds).getCheckedData(); List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
return convertSet(depts, DeptRespDTO::getLeaderUserId); return convertSet(depts, DeptRespDTO::getLeaderUserId);
} }

View File

@ -35,13 +35,13 @@ public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrat
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
deptApi.validateDeptList(deptIds).checkError(); deptApi.validateDeptList(deptIds);
} }
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(); List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds);
return convertSet(users, AdminUserRespDTO::getId); return convertSet(users, AdminUserRespDTO::getId);
} }

View File

@ -41,7 +41,7 @@ public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> postIds = StrUtils.splitToLongSet(param); Set<Long> postIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds).getCheckedData(); List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds);
return convertSet(users, AdminUserRespDTO::getId); return convertSet(users, AdminUserRespDTO::getId);
} }

View File

@ -28,7 +28,7 @@ public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
adminUserApi.validateUserList(StrUtils.splitToLongSet(param)).checkError(); adminUserApi.validateUserList(StrUtils.splitToLongSet(param));
} }
@Override @Override

View File

@ -456,6 +456,54 @@ public class BpmnModelUtils {
return new ArrayList<>(); return new ArrayList<>();
} }
/**
* source UserTask 线
*
* 1. 线 source UserTask线
* 2. 线 source UserTask
* 3. StartEvent SubProcess
*
* @param source
* @return UserTask 线
*/
public static List<SequenceFlow> getElementIncomingUserTaskFlows(FlowElement source) {
List<SequenceFlow> result = new ArrayList<>();
collectElementIncomingUserTaskFlows(source, new HashSet<>(), new HashSet<>(), result);
return result;
}
private static void collectElementIncomingUserTaskFlows(FlowElement source, Set<String> visitedSequenceFlowIds,
Set<String> resultSequenceFlowIds, List<SequenceFlow> result) {
// 如果是开始节点或子流程,则停止该分支向上查找
if (source == null || source instanceof StartEvent || source instanceof SubProcess) {
return;
}
// 获取入口连线
List<SequenceFlow> incomingFlows = getElementIncomingFlows(source);
if (CollUtil.isEmpty(incomingFlows)) {
return;
}
// 循环找到目标元素
for (SequenceFlow incomingFlow : incomingFlows) {
// 如果发现连线重复,说明连线已经走过。跳过
if (incomingFlow == null || !visitedSequenceFlowIds.add(incomingFlow.getId())) {
continue;
}
// 如果 source 是 UserTask则添加到结果中
FlowElement sourceFlowElement = incomingFlow.getSourceFlowElement();
if (sourceFlowElement instanceof UserTask) {
if (resultSequenceFlowIds.add(incomingFlow.getId())) {
result.add(incomingFlow);
}
continue;
}
// 递归向上查找 UserTask
collectElementIncomingUserTaskFlows(sourceFlowElement, visitedSequenceFlowIds,
resultSequenceFlowIds, result);
}
}
/** /**
* 线 * 线
* *
@ -673,7 +721,7 @@ public class BpmnModelUtils {
// 这条线路存在目标节点,直接返回 true // 这条线路存在目标节点,直接返回 true
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
if (target.getId().equals(sourceFlowElement.getId())) { if (target.getId().equals(sourceFlowElement.getId())) {
return true; return true;
} }
// 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?) // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
if (sourceFlowElement instanceof ParallelGateway) { if (sourceFlowElement instanceof ParallelGateway) {
@ -798,9 +846,9 @@ public class BpmnModelUtils {
// 情况StartEvent/EndEvent/UserTask/ServiceTask // 情况StartEvent/EndEvent/UserTask/ServiceTask
if (currentElement instanceof StartEvent if (currentElement instanceof StartEvent
|| currentElement instanceof EndEvent || currentElement instanceof EndEvent
|| currentElement instanceof UserTask || currentElement instanceof UserTask
|| currentElement instanceof ServiceTask) { || currentElement instanceof ServiceTask) {
// 添加节点 // 添加节点
FlowNode flowNode = (FlowNode) currentElement; FlowNode flowNode = (FlowNode) currentElement;
resultElements.add(flowNode); resultElements.add(flowNode);
@ -982,8 +1030,8 @@ public class BpmnModelUtils {
*/ */
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) { private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression()))); && (evalConditionExpress(variables, flow.getConditionExpression())));
if (matchSequenceFlow == null) { if (matchSequenceFlow == null) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));

View File

@ -101,7 +101,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 校验用户是否在允许发起的部门列表中 // 校验用户是否在允许发起的部门列表中
if (CollUtil.isNotEmpty(processDefinition.getStartDeptIds())) { if (CollUtil.isNotEmpty(processDefinition.getStartDeptIds())) {
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(userId);
return user != null return user != null
&& user.getDeptId() != null && user.getDeptId() != null
&& processDefinition.getStartDeptIds().contains(user.getDeptId()); && processDefinition.getStartDeptIds().contains(user.getDeptId());

View File

@ -872,7 +872,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) { if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) {
return definition.getName(); return definition.getName();
} }
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(userId);
Map<String, Object> cloneVariables = new HashMap<>(variables); Map<String, Object> cloneVariables = new HashMap<>(variables);
cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname());
cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now());
@ -920,7 +920,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
} }
// 2. 取消流程 // 2. 取消流程
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(userId);
updateProcessInstanceCancel(cancelReqVO.getId(), updateProcessInstanceCancel(cancelReqVO.getId(),
BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason()));
} }

View File

@ -618,7 +618,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn); runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn);
} }
// 6. 调用 BPM complete 去完成任务 // 6. 清理退回设置的不自动通过的变量。仅在该标记存在时才删除,避免每次完成任务都产生无谓的 DB delete
String returnFlagKey = String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey());
if (runtimeService.hasVariable(task.getProcessInstanceId(), returnFlagKey)) {
log.info("[approveTask][taskId({}) 清理退回标记变量({})]", task.getId(), returnFlagKey);
runtimeService.removeVariable(task.getProcessInstanceId(), returnFlagKey);
}
// 7. 调用 BPM complete 去完成任务
taskService.complete(task.getId(), variables, true); taskService.complete(task.getId(), variables, true);
// 【加签专属】处理加签任务 // 【加签专属】处理加签任务
@ -780,8 +787,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/ */
private void approveDelegateTask(BpmTaskApproveReqVO reqVO, Task task) { private void approveDelegateTask(BpmTaskApproveReqVO reqVO, Task task) {
// 1. 添加审批意见 // 1. 添加审批意见
AdminUserRespDTO currentUser = adminUserApi.getUser(WebFrameworkUtils.getLoginUserId()).getCheckedData(); AdminUserRespDTO currentUser = adminUserApi.getUser(WebFrameworkUtils.getLoginUserId());
AdminUserRespDTO ownerUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())).getCheckedData(); // 发起委托的用户 AdminUserRespDTO ownerUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())); // 发起委托的用户
Assert.notNull(ownerUser, "委派任务找不到原审批人,需要检查数据"); Assert.notNull(ownerUser, "委派任务找不到原审批人,需要检查数据");
taskService.addComment(reqVO.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(), taskService.addComment(reqVO.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(),
BpmCommentTypeEnum.DELEGATE_END.formatComment(currentUser.getNickname(), ownerUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.DELEGATE_END.formatComment(currentUser.getNickname(), ownerUser.getNickname(), reqVO.getReason()));
@ -914,7 +921,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @param reqVO * @param reqVO
*/ */
public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
// 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveActivityIdsToSingleActivityId 回撤 // 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveExecutionsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key // 1.1 获取所有正常进行的任务节点 Key
List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey); List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey);
@ -922,14 +929,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 为什么不直接使用 runTaskKeyList 呢因为可能存在多个审批分支例如说A -> B -> C 和 D -> F而只要 C 撤回到 A需要排除掉 F // 为什么不直接使用 runTaskKeyList 呢因为可能存在多个审批分支例如说A -> B -> C 和 D -> F而只要 C 撤回到 A需要排除掉 F
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId); List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
List<String> runExecutionIds = new ArrayList<>();
// 2. 给当前要被退回的 task 数组,设置退回意见 // 2. 给当前要被退回的 task 数组,设置退回意见
taskList.forEach(task -> { taskList.forEach(task -> {
// 需要排除掉,不需要设置退回意见的任务 // 需要排除掉,不需要设置退回意见的任务
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return; return;
} }
if (task.getExecutionId() != null) {
runExecutionIds.add(task.getExecutionId());
}
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
// 2.1.1 添加评论 // 2.1.1 添加评论
@ -946,18 +955,25 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement); Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
// 4. 执行驳回 // 4. 执行驳回
// 4.1 校验是否有可回撤的 execution避免 moveExecutionsToSingleActivityId 传入空集合时 Flowable 内部报错
if (CollUtil.isEmpty(runExecutionIds)) {
throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
}
// 4.2 执行驳回
// ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因当多实例任务回退的时候有问题。 // ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因当多实例任务回退的时候有问题。
// 相关 issue: https://github.com/flowable/flowable-engine/issues/3944 // 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
// ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因flowable 7.2.0 版本修复了该问题。 // ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因flowable 7.2.0 版本修复了该问题。
// 相关 issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/1018 // 相关 issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/1018
// ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS
// 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId
// 目前还有的相关问题 https://t.zsxq.com/z4d9i。 估计需要升级 flowable 8 版本试试
runtimeService.createChangeActivityStateBuilder() runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId()) .processInstanceId(currentTask.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey()) .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务 ids 的流程变量,用于辅助预测 // 设置需要预测的任务 ids 的流程变量,用于辅助预测
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys) .processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
// 设置流程变量local节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过 // 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
.localVariable(reqVO.getTargetTaskDefinitionKey(), .processVariable(String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
.changeState(); .changeState();
} }
@ -999,13 +1015,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
throw exception(TASK_DELEGATE_FAIL_USER_REPEAT); throw exception(TASK_DELEGATE_FAIL_USER_REPEAT);
} }
// 1.2 校验目标用户存在 // 1.2 校验目标用户存在
AdminUserRespDTO delegateUser = adminUserApi.getUser(reqVO.getDelegateUserId()).getCheckedData(); AdminUserRespDTO delegateUser = adminUserApi.getUser(reqVO.getDelegateUserId());
if (delegateUser == null) { if (delegateUser == null) {
throw exception(TASK_DELEGATE_FAIL_USER_NOT_EXISTS); throw exception(TASK_DELEGATE_FAIL_USER_NOT_EXISTS);
} }
// 2. 添加委托意见 // 2. 添加委托意见
AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO currentUser = adminUserApi.getUser(userId);
taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(), taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(),
BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason()));
@ -1030,13 +1046,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
throw exception(TASK_TRANSFER_FAIL_USER_REPEAT); throw exception(TASK_TRANSFER_FAIL_USER_REPEAT);
} }
// 1.2 校验目标用户存在 // 1.2 校验目标用户存在
AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId()).getCheckedData(); AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId());
if (assigneeUser == null) { if (assigneeUser == null) {
throw exception(TASK_TRANSFER_FAIL_USER_NOT_EXISTS); throw exception(TASK_TRANSFER_FAIL_USER_NOT_EXISTS);
} }
// 2. 添加委托意见 // 2. 添加委托意见
AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO currentUser = adminUserApi.getUser(userId);
taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(), taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(),
BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason()));
@ -1095,7 +1111,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) { public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) {
// 1. 获取和校验任务 // 1. 获取和校验任务
TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO); TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO);
List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds()).getCheckedData(); List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds());
if (CollUtil.isEmpty(userList)) { if (CollUtil.isEmpty(userList)) {
throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST); throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST);
} }
@ -1122,7 +1138,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
createSignTaskList(convertList(reqVO.getUserIds(), String::valueOf), taskEntity); createSignTaskList(convertList(reqVO.getUserIds(), String::valueOf), taskEntity);
// 4. 记录加签的评论到 task 任务 // 4. 记录加签的评论到 task 任务
AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO currentUser = adminUserApi.getUser(userId);
String comment = StrUtil.format(BpmCommentTypeEnum.ADD_SIGN.getComment(), String comment = StrUtil.format(BpmCommentTypeEnum.ADD_SIGN.getComment(),
currentUser.getNickname(), BpmTaskSignTypeEnum.nameOfType(reqVO.getType()), currentUser.getNickname(), BpmTaskSignTypeEnum.nameOfType(reqVO.getType()),
String.join(",", convertList(userList, AdminUserRespDTO::getNickname)), reqVO.getReason()); String.join(",", convertList(userList, AdminUserRespDTO::getNickname)), reqVO.getReason());
@ -1154,7 +1170,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner
Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))); Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) { if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) {
List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds())).getCheckedData(); List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds()));
throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname))); throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname)));
} }
return taskEntity; return taskEntity;
@ -1216,10 +1232,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 1.2 校验取消人存在 // 1.2 校验取消人存在
AdminUserRespDTO cancelUser = null; AdminUserRespDTO cancelUser = null;
if (StrUtil.isNotBlank(task.getAssignee())) { if (StrUtil.isNotBlank(task.getAssignee())) {
cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getAssignee())).getCheckedData(); cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getAssignee()));
} }
if (cancelUser == null && StrUtil.isNotBlank(task.getOwner())) { if (cancelUser == null && StrUtil.isNotBlank(task.getOwner())) {
cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())).getCheckedData(); cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner()));
} }
Assert.notNull(cancelUser, "任务中没有所有者和审批人,数据错误"); Assert.notNull(cancelUser, "任务中没有所有者和审批人,数据错误");
@ -1233,7 +1249,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
taskService.deleteTasks(convertList(childTaskList, Task::getId)); taskService.deleteTasks(convertList(childTaskList, Task::getId));
// 3. 记录日志到父任务中。先记录日志是因为,通过 handleParentTask 方法之后,任务可能被完成了,并且不存在了,会报异常,所以先记录 // 3. 记录日志到父任务中。先记录日志是因为,通过 handleParentTask 方法之后,任务可能被完成了,并且不存在了,会报异常,所以先记录
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(userId);
taskService.addComment(task.getParentTaskId(), task.getProcessInstanceId(), BpmCommentTypeEnum.SUB_SIGN.getType(), taskService.addComment(task.getParentTaskId(), task.getProcessInstanceId(), BpmCommentTypeEnum.SUB_SIGN.getType(),
StrUtil.format(BpmCommentTypeEnum.SUB_SIGN.getComment(), user.getNickname(), cancelUser.getNickname())); StrUtil.format(BpmCommentTypeEnum.SUB_SIGN.getComment(), user.getNickname(), cancelUser.getNickname()));
@ -1464,102 +1480,106 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return; return;
} }
// 自动去重,通过自动审批的方式 // 需要基于 instance 设置租户编号,避免 Flowable 内部异步执行时【例如:超时自动通过】 丢失租户编号
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId()); FlowableUtils.execute(processInstance.getTenantId(), () -> {
if (processDefinitionInfo == null) { // 自动去重,通过自动审批的方式
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId()); BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
return; if (processDefinitionInfo == null) {
} log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
if (processDefinitionInfo.getAutoApprovalType() != null) {
HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.taskAssignee(task.getAssignee()) // 相同审批人
.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
.finished();
if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
&& sameAssigneeQuery.count() > 0) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
return; return;
} }
if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) { if (processDefinitionInfo.getAutoApprovalType() != null) {
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); HistoricTaskInstanceQuery approvedTaskQuery = historyService.createHistoricTaskInstanceQuery()
if (bpmnModel == null) { .processInstanceId(task.getProcessInstanceId())
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId()); .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
return; .finished();
} if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 && approvedTaskQuery.taskAssignee(task.getAssignee()).count() > 0) {
BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
SequenceFlow::getSourceRef);
if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName())); .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
return; return;
} }
} // 连续审批的节点自动通过
} if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) {
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
// 获取发起人节点 if (bpmnModel == null) {
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId());
if (bpmnModel == null) { return;
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); }
return; List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingUserTaskFlows( // 获取所有的上一个 UserTask 节点连线
} BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); SequenceFlow::getSourceRef);
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable approvedTaskQuery.taskDefinitionKeys(sourceTaskIds).orderByTaskCreateTime().desc(); // 设置 taskIds, 并按创建时间倒序排序
Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(), HistoricTaskInstance firstHisTask = CollUtil.getFirst(approvedTaskQuery.list());
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); if (firstHisTask != null && StrUtil.equals(firstHisTask.getAssignee(), task.getAssignee())) {
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
return;
}
// 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
// 情况一:自动跳过
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
return;
}
// 情况二:转交给部门负责人审批
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()).getCheckedData() : null;
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
// 找不到部门负责人的情况下,自动审批通过
// noinspection DataFlowIssue
if (dept.getLeaderUserId() == null) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason())); .setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
return; return;
} }
// 找得到部门负责人的情况下,修改负责人
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
return;
}
// 如果部门负责人是自己,还是自己审批吧~
} }
} }
}
// 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 // 获取发起人节点
FlowableUtils.execute(processInstance.getTenantId(), () -> { BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData(); if (bpmnModel == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
return;
}
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
return;
}
// 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
// 情况一:自动跳过
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
return;
}
// 情况二:转交给部门负责人审批
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()) : null;
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
// 找不到部门负责人的情况下,自动审批通过
// noinspection DataFlowIssue
if (dept.getLeaderUserId() == null) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
return;
}
// 找得到部门负责人的情况下,修改负责人
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
return;
}
// 如果部门负责人是自己,还是自己审批吧~
}
}
}
// 发送消息
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
}); });
} }

View File

@ -14,7 +14,6 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@ -41,9 +40,9 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser)); when(adminUserApi.getUser(eq(1L))).thenReturn(startUser);
// mock 方法(getStartUserDept)没有部门 // mock 方法(getStartUserDept)没有部门
when(deptApi.getDept(eq(10L))).thenReturn(success(null)); when(deptApi.getDept(eq(10L))).thenReturn(null);
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 1); Set<Long> result = expression.calculateUsers(execution, 1);
@ -57,12 +56,12 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser)); when(adminUserApi.getUser(eq(1L))).thenReturn(startUser);
DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L)
.setLeaderUserId(20L)); .setLeaderUserId(20L));
// mock 方法getDept // mock 方法getDept
when(deptApi.getDept(eq(10L))).thenReturn(success(startUserDept)); when(deptApi.getDept(eq(10L))).thenReturn(startUserDept);
when(deptApi.getDept(eq(100L))).thenReturn(success(null)); when(deptApi.getDept(eq(100L))).thenReturn(null);
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 2); Set<Long> result = expression.calculateUsers(execution, 2);
@ -76,14 +75,14 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser)); when(adminUserApi.getUser(eq(1L))).thenReturn(startUser);
DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L)
.setLeaderUserId(20L)); .setLeaderUserId(20L));
when(deptApi.getDept(eq(10L))).thenReturn(success(startUserDept)); when(deptApi.getDept(eq(10L))).thenReturn(startUserDept);
// mock 方法(父 dept // mock 方法(父 dept
DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L) DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L)
.setLeaderUserId(200L)); .setLeaderUserId(200L));
when(deptApi.getDept(eq(100L))).thenReturn(success(parentDept)); when(deptApi.getDept(eq(100L))).thenReturn(parentDept);
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 2); Set<Long> result = expression.calculateUsers(execution, 2);

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@ -12,7 +11,6 @@ import org.mockito.stubbing.Answer;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -31,9 +29,9 @@ public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnit
// 准备参数 // 准备参数
String param = "10,20|2"; String param = "10,20|2";
// mock 方法 // mock 方法
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> { when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
Long deptId = invocationOnMock.getArgument(0); Long deptId = invocationOnMock.getArgument(0);
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
}); });
// 调用 // 调用

View File

@ -11,7 +11,6 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -31,9 +30,9 @@ public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest
// 准备参数 // 准备参数
String param = "10,20"; String param = "10,20";
// mock 方法 // mock 方法
when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList( when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList(
randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)), randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)),
randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L))))); randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L))));
// 调用 // 调用
Set<Long> userIds = strategy.calculateUsers(param); Set<Long> userIds = strategy.calculateUsers(param);

View File

@ -12,7 +12,6 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -34,9 +33,9 @@ public class BpmTaskCandidateDeptMemberStrategyTest extends BaseMockitoUnitTest
// 准备参数 // 准备参数
String param = "10,20"; String param = "10,20";
// mock 方法 // mock 方法
when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList( when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList(
randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)), randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)),
randomPojo(AdminUserRespDTO.class, o -> o.setId(21L))))); randomPojo(AdminUserRespDTO.class, o -> o.setId(21L))));
// 调用 // 调用
Set<Long> userIds = strategy.calculateUsers(param); Set<Long> userIds = strategy.calculateUsers(param);

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@ -17,7 +16,6 @@ import org.mockito.stubbing.Answer;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -74,10 +72,10 @@ public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMo
private void mockGetStartUserDept(Long startUserId) { private void mockGetStartUserDept(Long startUserId) {
when(adminUserApi.getUser(eq(startUserId))).thenReturn( when(adminUserApi.getUser(eq(startUserId))).thenReturn(
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)))); randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)));
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> { when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
Long deptId = invocationOnMock.getArgument(0); Long deptId = invocationOnMock.getArgument(0);
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
}); });
} }

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@ -17,7 +16,6 @@ import org.mockito.stubbing.Answer;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -74,10 +72,10 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockito
private void mockGetStartUserDeptLeader(Long startUserId) { private void mockGetStartUserDeptLeader(Long startUserId) {
when(adminUserApi.getUser(eq(startUserId))).thenReturn( when(adminUserApi.getUser(eq(startUserId))).thenReturn(
success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)))); randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)));
when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> { when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> {
Long deptId = invocationOnMock.getArgument(0); Long deptId = invocationOnMock.getArgument(0);
return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1));
}); });
} }

View File

@ -12,7 +12,6 @@ import org.mockito.Mock;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -37,7 +36,7 @@ public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest {
// mock 方法 // mock 方法
List<AdminUserRespDTO> users = convertList(asSet(11L, 22L), List<AdminUserRespDTO> users = convertList(asSet(11L, 22L),
id -> new AdminUserRespDTO().setId(id)); id -> new AdminUserRespDTO().setId(id));
when(adminUserApi.getUserListByPostIds(eq(asSet(1L, 2L)))).thenReturn(success(users)); when(adminUserApi.getUserListByPostIds(eq(asSet(1L, 2L)))).thenReturn(users);
// 调用 // 调用
Set<Long> userIds = strategy.calculateUsersByTask(null, param); Set<Long> userIds = strategy.calculateUsersByTask(null, param);

View File

@ -110,7 +110,7 @@ public class CrmBusinessStatusController {
public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusTypeSimpleList() { public CommonResult<List<CrmBusinessStatusRespVO>> getBusinessStatusTypeSimpleList() {
List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList(); List<CrmBusinessStatusTypeDO> list = businessStatusTypeService.getBusinessStatusTypeList();
// 过滤掉部门不匹配的 // 过滤掉部门不匹配的
Long deptId = adminUserApi.getUser(getLoginUserId()).getCheckedData().getDeptId(); Long deptId = adminUserApi.getUser(getLoginUserId()).getDeptId();
list.removeIf(statusType -> CollUtil.isNotEmpty(statusType.getDeptIds()) && !statusType.getDeptIds().contains(deptId)); list.removeIf(statusType -> CollUtil.isNotEmpty(statusType.getDeptIds()) && !statusType.getDeptIds().contains(deptId));
return success(BeanUtils.toBean(list, CrmBusinessStatusRespVO.class)); return success(BeanUtils.toBean(list, CrmBusinessStatusRespVO.class));
} }

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.crm.dal.mysql.customer;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO; import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.limitconfig.CrmCustomerLimitConfigPageReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO; import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerLimitConfigDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -28,9 +29,9 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>() LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
.eq(CrmCustomerLimitConfigDO::getType, type); .eq(CrmCustomerLimitConfigDO::getType, type);
query.and(w -> { query.and(w -> {
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId); w.apply(MyBatisUtils.findInSet("user_ids"), userId);
if (deptId != null) { if (deptId != null) {
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId); w.or().apply(MyBatisUtils.findInSet("dept_ids"), deptId);
} }
}); });
return selectList(query); return selectList(query);

View File

@ -34,7 +34,7 @@ public class SysAdminUserParseFunction implements IParseFunction {
} }
// 获取用户信息 // 获取用户信息
AdminUserRespDTO user = adminUserApi.getUser(Long.parseLong(value.toString())).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(Long.parseLong(value.toString()));
if (user == null) { if (user == null) {
log.warn("[apply][获取用户{{}}为空", value); log.warn("[apply][获取用户{{}}为空", value);
return ""; return "";

View File

@ -34,7 +34,7 @@ public class SysDeptParseFunction implements IParseFunction {
} }
// 获取部门信息 // 获取部门信息
DeptRespDTO dept = deptApi.getDept(Long.parseLong(value.toString())).getCheckedData(); DeptRespDTO dept = deptApi.getDept(Long.parseLong(value.toString()));
if (dept == null) { if (dept == null) {
log.warn("[apply][获取部门{{}}为空", value); log.warn("[apply][获取部门{{}}为空", value);
return ""; return "";

View File

@ -95,7 +95,7 @@ public class CrmPermissionAspect {
} }
// 3. 考虑下级的权限 // 3. 考虑下级的权限
List<AdminUserRespDTO> subordinateUserIds = adminUserApi.getUserListBySubordinate(userId).getCheckedData(); List<AdminUserRespDTO> subordinateUserIds = adminUserApi.getUserListBySubordinate(userId);
for (Long subordinateUserId : convertSet(subordinateUserIds, AdminUserRespDTO::getId)) { for (Long subordinateUserId : convertSet(subordinateUserIds, AdminUserRespDTO::getId)) {
CrmPermissionDO subordinatePermission = CollUtil.findOne(bizPermissions, CrmPermissionDO subordinatePermission = CollUtil.findOne(bizPermissions,
permission -> ObjUtil.equal(permission.getUserId(), subordinateUserId)); permission -> ObjUtil.equal(permission.getUserId(), subordinateUserId));

View File

@ -112,7 +112,7 @@ public class CrmClueServiceImpl implements CrmClueService {
private void validateRelationDataExists(CrmClueSaveReqVO reqVO) { private void validateRelationDataExists(CrmClueSaveReqVO reqVO) {
// 校验负责人 // 校验负责人
if (Objects.nonNull(reqVO.getOwnerUserId()) && if (Objects.nonNull(reqVO.getOwnerUserId()) &&
Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()).getCheckedData())) { Objects.isNull(adminUserApi.getUser(reqVO.getOwnerUserId()))) {
throw exception(USER_NOT_EXISTS); throw exception(USER_NOT_EXISTS);
} }
} }

View File

@ -110,13 +110,13 @@ public class CrmCustomerLimitConfigServiceImpl implements CrmCustomerLimitConfig
* @param deptIds ids * @param deptIds ids
*/ */
private void validateUserAndDept(Collection<Long> userIds, Collection<Long> deptIds) { private void validateUserAndDept(Collection<Long> userIds, Collection<Long> deptIds) {
deptApi.validateDeptList(deptIds).checkError(); deptApi.validateDeptList(deptIds);
adminUserApi.validateUserList(userIds).checkError(); adminUserApi.validateUserList(userIds);
} }
@Override @Override
public List<CrmCustomerLimitConfigDO> getCustomerLimitConfigListByUserId(Integer type, Long userId) { public List<CrmCustomerLimitConfigDO> getCustomerLimitConfigListByUserId(Integer type, Long userId) {
AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(userId);
Assert.notNull(user, "用户({})不存在", userId); Assert.notNull(user, "用户({})不存在", userId);
return customerLimitConfigMapper.selectListByTypeAndUserIdAndDeptId(type, userId, user.getDeptId()); return customerLimitConfigMapper.selectListByTypeAndUserIdAndDeptId(type, userId, user.getDeptId());
} }

View File

@ -391,7 +391,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
throw exception(CUSTOMER_NOT_EXISTS); throw exception(CUSTOMER_NOT_EXISTS);
} }
// 1.2 校验负责人是否存在 // 1.2 校验负责人是否存在
adminUserApi.validateUserList(singletonList(ownerUserId)).checkError(); adminUserApi.validateUserList(singletonList(ownerUserId));
// 1.3 校验状态 // 1.3 校验状态
customers.forEach(customer -> { customers.forEach(customer -> {
// 校验是否已有负责人 // 校验是否已有负责人
@ -424,7 +424,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
// 3. 记录操作日志 // 3. 记录操作日志
AdminUserRespDTO user = null; AdminUserRespDTO user = null;
if (!isReceive) { if (!isReceive) {
user = adminUserApi.getUser(ownerUserId).getCheckedData(); user = adminUserApi.getUser(ownerUserId);
} }
for (CrmCustomerDO customer : customers) { for (CrmCustomerDO customer : customers) {
getSelf().receiveCustomerLog(customer, user == null ? null : user.getNickname()); getSelf().receiveCustomerLog(customer, user == null ? null : user.getNickname());

View File

@ -136,7 +136,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
private void createBizTypePermissions(CrmPermissionSaveReqVO reqVO, Integer type, Long bizId, String name, private void createBizTypePermissions(CrmPermissionSaveReqVO reqVO, Integer type, Long bizId, String name,
List<CrmPermissionCreateReqBO> createPermissions) { List<CrmPermissionCreateReqBO> createPermissions) {
AdminUserRespDTO user = adminUserApi.getUser(reqVO.getUserId()).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(reqVO.getUserId());
// 1. 需要考虑,被添加人,是不是应该有对应的权限了; // 1. 需要考虑,被添加人,是不是应该有对应的权限了;
CrmPermissionDO permission = hasAnyPermission(type, bizId, reqVO.getUserId()); CrmPermissionDO permission = hasAnyPermission(type, bizId, reqVO.getUserId());
if (ObjUtil.isNotNull(permission)) { if (ObjUtil.isNotNull(permission)) {
@ -157,7 +157,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
private Long createPermission0(CrmPermissionCreateReqBO createReqBO) { private Long createPermission0(CrmPermissionCreateReqBO createReqBO) {
validatePermissionNotExists(Collections.singletonList(createReqBO)); validatePermissionNotExists(Collections.singletonList(createReqBO));
// 1. 校验用户是否存在 // 1. 校验用户是否存在
adminUserApi.validateUserList(Collections.singletonList(createReqBO.getUserId())).checkError(); adminUserApi.validateUserList(Collections.singletonList(createReqBO.getUserId()));
// 2. 插入权限 // 2. 插入权限
CrmPermissionDO permission = BeanUtils.toBean(createReqBO, CrmPermissionDO.class); CrmPermissionDO permission = BeanUtils.toBean(createReqBO, CrmPermissionDO.class);
permissionMapper.insert(permission); permissionMapper.insert(permission);
@ -168,7 +168,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
public void createPermissionBatch(List<CrmPermissionCreateReqBO> createReqBOs) { public void createPermissionBatch(List<CrmPermissionCreateReqBO> createReqBOs) {
validatePermissionNotExists(createReqBOs); validatePermissionNotExists(createReqBOs);
// 1. 校验用户是否存在 // 1. 校验用户是否存在
adminUserApi.validateUserList(convertSet(createReqBOs, CrmPermissionCreateReqBO::getUserId)).checkError(); adminUserApi.validateUserList(convertSet(createReqBOs, CrmPermissionCreateReqBO::getUserId));
// 2. 创建 // 2. 创建
List<CrmPermissionDO> permissions = BeanUtils.toBean(createReqBOs, CrmPermissionDO.class); List<CrmPermissionDO> permissions = BeanUtils.toBean(createReqBOs, CrmPermissionDO.class);
@ -219,7 +219,7 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName);
} }
// 1.2 校验新负责人是否存在 // 1.2 校验新负责人是否存在
adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId())).checkError(); adminUserApi.validateUserList(Collections.singletonList(transferReqBO.getNewOwnerUserId()));
// 2. 修改新负责人的权限 // 2. 修改新负责人的权限
List<CrmPermissionDO> permissions = permissionMapper.selectByBizTypeAndBizId( List<CrmPermissionDO> permissions = permissionMapper.selectByBizTypeAndBizId(

View File

@ -60,7 +60,7 @@ public class CrmProductServiceImpl implements CrmProductService {
success = CRM_PRODUCT_CREATE_SUCCESS) success = CRM_PRODUCT_CREATE_SUCCESS)
public Long createProduct(CrmProductSaveReqVO createReqVO) { public Long createProduct(CrmProductSaveReqVO createReqVO) {
// 1. 校验产品 // 1. 校验产品
adminUserApi.validateUserList(Collections.singleton(createReqVO.getOwnerUserId())).checkError(); adminUserApi.validateUserList(Collections.singleton(createReqVO.getOwnerUserId()));
validateProductNoDuplicate(null, createReqVO.getNo()); validateProductNoDuplicate(null, createReqVO.getNo());
validateProductCategoryExists(createReqVO.getCategoryId()); validateProductCategoryExists(createReqVO.getCategoryId());

View File

@ -359,10 +359,10 @@ public class CrmStatisticsCustomerServiceImpl implements CrmStatisticsCustomerSe
} }
// 情况二:选中某个部门 // 情况二:选中某个部门
// 2.1 获得部门列表 // 2.1 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()).getCheckedData(), DeptRespDTO::getId); List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
deptIds.add(reqVO.getDeptId()); deptIds.add(reqVO.getDeptId());
// 2.2 获得用户编号 // 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(), AdminUserRespDTO::getId); return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
} }
} }

View File

@ -145,10 +145,10 @@ public class CrmStatisticsFunnelServiceImpl implements CrmStatisticsFunnelServic
} }
// 情况二:选中某个部门 // 情况二:选中某个部门
// 2.1 获得部门列表 // 2.1 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()).getCheckedData(), DeptRespDTO::getId); List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
deptIds.add(reqVO.getDeptId()); deptIds.add(reqVO.getDeptId());
// 2.2 获得用户编号 // 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(), AdminUserRespDTO::getId); return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
} }
} }

View File

@ -111,10 +111,10 @@ public class CrmStatisticsPerformanceServiceImpl implements CrmStatisticsPerform
// 情况二:选中某个部门 // 情况二:选中某个部门
// 2.1 获得部门列表 // 2.1 获得部门列表
final Long deptId = reqVO.getDeptId(); final Long deptId = reqVO.getDeptId();
List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId).getCheckedData(), DeptRespDTO::getId); List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId), DeptRespDTO::getId);
deptIds.add(deptId); deptIds.add(deptId);
// 2.2 获得用户编号 // 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(), AdminUserRespDTO::getId); return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
} }
} }

View File

@ -122,10 +122,10 @@ public class CrmStatisticsPortraitServiceImpl implements CrmStatisticsPortraitSe
} }
// 情况二:选中某个部门 // 情况二:选中某个部门
// 2.1 获得部门列表 // 2.1 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()).getCheckedData(), DeptRespDTO::getId); List<Long> deptIds = convertList(deptApi.getChildDeptList(reqVO.getDeptId()), DeptRespDTO::getId);
deptIds.add(reqVO.getDeptId()); deptIds.add(reqVO.getDeptId());
// 2.2 获得用户编号 // 2.2 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(), AdminUserRespDTO::getId); return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
} }
} }

View File

@ -125,10 +125,10 @@ public class CrmStatisticsRankServiceImpl implements CrmStatisticsRankService {
*/ */
public List<Long> getUserIds(Long deptId) { public List<Long> getUserIds(Long deptId) {
// 1. 获得部门列表 // 1. 获得部门列表
List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId).getCheckedData(), DeptRespDTO::getId); List<Long> deptIds = convertList(deptApi.getChildDeptList(deptId), DeptRespDTO::getId);
deptIds.add(deptId); deptIds.add(deptId);
// 2. 获得用户编号 // 2. 获得用户编号
return convertList(adminUserApi.getUserListByDeptIds(deptIds).getCheckedData(), AdminUserRespDTO::getId); return convertList(adminUserApi.getUserListByDeptIds(deptIds), AdminUserRespDTO::getId);
} }
} }

View File

@ -67,7 +67,7 @@ public class CrmPermissionUtils {
// 场景三:下属负责的数据(下属是负责人) // 场景三:下属负责的数据(下属是负责人)
if (CrmSceneTypeEnum.isSubordinate(sceneType)) { if (CrmSceneTypeEnum.isSubordinate(sceneType)) {
AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class); AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class);
List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId).getCheckedData(); List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId);
if (CollUtil.isEmpty(subordinateUsers)) { if (CollUtil.isEmpty(subordinateUsers)) {
query.eq(ownerUserIdField, -1); // 不返回任何结果 query.eq(ownerUserIdField, -1); // 不返回任何结果
} else { } else {

View File

@ -79,7 +79,7 @@ public class ImGroupMemberController {
// 3. 转化 VO // 3. 转化 VO
ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class); ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
AdminUserRespDTO user = adminUserApi.getUser(member.getUserId()).getCheckedData(); AdminUserRespDTO user = adminUserApi.getUser(member.getUserId());
if (user != null) { if (user != null) {
memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar()); memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar());
} }

View File

@ -97,7 +97,7 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
ImFriendRequestDO request = createOrResetRequest(fromUserId, reqVO); ImFriendRequestDO request = createOrResetRequest(fromUserId, reqVO);
// 3. 推送 FRIEND_REQUEST_RECEIVED 给 toUser 多端payload 携带申请方昵称 / 头像,前端按 requestId 直推 push 进列表 // 3. 推送 FRIEND_REQUEST_RECEIVED 给 toUser 多端payload 携带申请方昵称 / 头像,前端按 requestId 直推 push 进列表
AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId).getCheckedData(); AdminUserRespDTO fromUser = adminUserApi.getUser(fromUserId);
FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification() FriendRequestNotification payload = (FriendRequestNotification) new FriendRequestNotification()
.setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource()) .setRequestId(request.getId()).setApplyContent(request.getApplyContent()).setAddSource(request.getAddSource())
.setOperatorUserId(fromUserId).setFriendUserId(fromUserId); .setOperatorUserId(fromUserId).setFriendUserId(fromUserId);

View File

@ -98,7 +98,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
ImGroupRequestDO request = createOrResetApplyRequest(groupId, userId, reqVO); ImGroupRequestDO request = createOrResetApplyRequest(groupId, userId, reqVO);
// 4. 1503 私聊定向推群主 + 全部管理员多端同步payload 携带申请方昵称 / 头像 // 4. 1503 私聊定向推群主 + 全部管理员多端同步payload 携带申请方昵称 / 头像
AdminUserRespDTO applyUser = adminUserApi.getUser(userId).getCheckedData(); AdminUserRespDTO applyUser = adminUserApi.getUser(userId);
GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser); GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) { for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) {
websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification( websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(

View File

@ -235,7 +235,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
rtcParticipantMapper.insertBatch(participants); rtcParticipantMapper.insertBatch(participants);
// 3.1 推送通知RTC_CALL(INVITE) 给每个被邀请人 // 3.1 推送通知RTC_CALL(INVITE) 给每个被邀请人
AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId).getCheckedData(); AdminUserRespDTO inviterUser = adminUserApi.getUser(inviterId);
Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(invitees); Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(invitees);
for (Long inviteeId : invitees) { for (Long inviteeId : invitees) {
pushCallInviteNotification(call, inviterUser, inviteeId, inviteeMap.get(inviteeId), invitees); pushCallInviteNotification(call, inviterUser, inviteeId, inviteeMap.get(inviteeId), invitees);
@ -303,7 +303,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
rtcParticipantMapper.insertBatch(participants); rtcParticipantMapper.insertBatch(participants);
// 3. 推送通知RTC_CALL(INVITE) 给每个新邀请人 // 3. 推送通知RTC_CALL(INVITE) 给每个新邀请人
AdminUserRespDTO inviter = adminUserApi.getUser(inviterId).getCheckedData(); AdminUserRespDTO inviter = adminUserApi.getUser(inviterId);
Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(incomingUserIds); Map<Long, AdminUserRespDTO> inviteeMap = adminUserApi.getUserMap(incomingUserIds);
for (Long inviteeId : incomingUserIds) { for (Long inviteeId : incomingUserIds) {
pushCallInviteNotification(call, inviter, inviteeId, inviteeMap.get(inviteeId), incomingUserIds); pushCallInviteNotification(call, inviter, inviteeId, inviteeMap.get(inviteeId), incomingUserIds);
@ -491,7 +491,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) { if (!ImRtcParticipantStatusEnum.isJoined(participant.getStatus())) {
throw exception(RTC_NOT_PARTICIPANT); throw exception(RTC_NOT_PARTICIPANT);
} }
return signToken(userId, resolveDisplayName(adminUserApi.getUser(userId).getCheckedData(), userId), room); return signToken(userId, resolveDisplayName(adminUserApi.getUser(userId), userId), room);
} }
@Override @Override
@ -1016,7 +1016,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
* @param operatorUserId * @param operatorUserId
*/ */
private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) { private void pushCallRejectNotification(ImRtcCallDO call, Long operatorUserId) {
AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId).getCheckedData() : null; AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator); ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
for (Long receiverUserId : getCallAudienceUserIdList(call)) { for (Long receiverUserId : getCallAudienceUserIdList(call)) {
webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification( webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification(
@ -1107,7 +1107,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
*/ */
private void pushCallEndNotification(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason, private void pushCallEndNotification(ImRtcCallDO call, Long operatorId, ImRtcCallEndReasonEnum reason,
Long durationSeconds) { Long durationSeconds) {
AdminUserRespDTO operator = operatorId != null ? adminUserApi.getUser(operatorId).getCheckedData() : null; AdminUserRespDTO operator = operatorId != null ? adminUserApi.getUser(operatorId) : null;
ImRtcCallEndNotification payload = ImRtcCallEndNotification.of(call, reason, durationSeconds, operatorId, operator); ImRtcCallEndNotification payload = ImRtcCallEndNotification.of(call, reason, durationSeconds, operatorId, operator);
Long peerUserId = null; Long peerUserId = null;
if (!ImConversationTypeEnum.isGroup(call.getConversationType())) { if (!ImConversationTypeEnum.isGroup(call.getConversationType())) {

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.im.service.friend; package cn.iocoder.yudao.module.im.service.friend;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
@ -23,7 +24,6 @@ import org.springframework.dao.DuplicateKeyException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -142,7 +142,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState());
when(friendService.getFriend(2L, 1L)).thenReturn(null); when(friendService.getFriend(2L, 1L)).thenReturn(null);
when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null); when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null);
when(adminUserApi.getUser(1L)).thenReturn(success(new AdminUserRespDTO().setNickname("张三").setAvatar("a.png"))); when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setNickname("张三").setAvatar("a.png"));
when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); when(imProperties.getFriend()).thenReturn(new ImProperties.Friend());
// 调用 // 调用
@ -172,7 +172,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
.setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()) .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult())
.setHandleContent("旧拒绝").setApplyContent("旧内容"); .setHandleContent("旧拒绝").setApplyContent("旧内容");
when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(old); when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(old);
when(adminUserApi.getUser(1L)).thenReturn(success(null)); when(adminUserApi.getUser(1L)).thenReturn(null);
when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); when(imProperties.getFriend()).thenReturn(new ImProperties.Friend());
// 调用 // 调用
@ -200,7 +200,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
.setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()); .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult());
when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null, old); when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null, old);
when(friendRequestMapper.insert(any(ImFriendRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); when(friendRequestMapper.insert(any(ImFriendRequestDO.class))).thenThrow(new DuplicateKeyException("dup"));
when(adminUserApi.getUser(1L)).thenReturn(success(null)); when(adminUserApi.getUser(1L)).thenReturn(null);
when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); when(imProperties.getFriend()).thenReturn(new ImProperties.Friend());
// 调用 // 调用
@ -229,7 +229,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
friendRequestService.agreeFriendRequest(2L, 100L); friendRequestService.agreeFriendRequest(2L, 100L);
// 断言:双向建立好友 + 推 APPROVED 给发起方 // 断言:双向建立好友 + 推 APPROVED 给发起方
verify(adminUserApi).validateUserList(List.of(1L, 2L)); verify(adminUserApi).validateUserList(ListUtil.of(1L, 2L));
verify(friendService).becomeFriends(request); verify(friendService).becomeFriends(request);
verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
} }

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.im.service.group; package cn.iocoder.yudao.module.im.service.group;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
@ -27,7 +29,6 @@ import org.springframework.dao.DuplicateKeyException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
@ -94,14 +95,14 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
.status(CommonStatusEnum.ENABLE.getStatus()).build(); .status(CommonStatusEnum.ENABLE.getStatus()).build();
when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupService.validateGroupExists(10L)).thenReturn(group);
// 群里有 owner + 一个 admin作为 1503 推送目标 // 群里有 owner + 一个 admin作为 1503 推送目标
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(99L) ImGroupMemberDO.builder().groupId(10L).userId(99L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build(), .status(CommonStatusEnum.ENABLE.getStatus()).build(),
ImGroupMemberDO.builder().groupId(10L).userId(98L) ImGroupMemberDO.builder().groupId(10L).userId(98L)
.role(ImGroupMemberRoleEnum.ADMIN.getRole()) .role(ImGroupMemberRoleEnum.ADMIN.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
when(adminUserApi.getUser(1L)).thenReturn(success(buildUser(1L, "申请人"))); when(adminUserApi.getUser(1L)).thenReturn(buildUser(1L, "申请人"));
ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO();
reqVO.setGroupId(10L); reqVO.setGroupId(10L);
@ -126,11 +127,11 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
.joinApproval(true) .joinApproval(true)
.status(CommonStatusEnum.ENABLE.getStatus()).build(); .status(CommonStatusEnum.ENABLE.getStatus()).build();
when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupService.validateGroupExists(10L)).thenReturn(group);
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(99L) ImGroupMemberDO.builder().groupId(10L).userId(99L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
when(adminUserApi.getUser(1L)).thenReturn(success(buildUser(1L, "申请人"))); when(adminUserApi.getUser(1L)).thenReturn(buildUser(1L, "申请人"));
ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(1L) ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(1L)
.setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult());
when(groupRequestMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null, old); when(groupRequestMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null, old);
@ -191,7 +192,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L)
.status(CommonStatusEnum.ENABLE.getStatus()).build(); .status(CommonStatusEnum.ENABLE.getStatus()).build();
when(groupService.getGroup(10L)).thenReturn(group); when(groupService.getGroup(10L)).thenReturn(group);
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(99L) ImGroupMemberDO.builder().groupId(10L).userId(99L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
@ -269,7 +270,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
.status(CommonStatusEnum.ENABLE.getStatus()).build(); .status(CommonStatusEnum.ENABLE.getStatus()).build();
when(groupService.getGroup(10L)).thenReturn(group); when(groupService.getGroup(10L)).thenReturn(group);
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(1L) ImGroupMemberDO.builder().groupId(10L).userId(1L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
@ -290,16 +291,17 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
.status(CommonStatusEnum.ENABLE.getStatus()).build(); .status(CommonStatusEnum.ENABLE.getStatus()).build();
when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupService.validateGroupExists(10L)).thenReturn(group);
when(groupRequestMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null); when(groupRequestMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null);
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(99L) ImGroupMemberDO.builder().groupId(10L).userId(99L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
when(adminUserApi.getUserMap(anyCollection())).thenReturn(java.util.Map.of( when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.<Long, AdminUserRespDTO>builder()
2L, buildUser(2L, "用户A"), .put(2L, buildUser(2L, "用户A"))
3L, buildUser(3L, "用户B"))); .put(3L, buildUser(3L, "用户B"))
.build());
// 调用:邀请人 1L 邀请 2L、3L都没有旧记录 // 调用:邀请人 1L 邀请 2L、3L都没有旧记录
groupRequestService.createInviteRequestList(10L, 1L, List.of(2L, 3L)); groupRequestService.createInviteRequestList(10L, 1L, ListUtil.of(2L, 3L));
// 断言:插入 2 条 + 推 1503 给 owner每条 1 帧)共 2 帧 // 断言:插入 2 条 + 推 1503 给 owner每条 1 帧)共 2 帧
ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class);
@ -324,14 +326,14 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
.setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult());
when(groupRequestMapper.selectByGroupIdAndUserId(10L, 2L)).thenReturn(null, old); when(groupRequestMapper.selectByGroupIdAndUserId(10L, 2L)).thenReturn(null, old);
when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup"));
when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(List.of( when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of(
ImGroupMemberDO.builder().groupId(10L).userId(99L) ImGroupMemberDO.builder().groupId(10L).userId(99L)
.role(ImGroupMemberRoleEnum.OWNER.getRole()) .role(ImGroupMemberRoleEnum.OWNER.getRole())
.status(CommonStatusEnum.ENABLE.getStatus()).build())); .status(CommonStatusEnum.ENABLE.getStatus()).build()));
when(adminUserApi.getUserMap(anyCollection())).thenReturn(java.util.Map.of(2L, buildUser(2L, "用户A"))); when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.of(2L, buildUser(2L, "用户A")));
// 调用 // 调用
groupRequestService.createInviteRequestList(10L, 1L, List.of(2L)); groupRequestService.createInviteRequestList(10L, 1L, ListUtil.of(2L));
// 断言:复用并重置旧邀请申请 // 断言:复用并重置旧邀请申请
verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L), verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L),

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.infra.api.config; package cn.iocoder.yudao.module.infra.api.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.infra.enums.ApiConstants; import cn.iocoder.yudao.module.infra.enums.ApiConstants;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -16,6 +15,6 @@ public interface ConfigApi {
@GetMapping(PREFIX + "/get-value-by-key") @GetMapping(PREFIX + "/get-value-by-key")
@Operation(summary = "根据参数键查询参数值") @Operation(summary = "根据参数键查询参数值")
CommonResult<String> getConfigValueByKey(@RequestParam("key") String key); String getConfigValueByKey(@RequestParam("key") String key);
} }

View File

@ -1,14 +1,11 @@
package cn.iocoder.yudao.module.infra.api.config; package cn.iocoder.yudao.module.infra.api.config;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO; import cn.iocoder.yudao.module.infra.dal.dataobject.config.ConfigDO;
import cn.iocoder.yudao.module.infra.service.config.ConfigService; import cn.iocoder.yudao.module.infra.service.config.ConfigService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@RestController // 提供 RESTful API 接口,给 Feign 调用 @RestController // 提供 RESTful API 接口,给 Feign 调用
@Validated @Validated
public class ConfigApiImpl implements ConfigApi { public class ConfigApiImpl implements ConfigApi {
@ -17,9 +14,9 @@ public class ConfigApiImpl implements ConfigApi {
private ConfigService configService; private ConfigService configService;
@Override @Override
public CommonResult<String> getConfigValueByKey(String key) { public String getConfigValueByKey(String key) {
ConfigDO config = configService.getConfigByKey(key); ConfigDO config = configService.getConfigByKey(key);
return success(config != null ? config.getValue() : null); return config != null ? config.getValue() : null;
} }
} }

View File

@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.infra.controller.admin.file; package cn.iocoder.yudao.module.infra.controller.admin.file;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*;
@ -27,7 +28,6 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@ -111,10 +111,10 @@ public class FileController {
if (StrUtil.isEmpty(path)) { if (StrUtil.isEmpty(path)) {
throw new IllegalArgumentException("结尾的 path 路径必须传递"); throw new IllegalArgumentException("结尾的 path 路径必须传递");
} }
// 解码,解决中文路径的问题 // 解码,解决中文、%、+ 等特殊字符路径的问题
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/ // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/
// https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/ // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/
path = URLUtil.decode(path, StandardCharsets.UTF_8, false); path = HttpUtils.decodeUrlPath(path);
// 读取内容 // 读取内容
byte[] content = fileService.getFileContent(configId, path); byte[] content = fileService.getFileContent(configId, path);
@ -123,7 +123,9 @@ public class FileController {
response.setStatus(HttpStatus.NOT_FOUND.value()); response.setStatus(HttpStatus.NOT_FOUND.value());
return; return;
} }
writeAttachment(response, path, content); FileDO file = fileService.getFileByConfigIdAndPath(configId, path);
String filename = file != null && StrUtil.isNotEmpty(file.getName()) ? file.getName() : FileUtil.getName(path);
writeAttachment(response, filename, content);
} }
@GetMapping("/page") @GetMapping("/page")

View File

@ -23,4 +23,11 @@ public interface FileMapper extends BaseMapperX<FileDO> {
.orderByDesc(FileDO::getId)); .orderByDesc(FileDO::getId));
} }
default FileDO selectLatestByConfigIdAndPath(Long configId, String path) {
return selectLastOne(new LambdaQueryWrapperX<FileDO>()
.eq(FileDO::getConfigId, configId)
.eq(FileDO::getPath, path)
.orderByAsc(FileDO::getId));
}
} }

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.infra.framework.file.core.client; package cn.iocoder.yudao.module.infra.framework.file.core.client;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -72,7 +73,7 @@ public abstract class AbstractFileClient<Config extends FileClientConfig> implem
* @return URL 访 * @return URL 访
*/ */
protected String formatFileUrl(String domain, String path) { protected String formatFileUrl(String domain, String path) {
return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), HttpUtils.encodeUrlPath(path));
} }
} }

View File

@ -116,13 +116,18 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
@Override @Override
public String presignGetUrl(String url, Integer expirationSeconds) { public String presignGetUrl(String url, Integer expirationSeconds) {
// 1. 将 url 转换为 path // 1. 将 url 转换为 path
String path = StrUtil.removePrefix(url, config.getDomain() + "/"); // 完整 Domain URL 会解码还原为原始对象名;裸 path 约定已经是未编码的对象名
path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path)); boolean domainUrl = StrUtil.startWith(url, config.getDomain() + "/");
String path = domainUrl ? StrUtil.removePrefix(url, config.getDomain() + "/") : url;
if (domainUrl) {
path = HttpUtils.removeUrlPathQueryAndFragment(path);
path = HttpUtils.decodeUrlPath(path);
}
// 2.1 情况一:公开访问:无需签名 // 2.1 情况一:公开访问:无需签名
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) { if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) {
return config.getDomain() + "/" + path; return config.getDomain() + "/" + HttpUtils.encodeUrlPath(path);
} }
// 2.2 情况二:私有访问:生成 GET 预签名 URL // 2.2 情况二:私有访问:生成 GET 预签名 URL

View File

@ -85,9 +85,9 @@ public class FileTypeUtils {
// 设置内容显示、下载文件名https://www.cnblogs.com/wq-9/articles/12165056.html // 设置内容显示、下载文件名https://www.cnblogs.com/wq-9/articles/12165056.html
if (isImage(mineType)) { if (isImage(mineType)) {
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); response.setHeader("Content-Disposition", buildContentDisposition("inline", filename));
} else { } else {
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); response.setHeader("Content-Disposition", buildContentDisposition("attachment", filename));
} }
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
if (StrUtil.containsIgnoreCase(mineType, "video")) { if (StrUtil.containsIgnoreCase(mineType, "video")) {
@ -98,6 +98,29 @@ public class FileTypeUtils {
IoUtil.write(response.getOutputStream(), false, content); IoUtil.write(response.getOutputStream(), false, content);
} }
private static String buildContentDisposition(String dispositionType, String filename) {
return StrUtil.format("{};filename=\"{}\";filename*=UTF-8''{}",
dispositionType, buildFallbackFilename(filename), HttpUtils.encodeUrlPathSegment(filename));
}
private static String buildFallbackFilename(String filename) {
if (StrUtil.isEmpty(filename)) {
return "download";
}
StringBuilder result = new StringBuilder(filename.length());
for (int i = 0; i < filename.length(); i++) {
char ch = filename.charAt(i);
if (ch == '"' || ch == '\\') {
result.append('\\').append(ch);
} else if (ch >= 0x20 && ch <= 0x7E) {
result.append(ch);
} else {
result.append('_');
}
}
return result.toString();
}
/** /**
* *
* *

View File

@ -459,7 +459,7 @@ public class CodegenEngine {
private String prettyCode(String content, String vmPath) { private String prettyCode(String content, String vmPath) {
// Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错(需要排除 vben5、vue3_admin_uniapp
if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) { if (!StrUtil.containsAny(vmPath, "vben5", "vue3_admin_uniapp")) {
content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); content = content.replaceAll(",\\r?\\n}", "\n}").replaceAll(",\\r?\\n }", "\n }");
} }
// Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到 // Vue 界面:去除多的 dateFormatter只有一个的情况下说明没使用到
if (StrUtil.count(content, "dateFormatter") == 1) { if (StrUtil.count(content, "dateFormatter") == 1) {

View File

@ -86,4 +86,13 @@ public interface FileService {
*/ */
byte[] getFileContent(Long configId, String path) throws Exception; byte[] getFileContent(Long configId, String path) throws Exception;
/**
*
*
* @param configId
* @param path
* @return
*/
FileDO getFileByConfigIdAndPath(Long configId, String path);
} }

View File

@ -238,4 +238,9 @@ public class FileServiceImpl implements FileService {
return client.getContent(path); return client.getContent(path);
} }
@Override
public FileDO getFileByConfigIdAndPath(Long configId, String path) {
return fileMapper.selectLatestByConfigIdAndPath(configId, path);
}
} }

View File

@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
#set ($primaryTsType = "number")
#else
#set ($primaryTsType = "string")
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
#set ($javaType = $column.javaType.toLowerCase())
#set ($javaFieldLower = $column.javaField.toLowerCase())
#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
${column.javaField}#if($optional)?#end: number
#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
${column.javaField}#if($optional)?#end: Date
#elseif(${javaType} == "boolean")
${column.javaField}#if($optional)?#end: boolean
#else
${column.javaField}#if($optional)?#end: string
#end
#end
#end
}
/** 获取${table.classComment}分页列表 */
export function get${simpleClassName}Page(params: PageParam) {
return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
}
/** 获取${table.classComment}详情 */
export function get${simpleClassName}(id: ${primaryTsType}) {
return http.get<${simpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/get?id=${id}`)
}
/** 创建${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}) {
return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
}
/** 更新${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}) {
return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/update', data)
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: ${primaryTsType}) {
return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`)
}

View File

@ -177,7 +177,7 @@ const emit = defineEmits<{
reset: [] reset: []
}>() }>()
const visible = ref(false) const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({ const formData = reactive({
#foreach($column in $columns) #foreach($column in $columns)
#if ($column.listOperation) #if ($column.listOperation)
@ -201,7 +201,7 @@ const formData = reactive({
#end #end
#end #end
#end #end
}) }) // 搜索表单数据
/** 搜索条件 placeholder 拼接 */ /** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => { const placeholder = computed(() => {
@ -237,21 +237,20 @@ const placeholder = computed(() => {
}) })
#if ($hasDateTimeBetween == 1) #if ($hasDateTimeBetween == 1)
// 时间范围选择器状态
#foreach($column in $columns) #foreach($column in $columns)
#if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN") #if ($column.listOperation && $column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
#set ($javaField = $column.javaField) #set ($javaField = $column.javaField)
#set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)}) #set ($AttrName = $javaField.substring(0,1).toUpperCase() + ${javaField.substring(1)})
const visible${AttrName} = ref<[boolean, boolean]>([false, false]) const visible${AttrName} = ref<[boolean, boolean]>([false, false]) // ${column.columnComment}选择器状态
const temp${AttrName} = ref<[number, number]>([Date.now(), Date.now()]) const temp${AttrName} = ref<[number, number]>([Date.now(), Date.now()]) // ${column.columnComment}临时值
/** ${column.columnComment}[0]确认 */ /** 确认${column.columnComment}开始日期 */
function handle${AttrName}0Confirm() { function handle${AttrName}0Confirm() {
formData.${javaField} = [temp${AttrName}.value[0], formData.${javaField}?.[1]] formData.${javaField} = [temp${AttrName}.value[0], formData.${javaField}?.[1]]
visible${AttrName}.value[0] = false visible${AttrName}.value[0] = false
} }
/** ${column.columnComment}[1]确认 */ /** 确认${column.columnComment}结束日期 */
function handle${AttrName}1Confirm() { function handle${AttrName}1Confirm() {
formData.${javaField} = [formData.${javaField}?.[0], temp${AttrName}.value[1]] formData.${javaField} = [formData.${javaField}?.[0], temp${AttrName}.value[1]]
visible${AttrName}.value[1] = false visible${AttrName}.value[1] = false
@ -260,7 +259,7 @@ function handle${AttrName}1Confirm() {
#end #end
#end #end
/** 搜索 */ /** 搜索按钮操作 */
function handleSearch() { function handleSearch() {
visible.value = false visible.value = false
emit('search', { emit('search', {
@ -277,7 +276,7 @@ function handleSearch() {
}) })
} }
/** 重置 */ /** 重置按钮操作 */
function handleReset() { function handleReset() {
#foreach($column in $columns) #foreach($column in $columns)
#if ($column.listOperation) #if ($column.listOperation)

View File

@ -62,6 +62,7 @@
#end #end
#end #end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni' import { useToast } from 'wot-design-uni'
import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
@ -87,8 +88,8 @@ definePage({
const { hasAccessByCodes } = useAccess() const { hasAccessByCodes } = useAccess()
const toast = useToast() const toast = useToast()
const formData = ref<${simpleClassName}>() const formData = ref<${simpleClassName}>() // 详情数据
const deleting = ref(false) const deleting = ref(false) // 删除状态
/** 返回上一页 */ /** 返回上一页 */
function handleBack() { function handleBack() {
@ -97,7 +98,7 @@ function handleBack() {
/** 加载${table.classComment}详情 */ /** 加载${table.classComment}详情 */
async function getDetail() { async function getDetail() {
if (!props.id) { if (!props.id || deleting.value) {
return return
} }
try { try {
@ -131,6 +132,7 @@ function handleDelete() {
try { try {
await delete${simpleClassName}(props.id) await delete${simpleClassName}(props.id)
toast.success('删除成功') toast.success('删除成功')
uni.$emit('${table.moduleName}:${table.businessName}:reload')
setTimeout(() => { setTimeout(() => {
handleBack() handleBack()
}, 500) }, 500)
@ -143,8 +145,14 @@ function handleDelete() {
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
uni.$on('${table.moduleName}:${table.businessName}:reload', getDetail)
getDetail() getDetail()
}) })
/** 卸载 */
onUnload(() => {
uni.$off('${table.moduleName}:${table.businessName}:reload', getDetail)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -175,7 +175,7 @@ definePage({
const toast = useToast() const toast = useToast()
const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}') const getTitle = computed(() => props.id ? '编辑${table.classComment}' : '新增${table.classComment}')
const formLoading = ref(false) const formLoading = ref(false) // 表单提交状态
const formData = ref<${simpleClassName}>({ const formData = ref<${simpleClassName}>({
#foreach($column in $columns) #foreach($column in $columns)
#if (($column.createOperation || $column.updateOperation) || $column.primaryKey) #if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
@ -195,7 +195,7 @@ const formData = ref<${simpleClassName}>({
#end #end
#end #end
#end #end
}) }) // 表单数据
const formRules = { const formRules = {
#foreach($column in $columns) #foreach($column in $columns)
#set ($javaFieldLower = $column.javaField.toLowerCase()) #set ($javaFieldLower = $column.javaField.toLowerCase())
@ -204,8 +204,8 @@ const formRules = {
${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }], ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
#end #end
#end #end
} } // 表单校验规则
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */ /** 返回上一页 */
function handleBack() { function handleBack() {
@ -236,6 +236,7 @@ async function handleSubmit() {
await create${simpleClassName}(formData.value) await create${simpleClassName}(formData.value)
toast.success('新增成功') toast.success('新增成功')
} }
uni.$emit('${table.moduleName}:${table.businessName}:reload')
setTimeout(() => { setTimeout(() => {
handleBack() handleBack()
}, 500) }, 500)

View File

@ -1,5 +1,5 @@
<template> <template>
<view class="yd-page-container"> <view class="yd-page-container yd-page-container-paging">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<wd-navbar <wd-navbar
title="${table.classComment}管理" title="${table.classComment}管理"
@ -11,14 +11,26 @@
<SearchForm @search="handleQuery" @reset="handleReset" /> <SearchForm @search="handleQuery" @reset="handleReset" />
<!-- ${table.classComment}列表 --> <!-- ${table.classComment}列表 -->
<view class="p-24rpx"> <z-paging
<view ref="pagingRef"
v-for="item in list" v-model="list"
:key="item.${primaryColumn.javaField}" :fixed="false"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm" class="min-h-0 flex-1"
@click="handleDetail(item)" :default-page-size="10"
> :refresher-enabled="true"
<view class="p-24rpx"> :inside-more="true"
:loading-more-default-as-loading="true"
empty-view-text="暂无${table.classComment}数据"
@query="queryList"
>
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.${primaryColumn.javaField}"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
#set ($titleField = "") #set ($titleField = "")
#set ($statusField = "") #set ($statusField = "")
#set ($statusDictType = "") #set ($statusDictType = "")
@ -35,14 +47,14 @@
#if ($titleField == "") #if ($titleField == "")
#set ($titleField = $primaryColumn.javaField) #set ($titleField = $primaryColumn.javaField)
#end #end
<view class="mb-16rpx flex items-center justify-between"> <view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold"> <view class="text-32rpx text-[#333] font-semibold">
{{ item.${titleField} }} {{ item.${titleField} }}
</view> </view>
#if($statusField != "") #if($statusField != "")
<dict-tag :type="DICT_TYPE.${statusDictType.toUpperCase()}" :value="item.${statusField}" /> <dict-tag :type="DICT_TYPE.${statusDictType.toUpperCase()}" :value="item.${statusField}" />
#end #end
</view> </view>
#foreach($column in $columns) #foreach($column in $columns)
#if ($column.listOperationResult && !$column.primaryKey && $column.javaField != $titleField && $column.javaField != $statusField) #if ($column.listOperationResult && !$column.primaryKey && $column.javaField != $titleField && $column.javaField != $statusField)
#set ($javaField = $column.javaField) #set ($javaField = $column.javaField)
@ -50,36 +62,27 @@
#set ($dictType = $column.dictType) #set ($dictType = $column.dictType)
#set ($javaType = $column.javaType) #set ($javaType = $column.javaType)
#if ($dictType && "" != $dictType) #if ($dictType && "" != $dictType)
<view class="mb-12rpx flex items-center text-28rpx text-[#666]"> <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text> <text class="mr-8rpx text-[#999]">${comment}</text>
<dict-tag :type="DICT_TYPE.${dictType.toUpperCase()}" :value="item.${javaField}" /> <dict-tag :type="DICT_TYPE.${dictType.toUpperCase()}" :value="item.${javaField}" />
</view> </view>
#elseif ($javaType == "LocalDateTime") #elseif ($javaType == "LocalDateTime")
<view class="mb-12rpx flex items-center text-28rpx text-[#666]"> <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text> <text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ formatDateTime(item.${javaField}) || '-' }}</text> <text class="line-clamp-1">{{ formatDateTime(item.${javaField}) || '-' }}</text>
</view> </view>
#else #else
<view class="mb-12rpx flex items-center text-28rpx text-[#666]"> <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">${comment}</text> <text class="mr-8rpx text-[#999]">${comment}</text>
<text class="line-clamp-1">{{ item.${javaField} }}</text> <text class="line-clamp-1">{{ item.${javaField} }}</text>
</view> </view>
#end #end
#end #end
#end #end
</view>
</view> </view>
</view> </view>
</z-paging>
<!-- 加载更多 -->
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无${table.classComment}数据" />
</view>
<wd-loadmore
v-if="list.length > 0"
:state="loadMoreState"
@reload="loadMore"
/>
</view>
<!-- 新增按钮 --> <!-- 新增按钮 -->
<wd-fab <wd-fab
@ -108,8 +111,7 @@
#end #end
#end #end
import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}' import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
import type { LoadMoreState } from '@/http/types' import { onUnload } from '@dcloudio/uni-app'
import { onReachBottom } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}' import { get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
import { useAccess } from '@/hooks/useAccess' import { useAccess } from '@/hooks/useAccess'
@ -130,13 +132,9 @@ definePage({
}) })
const { hasAccessByCodes } = useAccess() const { hasAccessByCodes } = useAccess()
const total = ref(0) const list = ref<${simpleClassName}[]>([]) // 列表数据
const list = ref<${simpleClassName}[]>([]) const pagingRef = ref<any>() // 分页组件引用
const loadMoreState = ref<LoadMoreState>('loading') const queryParams = ref<Record<string, any>>({}) // 查询参数
const queryParams = ref({
pageNo: 1,
pageSize: 10,
})
/** 返回上一页 */ /** 返回上一页 */
function handleBack() { function handleBack() {
@ -144,28 +142,24 @@ function handleBack() {
} }
/** 查询${table.classComment}列表 */ /** 查询${table.classComment}列表 */
async function getList() { async function queryList(pageNo: number, pageSize: number) {
loadMoreState.value = 'loading'
try { try {
const data = await get${simpleClassName}Page(queryParams.value) const params = {
list.value = [...list.value, ...data.list] ...queryParams.value,
total.value = data.total pageNo,
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading' pageSize,
}
const data = await get${simpleClassName}Page(params)
pagingRef.value?.completeByTotal(data.list, data.total)
} catch { } catch {
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1 pagingRef.value?.complete(false)
loadMoreState.value = 'error'
} }
} }
/** 搜索按钮操作 */ /** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) { function handleQuery(data?: Record<string, any>) {
queryParams.value = { queryParams.value = { ...data }
...data, reload()
pageNo: 1,
pageSize: queryParams.value.pageSize,
}
list.value = []
getList()
} }
/** 重置按钮操作 */ /** 重置按钮操作 */
@ -173,13 +167,9 @@ function handleReset() {
handleQuery() handleQuery()
} }
/** 加载更多 */ /** 重新加载 */
function loadMore() { function reload() {
if (loadMoreState.value === 'finished') { pagingRef.value?.reload()
return
}
queryParams.value.pageNo++
getList()
} }
/** 新增${table.classComment} */ /** 新增${table.classComment} */
@ -196,14 +186,14 @@ function handleDetail(item: ${simpleClassName}) {
}) })
} }
/** 触底加载更多 */
onReachBottom(() => {
loadMore()
})
/** 初始化 */ /** 初始化 */
onMounted(() => { onMounted(() => {
getList() uni.$on('${table.moduleName}:${table.businessName}:reload', reload)
})
/** 卸载 */
onUnload(() => {
uni.$off('${table.moduleName}:${table.businessName}:reload', reload)
}) })
</script> </script>

View File

@ -0,0 +1,179 @@
import type { PageParam, PageResult } from '@vben/request';
import type { Dayjs } from 'dayjs';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subColumns = $subColumnsList.get($index))##当前字段数组
/** ${subTable.classComment}信息 */
export interface ${subSimpleClassName} {
#foreach ($column in $subColumns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
}
#end
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: string | Dayjs; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
## 特殊:主子表专属逻辑
#if ( $table.templateType == 10 || $table.templateType == 12 )
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#if ( $subTable.subJoinMany )
${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
#else
${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
#end
#end
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
#if ( $table.templateType != 2 && $deleteBatchEnable)
/** 批量删除${table.classComment} */
export function delete${simpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', { params });
}
#if ($importEnable)
/** 下载${table.classComment}导入模板 */
export function import${simpleClassName}Template() {
return requestClient.download('${baseURL}/get-import-template');
}
/** 导入${table.classComment} */
export function import${simpleClassName}(data: FormData) {
return requestClient.post('${baseURL}/import', data);
}
#end
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
#if ($deleteBatchEnable)
/** 批量删除${subTable.classComment} */
export function delete${subSimpleClassName}List(ids: number[]) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete-list?ids=${ids.join(',')}`)
}
#end
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end

View File

@ -0,0 +1 @@
#parse("codegen/vue3_vben5_ele/schema/api/api.ts.vm")

View File

@ -41,6 +41,21 @@ public class LocalFileClientTest {
assertFalse(FileUtil.exist(new File(tempDir, path))); assertFalse(FileUtil.exist(new File(tempDir, path)));
} }
@Test
public void testUpload_encodeUrlPath() {
// 准备参数
LocalFileClient client = createClient();
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
String path = "avatar/中文 100%+文件.txt";
// 调用
String url = client.upload(content, path, "text/plain");
// 断言
assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.txt", url);
assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path)));
}
@Test @Test
public void testUpload_pathInvalid() { public void testUpload_pathInvalid() {
// 准备参数 // 准备参数

View File

@ -9,9 +9,66 @@ import jakarta.validation.Validation;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings("resource") @SuppressWarnings("resource")
public class S3FileClientTest { public class S3FileClientTest {
@Test
public void testPresignGetUrl_publicAccess_encodeUrlPath() {
// 准备参数
S3FileClientConfig config = new S3FileClientConfig();
config.setDomain("https://static.iocoder.cn");
config.setEnablePublicAccess(true);
S3FileClient client = new S3FileClient(0L, config);
// 调用
String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300);
// 断言
assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
}
@Test
public void testPresignGetUrl_publicAccess_decodeDomainUrl() {
// 准备参数
S3FileClientConfig config = new S3FileClientConfig();
config.setDomain("https://static.iocoder.cn");
config.setEnablePublicAccess(true);
S3FileClient client = new S3FileClient(0L, config);
// 调用
String result = client.presignGetUrl("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg?token=1", 300);
// 断言
assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
}
@Test
public void testPresignGetUrl_privateAccess_rawPath() {
// 准备参数
S3FileClientConfig config = new S3FileClientConfig();
config.setAccessKey("admin");
config.setAccessSecret("password");
config.setBucket("yudao");
config.setDomain("http://127.0.0.1:9000/yudao");
config.setEndpoint("http://127.0.0.1:9000");
config.setEnablePathStyleAccess(true);
config.setEnablePublicAccess(false);
S3FileClient client = new S3FileClient(0L, config);
client.init();
// 调用
String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300);
// 断言
assertTrue(result.contains("/yudao/avatar/%E4%B8%AD%E6%96%87%20100%25"));
assertTrue(result.contains("%E6%96%87%E4%BB%B6.jpg"));
assertFalse(result.contains("%25E4%25B8%25AD"));
}
@Test @Test
@Disabled // MinIO如果要集成测试可以注释本行 @Disabled // MinIO如果要集成测试可以注释本行
public void testMinIO() throws Exception { public void testMinIO() throws Exception {

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.infra.framework.file.core.utils;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link FileTypeUtils}
*/
public class FileTypeUtilsTest {
@Test
public void testWriteAttachment_contentDispositionEncodeFilename() throws Exception {
// 准备参数
MockHttpServletResponse response = new MockHttpServletResponse();
byte[] content = "test".getBytes(StandardCharsets.UTF_8);
// 调用
FileTypeUtils.writeAttachment(response, "中文 100%+文件.txt", content);
// 断言
assertEquals("attachment;filename=\"__ 100%+__.txt\";"
+ "filename*=UTF-8''%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.txt",
response.getHeader("Content-Disposition"));
assertArrayEquals(content, response.getContentAsByteArray());
}
}

View File

@ -108,14 +108,23 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
asserts.forEach(assertMap -> { asserts.forEach(assertMap -> {
String contentPath = (String) assertMap.get("contentPath"); String contentPath = (String) assertMap.get("contentPath");
String filePath = (String) assertMap.get("filePath"); String filePath = (String) assertMap.get("filePath");
String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath) String expected = normalizeLineEndings(ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath));
.replace("\r\n", "\n");
String actual = result.get(filePath); String actual = result.get(filePath);
assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"), assertEquals(expected, normalizeLineEndings(actual), filePath + ":不匹配");
filePath + ":不匹配");
}); });
} }
/**
* Windows/Unix
*/
private static String normalizeLineEndings(String content) {
if (content == null) {
return null;
}
content = content.replace("\r\n", "\n").replace('\r', '\n');
return content.replaceAll("\\n+\\z", "");
}
// ==================== 调试专用 ==================== // ==================== 调试专用 ====================
/** /**

View File

@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.infra.service.codegen.inner;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import com.baomidou.mybatisplus.annotation.DbType;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
/**
* {@link CodegenEngine} Vue3 Admin Uniapp + Wot UI
*
* @author
*/
public class CodegenEngineUniappTest extends CodegenEngineAbstractTest {
@Test
public void testExecute_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 调用
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
// 断言
assertResult(result, "/vue3_admin_uniapp_one");
}
}

View File

@ -211,6 +211,23 @@ public class FileServiceImplTest extends BaseDbUnitTest {
assertServiceException(() -> fileService.getFileContent(configId, path), FILE_PATH_INVALID); assertServiceException(() -> fileService.getFileContent(configId, path), FILE_PATH_INVALID);
} }
@Test
public void testGetFileByConfigIdAndPath() {
// mock 数据
FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("avatar/中文 100%+文件.jpg"));
fileMapper.insert(dbFile);
FileDO latestFile = ObjectUtils.cloneIgnoreId(dbFile, o -> o.setName("最新文件名.jpg"));
fileMapper.insert(latestFile);
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("avatar/other.jpg")));
fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setConfigId(20L)));
// 调用
FileDO result = fileService.getFileByConfigIdAndPath(10L, "avatar/中文 100%+文件.jpg");
// 断言
AssertUtils.assertPojoEquals(latestFile, result);
}
@Test @Test
public void testCreateFileByPresignedPath_success() { public void testCreateFileByPresignedPath_success() {
// 准备参数 // 准备参数

View File

@ -0,0 +1,55 @@
[ {
"contentPath" : "java/InfraStudentPageReqVO",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
}, {
"contentPath" : "java/InfraStudentRespVO",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
}, {
"contentPath" : "java/InfraStudentSaveReqVO",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
}, {
"contentPath" : "java/InfraStudentController",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
}, {
"contentPath" : "java/InfraStudentDO",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
}, {
"contentPath" : "java/InfraStudentMapper",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
}, {
"contentPath" : "xml/InfraStudentMapper",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
}, {
"contentPath" : "java/InfraStudentServiceImpl",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
}, {
"contentPath" : "java/InfraStudentService",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
}, {
"contentPath" : "java/InfraStudentServiceImplTest",
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
}, {
"contentPath" : "java/ErrorCodeConstants_手动操作",
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
}, {
"contentPath" : "sql/sql",
"filePath" : "sql/sql.sql"
}, {
"contentPath" : "sql/h2",
"filePath" : "sql/h2.sql"
}, {
"contentPath" : "ts/index",
"filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
}, {
"contentPath" : "vue/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
}, {
"contentPath" : "vue/search-form",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
}, {
"contentPath" : "vue/form/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
}, {
"contentPath" : "vue/detail/index",
"filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
} ]

View File

@ -0,0 +1,3 @@
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意请给“TODO 补充编号”设置一个错误码编号!!!
// ========== 学生 TODO 补充编号 ==========
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

View File

@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.constraints.*;
import jakarta.validation.*;
import jakarta.servlet.http.*;
import java.util.*;
import java.io.IOException;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
@Tag(name = "管理后台 - 学生")
@RestController
@RequestMapping("/infra/student")
@Validated
public class InfraStudentController {
@Resource
private InfraStudentService studentService;
@PostMapping("/create")
@Operation(summary = "创建学生")
@PreAuthorize("@ss.hasPermission('infra:student:create')")
public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
return success(studentService.createStudent(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新学生")
@PreAuthorize("@ss.hasPermission('infra:student:update')")
public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
studentService.updateStudent(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除学生")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
studentService.deleteStudent(id);
return success(true);
}
@DeleteMapping("/delete-list")
@Parameter(name = "ids", description = "编号", required = true)
@Operation(summary = "批量删除学生")
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
studentService.deleteStudentListByIds(ids);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得学生")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
InfraStudentDO student = studentService.getStudent(id);
return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得学生分页")
@PreAuthorize("@ss.hasPermission('infra:student:query')")
public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出学生 Excel")
@PreAuthorize("@ss.hasPermission('infra:student:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
BeanUtils.toBean(list, InfraStudentRespVO.class));
}
}

View File

@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
/**
* 学生 DO
*
* @author 芋道源码
*/
@TableName("infra_student")
@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfraStudentDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 名字
*/
private String name;
/**
* 简介
*/
private String description;
/**
* 出生日期
*/
private LocalDateTime birthday;
/**
* 性别
*
* 枚举 {@link TODO system_user_sex 对应的类}
*/
private Integer sex;
/**
* 是否有效
*
* 枚举 {@link TODO infra_boolean_string 对应的类}
*/
private Boolean enabled;
/**
* 头像
*/
private String avatar;
/**
* 附件
*/
private String video;
/**
* 备注
*/
private String memo;
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.infra.dal.mysql.demo;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
/**
* 学生 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
.likeIfPresent(InfraStudentDO::getName, reqVO.getName())
.eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
.eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
.eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
.betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(InfraStudentDO::getId));
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - 学生分页 Request VO")
@Data
public class InfraStudentPageReqVO extends PageParam {
@Schema(description = "名字", example = "芋头")
private String name;
@Schema(description = "出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", example = "1")
private Integer sex;
@Schema(description = "是否有效", example = "true")
private Boolean enabled;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import cn.idev.excel.annotation.*;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
@Schema(description = "管理后台 - 学生 Response VO")
@Data
@ExcelIgnoreUnannotated
public class InfraStudentRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@ExcelProperty("编号")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@ExcelProperty("名字")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@ExcelProperty("简介")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("出生日期")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "性别", converter = DictConvert.class)
@DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@ExcelProperty(value = "是否有效", converter = DictConvert.class)
@DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@ExcelProperty("头像")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@ExcelProperty("附件")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@ExcelProperty("备注")
private String memo;
@Schema(description = "创建时间")
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 学生新增/修改 Request VO")
@Data
public class InfraStudentSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
@NotEmpty(message = "名字不能为空")
private String name;
@Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
@NotEmpty(message = "简介不能为空")
private String description;
@Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "出生日期不能为空")
private LocalDateTime birthday;
@Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "性别不能为空")
private Integer sex;
@Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否有效不能为空")
private Boolean enabled;
@Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
@NotEmpty(message = "头像不能为空")
private String avatar;
@Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
@NotEmpty(message = "附件不能为空")
private String video;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
@NotEmpty(message = "备注不能为空")
private String memo;
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.infra.service.demo;
import java.util.*;
import jakarta.validation.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
/**
* 学生 Service 接口
*
* @author 芋道源码
*/
public interface InfraStudentService {
/**
* 创建学生
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
/**
* 更新学生
*
* @param updateReqVO 更新信息
*/
void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
/**
* 删除学生
*
* @param id 编号
*/
void deleteStudent(Long id);
/**
* 批量删除学生
*
* @param ids 编号
*/
void deleteStudentListByIds(List<Long> ids);
/**
* 获得学生
*
* @param id 编号
* @return 学生
*/
InfraStudentDO getStudent(Long id);
/**
* 获得学生分页
*
* @param pageReqVO 分页查询
* @return 学生分页
*/
PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
}

View File

@ -0,0 +1,85 @@
package cn.iocoder.yudao.module.infra.service.demo;
import cn.hutool.core.collection.CollUtil;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
/**
* 学生 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
public class InfraStudentServiceImpl implements InfraStudentService {
@Resource
private InfraStudentMapper studentMapper;
@Override
public Long createStudent(InfraStudentSaveReqVO createReqVO) {
// 插入
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
studentMapper.insert(student);
// 返回
return student.getId();
}
@Override
public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
// 校验存在
validateStudentExists(updateReqVO.getId());
// 更新
InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
studentMapper.updateById(updateObj);
}
@Override
public void deleteStudent(Long id) {
// 校验存在
validateStudentExists(id);
// 删除
studentMapper.deleteById(id);
}
@Override
public void deleteStudentListByIds(List<Long> ids) {
// 删除
studentMapper.deleteByIds(ids);
}
private void validateStudentExists(Long id) {
if (studentMapper.selectById(id) == null) {
throw exception(STUDENT_NOT_EXISTS);
}
}
@Override
public InfraStudentDO getStudent(Long id) {
return studentMapper.selectById(id);
}
@Override
public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
return studentMapper.selectPage(pageReqVO);
}
}

View File

@ -0,0 +1,145 @@
package cn.iocoder.yudao.module.infra.service.demo;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Import;
import java.util.*;
import java.time.LocalDateTime;
import static cn.hutool.core.util.RandomUtil.*;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link InfraStudentServiceImpl} 的单元测试类
*
* @author 芋道源码
*/
@Import(InfraStudentServiceImpl.class)
public class InfraStudentServiceImplTest extends BaseDbUnitTest {
@Resource
private InfraStudentServiceImpl studentService;
@Resource
private InfraStudentMapper studentMapper;
@Test
public void testCreateStudent_success() {
// 准备参数
InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
// 调用
Long studentId = studentService.createStudent(createReqVO);
// 断言
assertNotNull(studentId);
// 校验记录的属性是否正确
InfraStudentDO student = studentMapper.selectById(studentId);
assertPojoEquals(createReqVO, student, "id");
}
@Test
public void testUpdateStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
o.setId(dbStudent.getId()); // 设置更新的 ID
});
// 调用
studentService.updateStudent(updateReqVO);
// 校验是否更新正确
InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
assertPojoEquals(updateReqVO, student);
}
@Test
public void testUpdateStudent_notExists() {
// 准备参数
InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
// 调用, 并断言异常
assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
}
@Test
public void testDeleteStudent_success() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
// 准备参数
Long id = dbStudent.getId();
// 调用
studentService.deleteStudent(id);
// 校验数据不存在了
assertNull(studentMapper.selectById(id));
}
@Test
public void testDeleteStudent_notExists() {
// 准备参数
Long id = randomLongId();
// 调用, 并断言异常
assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
}
@Test
@Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
public void testGetStudentPage() {
// mock 数据
InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
o.setName(null);
o.setBirthday(null);
o.setSex(null);
o.setEnabled(null);
o.setCreateTime(null);
});
studentMapper.insert(dbStudent);
// 测试 name 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
// 测试 birthday 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
// 测试 sex 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
// 测试 enabled 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
// 测试 createTime 不匹配
studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
// 准备参数
InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
reqVO.setName(null);
reqVO.setBirthday(null);
reqVO.setSex(null);
reqVO.setEnabled(null);
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
// 调用
PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
// 断言
assertEquals(1, pageResult.getTotal());
assertEquals(1, pageResult.getList().size());
assertPojoEquals(dbStudent, pageResult.getList().get(0));
}
}

View File

@ -0,0 +1,17 @@
-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
CREATE TABLE IF NOT EXISTS "infra_student" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar NOT NULL,
"description" varchar NOT NULL,
"birthday" varchar NOT NULL,
"sex" int NOT NULL,
"enabled" bit NOT NULL,
"avatar" varchar NOT NULL,
"video" varchar NOT NULL,
"memo" varchar NOT NULL,
"create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("id")
) COMMENT '学生表';
-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
DELETE FROM "infra_student";

View File

@ -0,0 +1,54 @@
-- 菜单 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status, component_name
)
VALUES (
'学生管理', '', 2, 0, 888,
'student', '', 'infra/demo/index', 0, 'InfraStudent'
);
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生查询', 'infra:student:query', 3, 1, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生创建', 'infra:student:create', 3, 2, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生更新', 'infra:student:update', 3, 3, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生删除', 'infra:student:delete', 3, 4, @parentId,
'', '', '', 0
);
INSERT INTO system_menu(
name, permission, type, sort, parent_id,
path, icon, component, status
)
VALUES (
'学生导出', 'infra:student:export', 3, 5, @parentId,
'', '', '', 0
);

View File

@ -0,0 +1,41 @@
import type { PageParam, PageResult } from '@/http/types'
import { http } from '@/http/http'
/** 学生信息 */
export interface Student {
id?: number
name: string
description: string
birthday: Date
sex: number
enabled: boolean
avatar: string
video: string
memo: string
createTime?: Date
}
/** 获取学生分页列表 */
export function getStudentPage(params: PageParam) {
return http.get<PageResult<Student>>('/infra/student/page', params)
}
/** 获取学生详情 */
export function getStudent(id: number) {
return http.get<Student>(`/infra/student/get?id=${id}`)
}
/** 创建学生 */
export function createStudent(data: Student) {
return http.post<number>('/infra/student/create', data)
}
/** 更新学生 */
export function updateStudent(data: Student) {
return http.put<boolean>('/infra/student/update', data)
}
/** 删除学生 */
export function deleteStudent(id: number) {
return http.delete<boolean>(`/infra/student/delete?id=${id}`)
}

View File

@ -0,0 +1,142 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
<wd-cell title="编号" :value="formData?.id ?? '-'" />
<wd-cell title="名字" :value="formData?.name ?? '-'" />
<wd-cell title="简介" :value="formData?.description ?? '-'" />
<wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
<wd-cell title="性别">
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
</wd-cell>
<wd-cell title="是否有效">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
</wd-cell>
<wd-cell title="头像" :value="formData?.avatar ?? '-'" />
<wd-cell title="附件" :value="formData?.video ?? '-'" />
<wd-cell title="备注" :value="formData?.memo ?? '-'" />
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
</wd-cell-group>
</view>
<!-- 底部操作按钮 -->
<view class="yd-detail-footer">
<view class="yd-detail-footer-actions">
<wd-button
v-if="hasAccessByCodes(['infra:student:update'])"
class="flex-1" type="warning" @click="handleEdit"
>
编辑
</wd-button>
<wd-button
v-if="hasAccessByCodes(['infra:student:delete'])"
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
>
删除
</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { deleteStudent, getStudent } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const toast = useToast()
const formData = ref<Student>() // 详情数据
const deleting = ref(false) // 删除状态
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载学生详情 */
async function getDetail() {
if (!props.id || deleting.value) {
return
}
try {
toast.loading('加载中...')
formData.value = await getStudent(props.id)
} finally {
toast.close()
}
}
/** 编辑学生 */
function handleEdit() {
uni.navigateTo({
url: `/pages-infra/demo/form/index?id=${props.id}`,
})
}
/** 删除学生 */
function handleDelete() {
if (!props.id) {
return
}
uni.showModal({
title: '提示',
content: '确定要删除该学生吗?',
success: async (res) => {
if (!res.confirm) {
return
}
deleting.value = true
try {
await deleteStudent(props.id)
toast.success('删除成功')
uni.$emit('infra:demo:reload')
setTimeout(() => {
handleBack()
}, 500)
} finally {
deleting.value = false
}
},
})
}
/** 初始化 */
onMounted(() => {
uni.$on('infra:demo:reload', getDetail)
getDetail()
})
/** 卸载 */
onUnload(() => {
uni.$off('infra:demo:reload', getDetail)
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,194 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group border>
<wd-input
v-model="formData.name"
label="名字"
label-width="180rpx"
prop="name"
clearable
placeholder="请输入名字"
/>
<wd-textarea
v-model="formData.description"
label="简介"
label-width="180rpx"
placeholder="请输入简介"
:maxlength="200"
show-word-limit
clearable
/>
<wd-datetime-picker
v-model="formData.birthday"
type="datetime"
label="出生日期"
label-width="180rpx"
prop="birthday"
/>
<wd-cell title="性别" title-width="180rpx" prop="sex" center>
<wd-radio-group v-model="formData.sex" shape="button">
<wd-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-cell title="是否有效" title-width="180rpx" prop="enabled" center>
<wd-radio-group v-model="formData.enabled" shape="button">
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-input
v-model="formData.avatar"
label="头像"
label-width="180rpx"
prop="avatar"
clearable
placeholder="请输入头像"
/>
<wd-input
v-model="formData.video"
label="附件"
label-width="180rpx"
prop="video"
clearable
placeholder="请输入附件"
/>
<wd-input
v-model="formData.memo"
label="备注"
label-width="180rpx"
prop="memo"
clearable
placeholder="请输入备注"
/>
</wd-cell-group>
</wd-form>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
import type { Student } from '@/api/infra/demo'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { createStudent, getStudent, updateStudent } from '@/api/infra/demo'
import { getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
const formLoading = ref(false) // 表单提交状态
const formData = ref<Student>({
id: undefined,
name: '',
description: '',
birthday: undefined,
sex: 0,
enabled: false,
avatar: '',
video: '',
memo: '',
}) // 表单数据
const formRules = {
name: [{ required: true, message: '名字不能为空' }],
description: [{ required: true, message: '简介不能为空' }],
birthday: [{ required: true, message: '出生日期不能为空' }],
sex: [{ required: true, message: '性别不能为空' }],
enabled: [{ required: true, message: '是否有效不能为空' }],
avatar: [{ required: true, message: '头像不能为空' }],
video: [{ required: true, message: '附件不能为空' }],
memo: [{ required: true, message: '备注不能为空' }],
} // 表单校验规则
const formRef = ref<FormInstance>() // 表单组件引用
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-infra/demo/index')
}
/** 加载学生详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await getStudent(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value.validate()
if (!valid) {
return
}
formLoading.value = true
try {
if (props.id) {
await updateStudent(formData.value)
toast.success('修改成功')
} else {
await createStudent(formData.value)
toast.success('新增成功')
}
uni.$emit('infra:demo:reload')
setTimeout(() => {
handleBack()
}, 500)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(() => {
getDetail()
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,169 @@
<template>
<view class="yd-page-container yd-page-container-paging">
<!-- 顶部导航栏 -->
<wd-navbar
title="学生管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 学生列表 -->
<z-paging
ref="pagingRef"
v-model="list"
:fixed="false"
class="min-h-0 flex-1"
:default-page-size="10"
:refresher-enabled="true"
:inside-more="true"
:loading-more-default-as-loading="true"
empty-view-text="暂无学生数据"
@query="queryList"
>
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.name }}
</view>
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">简介:</text>
<text class="line-clamp-1">{{ item.description }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">出生日期:</text>
<text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">是否有效:</text>
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">头像:</text>
<text class="line-clamp-1">{{ item.avatar }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">附件:</text>
<text class="line-clamp-1">{{ item.video }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">备注:</text>
<text class="line-clamp-1">{{ item.memo }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">创建时间:</text>
<text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
</view>
</view>
</view>
</z-paging>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['infra:student:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
</view>
</template>
<script lang="ts" setup>
import type { Student } from '@/api/infra/demo'
import { onUnload } from '@dcloudio/uni-app'
import { onMounted, ref } from 'vue'
import { getStudentPage } from '@/api/infra/demo'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDateTime } from '@/utils/date'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const list = ref<Student[]>([]) // 列表数据
const pagingRef = ref<any>() // 分页组件引用
const queryParams = ref<Record<string, any>>({}) // 查询参数
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询学生列表 */
async function queryList(pageNo: number, pageSize: number) {
try {
const params = {
...queryParams.value,
pageNo,
pageSize,
}
const data = await getStudentPage(params)
pagingRef.value?.completeByTotal(data.list, data.total)
} catch {
pagingRef.value?.complete(false)
}
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = { ...data }
reload()
}
/** 重置按钮操作 */
function handleReset() {
handleQuery()
}
/** 重新加载 */
function reload() {
pagingRef.value?.reload()
}
/** 新增学生 */
function handleAdd() {
uni.navigateTo({
url: '/pages-infra/demo/form/index',
})
}
/** 查看详情 */
function handleDetail(item: Student) {
uni.navigateTo({
url: `/pages-infra/demo/detail/index?id=${item.id}`,
})
}
/** 初始化 */
onMounted(() => {
uni.$on('infra:demo:reload', reload)
})
/** 卸载 */
onUnload(() => {
uni.$off('infra:demo:reload', reload)
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,190 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
名字
</view>
<wd-input
v-model="formData.name"
placeholder="请输入名字"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
出生日期
</view>
<wd-input
v-model="formData.birthday"
placeholder="请输入出生日期"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
性别
</view>
<wd-radio-group v-model="formData.sex" shape="button">
<wd-radio :value="-1">
全部
</wd-radio>
<wd-radio
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
是否有效
</view>
<wd-radio-group v-model="formData.enabled" shape="button">
<wd-radio :value="-1">
全部
</wd-radio>
<wd-radio
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
创建时间
</view>
<view class="yd-search-form-date-range-container">
<view @click="visibleCreateTime[0] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
</view>
</view>
-
<view @click="visibleCreateTime[1] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
</view>
</view>
</view>
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
确定
</wd-button>
</view>
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
确定
</wd-button>
</view>
</view>
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { getNavbarHeight } from '@/utils'
import { formatDate, formatDateRange } from '@/utils/date'
import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false) // 搜索弹窗显示状态
const formData = reactive({
name: undefined as string | undefined,
birthday: undefined as string | undefined,
sex: -1, // -1 表示全部
enabled: -1 as -1 | boolean, // -1 表示全部
createTime: [undefined, undefined] as [number | undefined, number | undefined],
}) // 搜索表单数据
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
if (formData.name) {
conditions.push(`名字:${formData.name}`)
}
if (formData.birthday) {
conditions.push(`出生日期:${formData.birthday}`)
}
if (formData.sex !== -1) {
conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
}
if (formData.enabled !== -1) {
conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
})
const visibleCreateTime = ref<[boolean, boolean]>([false, false]) // 创建时间选择器状态
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()]) // 创建时间临时值
/** 确认创建时间开始日期 */
function handleCreateTime0Confirm() {
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
visibleCreateTime.value[0] = false
}
/** 确认创建时间结束日期 */
function handleCreateTime1Confirm() {
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
visibleCreateTime.value[1] = false
}
/** 搜索按钮操作 */
function handleSearch() {
visible.value = false
emit('search', {
...formData,
sex: formData.sex === -1 ? undefined : formData.sex,
enabled: formData.enabled === -1 ? undefined : formData.enabled,
createTime: formatDateRange(formData.createTime),
})
}
/** 重置按钮操作 */
function handleReset() {
formData.name = undefined
formData.birthday = undefined
formData.sex = -1
formData.enabled = -1
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
}
</script>

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