diff --git a/.gitignore b/.gitignore index 09ec36308..49330ee16 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ rebel.xml application-my.yaml /yudao-ui-app/unpackage/ +**/.DS_Store diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png index b4a55f547..552ed59b4 100644 Binary files a/.image/common/ai-feature.png and b/.image/common/ai-feature.png differ diff --git a/pom.xml b/pom.xml index 43da0c8d0..142869ff9 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.4.0-SNAPSHOT + 2.4.1-SNAPSHOT 17 ${java.version} diff --git a/script/idea/http-client.env.json b/script/idea/http-client.env.json index 4a4cb5221..e6e8a7cf8 100644 --- a/script/idea/http-client.env.json +++ b/script/idea/http-client.env.json @@ -2,16 +2,16 @@ "local": { "baseUrl": "http://127.0.0.1:48080/admin-api", "token": "test1", - "adminTenentId": "1", + "adminTenantId": "1", "appApi": "http://127.0.0.1:48080/app-api", "appToken": "test247", - "appTenentId": "1" + "appTenantId": "1" }, "gateway": { "baseUrl": "http://127.0.0.1:8888/admin-api", "token": "test1", - "adminTenentId": "1", + "adminTenantId": "1", "appApi": "http://127.0.0.1:8888/app-api", "appToken": "test1", diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 0c810bef8..bd9cd6d4a 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -11,7 +11,7 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 31/12/2024 09:16:18 + Date: 17/03/2025 13:14:16 */ SET NAMES utf8mb4; @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 21226 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 21482 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; -- ---------------------------- -- Records of infra_api_error_log @@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 2538 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; -- ---------------------------- -- Records of infra_codegen_column @@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 191 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; -- ---------------------------- -- Records of infra_codegen_table @@ -250,7 +250,7 @@ CREATE TABLE `infra_file` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1577 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1657 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; -- ---------------------------- -- Records of infra_file @@ -444,7 +444,7 @@ CREATE TABLE `system_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1683 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; +) ENGINE = InnoDB AUTO_INCREMENT = 3000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; -- ---------------------------- -- Records of system_dict_data @@ -831,7 +831,7 @@ 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 (1551, 2, '描述模式', '2', 'ai_generate_mode', 0, '', '', '', '1', '2024-06-27 22:46:37', '1', '2024-06-28 01:22:24', 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 (1552, 8, 'Suno', 'Suno', 'ai_platform', 0, '', '', '', '1', '2024-06-29 09:13:36', '1', '2024-06-29 09:13:41', 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 (1553, 9, 'DeepSeek', 'DeepSeek', 'ai_platform', 0, '', '', '', '1', '2024-07-06 12:04:30', '1', '2024-07-06 12:05:20', 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 (1554, 10, '智谱', 'ZhiPu', 'ai_platform', 0, '', '', '', '1', '2024-07-06 18:00:35', '1', '2024-07-06 18:00: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 (1554, 13, '智谱', 'ZhiPu', 'ai_platform', 0, '', '', '', '1', '2024-07-06 18:00:35', '1', '2025-02-24 20:18:41', 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 (1555, 4, '长', '4', 'ai_write_length', 0, '', '', '', '1', '2024-07-07 15:49:03', '1', '2024-07-07 15:49:03', 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 (1556, 5, '段落', '5', 'ai_write_format', 0, '', '', '', '1', '2024-07-07 15:49:54', '1', '2024-07-07 15:49:54', 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 (1557, 6, '文章', '6', 'ai_write_format', 0, '', '', '', '1', '2024-07-07 15:50:05', '1', '2024-07-07 15:50:05', b'0'); @@ -870,31 +870,190 @@ 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 (1591, 4, '七牛云', 'QINIU', 'system_sms_channel_code', 0, '', '', '', '1', '2024-08-31 08:45:03', '1', '2024-08-31 08:45:24', 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 (1592, 3, '新人券', '3', 'promotion_coupon_take_type', 0, 'info', '', '新人注册后,自动发放', '1', '2024-09-03 11:57:16', '1', '2024-09-03 11:57:28', 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 (1593, 5, '微信零钱', '5', 'brokerage_withdraw_type', 0, '', '', '自动打款', '1', '2024-10-13 11:06:48', '1', '2024-10-13 11:06:59', 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 (1655, 0, '标准数据格式(JSON)', '0', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:26', '1', '2024-09-06 14:31:02', 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 (1656, 1, '透传/自定义', '1', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:37', '1', '2024-09-06 14:30:54', 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 (1657, 0, '直连设备', '0', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:54:58', '1', '2024-09-06 21:57:01', 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 (1658, 2, '网关设备', '2', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:08', '1', '2024-09-06 21:56:46', 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 (1659, 1, '网关子设备', '1', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:20', '1', '2024-09-06 21:57:10', 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 (1661, 1, '已发布', '1', 'iot_product_status', 0, 'success', '', '', '1', '2024-08-10 12:10:33', '1', '2024-09-06 22:06:22', 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 (1663, 0, '开发中', '0', 'iot_product_status', 0, 'default', '', '', '1', '2024-08-10 14:19:18', '1', '2024-09-07 10:58:07', 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 (1665, 0, '弱校验', '0', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:05:48', '1', '2024-09-06 22:02:44', 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 (1666, 1, '免校验', '1', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:06:03', '1', '2024-09-06 22:02:51', 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 (1667, 0, 'Wi-Fi', '0', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:04:47', '1', '2024-09-06 22:04: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 (1668, 1, '蜂窝(2G / 3G / 4G / 5G)', '1', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:14', '1', '2024-09-06 22:05:14', 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 (1669, 2, '以太网', '2', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:35', '1', '2024-09-06 22:05: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 (1670, 3, '其他', '3', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:52', '1', '2024-09-06 22:05:52', 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 (1671, 0, '自定义', '0', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:10', '1', '2024-09-06 22:26:10', 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 (1672, 1, 'Modbus', '1', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:21', '1', '2024-09-06 22:26:21', 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 (1673, 2, 'OPC UA', '2', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:31', '1', '2024-09-06 22:26: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 (1674, 3, 'ZigBee', '3', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:39', '1', '2024-09-06 22:26:39', 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 (1675, 4, 'BLE', '4', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:48', '1', '2024-09-06 22:26:48', 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 (1676, 0, '未激活', '0', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2024-09-21 08:13: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 (1677, 1, '在线', '1', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2024-09-21 08:13:48', 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 (1678, 2, '离线', '2', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2024-09-21 08:13:59', 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 (1679, 3, '已禁用', '3', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:14:13', '1', '2024-09-21 08:14:13', 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 (1680, 1, '属性', '1', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:01', '1', '2024-09-29 20:09:41', 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 (1681, 2, '服务', '2', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:11', '1', '2024-09-29 20:08:23', 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 (1682, 3, '事件', '3', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:20', '1', '2024-09-29 20:08:20', 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 (1683, 10, '字节豆包', 'DouBao', 'ai_platform', 0, '', '', '', '1', '2025-02-23 19:51:40', '1', '2025-02-23 19:52:02', 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 (1684, 11, '腾讯混元', 'HunYuan', 'ai_platform', 0, '', '', '', '1', '2025-02-23 20:58:04', '1', '2025-02-23 20:58:04', 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 (1685, 12, '硅基流动', 'SiliconFlow', 'ai_platform', 0, '', '', '', '1', '2025-02-24 20:19:09', '1', '2025-02-24 20:19:09', 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 (1686, 1, '聊天', '1', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:26:34', '1', '2025-03-03 12:26: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 (1687, 2, '图像', '2', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:27:23', '1', '2025-03-03 12:27:23', 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 (1688, 3, '音频', '3', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:27:51', '1', '2025-03-03 12:27:51', 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 (1689, 4, '视频', '4', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:03', '1', '2025-03-03 12:28:03', 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 (1690, 5, '向量', '5', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:15', '1', '2025-03-03 12:28: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 (1691, 6, '重排', '6', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:26', '1', '2025-03-03 12:28:26', 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 (1692, 14, 'MiniMax', 'MiniMax', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:04:51', '1', '2025-03-11 20:04:51', 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 (1693, 15, '月之暗灭', 'Moonshot', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:05:08', '1', '2025-03-11 20:05:08', 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 (2000, 0, '标准数据格式(JSON)', '0', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:26', '1', '2025-03-17 09:28: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 (2001, 1, '透传/自定义', '1', 'iot_data_format', 0, 'default', '', '', '1', '2024-08-10 11:53:37', '1', '2025-03-17 09:28:19', 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 (2002, 0, '直连设备', '0', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:54:58', '1', '2025-03-17 09:28:22', 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 (2003, 2, '网关设备', '2', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:08', '1', '2025-03-17 09:28:28', 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 (2004, 1, '网关子设备', '1', 'iot_product_device_type', 0, 'default', '', '', '1', '2024-08-10 11:55:20', '1', '2025-03-17 09:28: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 (2005, 1, '已发布', '1', 'iot_product_status', 0, 'success', '', '', '1', '2024-08-10 12:10:33', '1', '2025-03-17 09:28: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 (2006, 0, '开发中', '0', 'iot_product_status', 0, 'default', '', '', '1', '2024-08-10 14:19:18', '1', '2025-03-17 09:28:39', 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 (2007, 0, '弱校验', '0', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:05:48', '1', '2025-03-17 09:28:41', 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 (2008, 1, '免校验', '1', 'iot_validate_type', 0, '', '', '', '1', '2024-09-06 20:06:03', '1', '2025-03-17 09:28:44', 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 (2009, 0, 'Wi-Fi', '0', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:04:47', '1', '2025-03-17 09:28: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 (2010, 1, '蜂窝(2G / 3G / 4G / 5G)', '1', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:14', '1', '2025-03-17 09:28:49', 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 (2011, 2, '以太网', '2', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:35', '1', '2025-03-17 09:28:51', 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 (2012, 3, '其他', '3', 'iot_net_type', 0, '', '', '', '1', '2024-09-06 22:05:52', '1', '2025-03-17 09:28:54', 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 (2013, 0, '自定义', '0', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:10', '1', '2025-03-17 09:28:56', 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 (2014, 1, 'Modbus', '1', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:21', '1', '2025-03-17 09:28:58', 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 (2015, 2, 'OPC UA', '2', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:31', '1', '2025-03-17 09:29:00', 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 (2016, 3, 'ZigBee', '3', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:39', '1', '2025-03-17 09:29:04', 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 (2017, 4, 'BLE', '4', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:48', '1', '2025-03-17 09:29:06', 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 (2018, 0, '未激活', '0', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2025-03-17 09:29:09', 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 (2019, 1, '在线', '1', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2025-03-17 09:29: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 (2020, 2, '离线', '2', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2025-03-17 09:29:14', 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 (2021, 1, '属性', '1', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:01', '1', '2025-03-17 09:29:24', 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 (2022, 2, '服务', '2', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:11', '1', '2025-03-17 09:29:27', 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 (2023, 3, '事件', '3', 'iot_thing_model_type', 0, '', '', '', '1', '2024-09-29 20:03:20', '1', '2025-03-17 09:29:29', 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 (2024, 1, 'JAR 部署', '0', 'iot_plugin_deploy_type', 0, '', '', '', '1', '2024-12-13 10:55:32', '1', '2025-03-17 09:29:32', 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 (2025, 2, '独立部署', '1', 'iot_plugin_deploy_type', 0, '', '', '', '1', '2024-12-13 10:55:43', '1', '2025-03-17 09:29: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 (2026, 0, '停止', '0', 'iot_plugin_status', 0, 'danger', '', '', '1', '2024-12-13 11:07:37', '1', '2025-03-17 09:29:37', 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 (2027, 1, '运行', '1', 'iot_plugin_status', 0, '', '', '', '1', '2024-12-13 11:07:45', '1', '2025-03-17 09:34:17', 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 (2028, 0, '普通插件', '0', 'iot_plugin_type', 0, '', '', '', '1', '2024-12-13 11:08:32', '1', '2025-03-17 09:34:19', 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 (2029, 1, '设备插件', '1', 'iot_plugin_type', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:22', 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 (2030, 1, '升每分钟', 'L/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:24', 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 (2031, 2, '毫克每千克', 'mg/kg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:27', 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 (2032, 3, '浊度', 'NTU', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34: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 (2033, 4, 'PH值', 'pH', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:36', 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 (2034, 5, '土壤EC值', 'dS/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:34:43', 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 (2035, 6, '太阳总辐射', 'W/㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:20', 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 (2036, 7, '降雨量', 'mm/hour', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:24', 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 (2037, 8, '乏', 'var', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:27', 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 (2038, 9, '厘泊', 'cP', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:36:33', 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 (2039, 10, '饱和度', 'aw', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:11', 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 (2040, 11, '个', 'pcs', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:19', 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 (2041, 12, '厘斯', 'cst', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:22', 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 (2042, 13, '巴', 'bar', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:24', 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 (2043, 14, '纳克每升', 'ppt', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:27', 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 (2044, 15, '微克每升', 'ppb', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37: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 (2045, 16, '微西每厘米', 'uS/cm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37: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 (2046, 17, '牛顿每库仑', 'N/C', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:38', 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 (2047, 18, '伏特每米', 'V/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:43', 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 (2048, 19, '滴速', 'ml/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:46', 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 (2049, 20, '毫米汞柱', 'mmHg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:48', 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 (2050, 21, '血糖', 'mmol/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:37:54', 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 (2051, 22, '毫米每秒', 'mm/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:02', 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 (2052, 23, '转每分钟', 'turn/m', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:07', 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 (2053, 24, '次', 'count', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:09', 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 (2054, 25, '档', 'gear', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:11', 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 (2055, 26, '步', 'stepCount', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:13', 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 (2056, 27, '标准立方米每小时', 'Nm3/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38: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 (2057, 28, '千伏', 'kV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:20', 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 (2058, 29, '千伏安', 'kVA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:38:24', 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 (2060, 30, '千乏', 'kVar', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2061, 31, '微瓦每平方厘米', 'uw/cm2', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2062, 32, '只', '只', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2063, 33, '相对湿度', '%RH', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2064, 34, '立方米每秒', 'm³/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2065, 35, '公斤每秒', 'kg/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2066, 36, '转每分钟', 'r/min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2067, 37, '吨每小时', 't/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2068, 38, '千卡每小时', 'KCL/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2069, 39, '升每秒', 'L/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2070, 40, '兆帕', 'Mpa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2071, 41, '立方米每小时', 'm³/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2072, 42, '千乏时', 'kvarh', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2073, 43, '微克每升', 'μg/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2074, 44, '千卡路里', 'kcal', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2075, 45, '吉字节', 'GB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2076, 46, '兆字节', 'MB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2077, 47, '千字节', 'KB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2078, 48, '字节', 'B', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2079, 49, '微克每平方分米每天', 'μg/(d㎡·d)', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2080, 50, '无', '', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2081, 51, '百万分率', 'ppm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2082, 52, '像素', 'pixel', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2083, 53, '照度', 'Lux', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2084, 54, '重力加速度', 'grav', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2085, 55, '分贝', 'dB', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2086, 56, '百分比', '%', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2087, 57, '流明', 'lm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2088, 58, '比特', 'bit', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2089, 59, '克每毫升', 'g/mL', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2090, 60, '克每升', 'g/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2091, 61, '毫克每升', 'mg/L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2092, 62, '微克每立方米', 'μg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2093, 63, '毫克每立方米', 'mg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2094, 64, '克每立方米', 'g/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2095, 65, '千克每立方米', 'kg/m³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2096, 66, '纳法', 'nF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2097, 67, '皮法', 'pF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2098, 68, '微法', 'μF', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2099, 69, '法拉', 'F', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2100, 70, '欧姆', 'Ω', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2101, 71, '微安', 'μA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2102, 72, '毫安', 'mA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2103, 73, '千安', 'kA', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2104, 74, '安培', 'A', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2105, 75, '毫伏', 'mV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2106, 76, '伏特', 'V', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2107, 77, '毫秒', 'ms', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2108, 78, '秒', 's', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2109, 79, '分钟', 'min', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2110, 80, '小时', 'h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2111, 81, '日', 'day', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2112, 82, '周', 'week', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2113, 83, '月', 'month', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2114, 84, '年', 'year', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2115, 85, '节', 'kn', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2116, 86, '千米每小时', 'km/h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2117, 87, '米每秒', 'm/s', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2118, 88, '秒', '″', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2119, 89, '分', '′', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2120, 90, '度', '°', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2121, 91, '弧度', 'rad', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2122, 92, '赫兹', 'Hz', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2123, 93, '微瓦', 'μW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2124, 94, '毫瓦', 'mW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2125, 95, '千瓦特', 'kW', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2126, 96, '瓦特', 'W', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2127, 97, '卡路里', 'cal', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2128, 98, '千瓦时', 'kW·h', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2129, 99, '瓦时', 'Wh', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2130, 100, '电子伏', 'eV', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2131, 101, '千焦', 'kJ', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2132, 102, '焦耳', 'J', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2133, 103, '华氏度', '℉', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2134, 104, '开尔文', 'K', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2135, 105, '吨', 't', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2136, 106, '摄氏度', '°C', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2137, 107, '毫帕', 'mPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2138, 108, '百帕', 'hPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2139, 109, '千帕', 'kPa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2140, 110, '帕斯卡', 'Pa', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2141, 111, '毫克', 'mg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2142, 112, '克', 'g', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2143, 113, '千克', 'kg', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2144, 114, '牛', 'N', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2145, 115, '毫升', 'mL', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2146, 116, '升', 'L', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2147, 117, '立方毫米', 'mm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2148, 118, '立方厘米', 'cm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2149, 119, '立方千米', 'km³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2150, 120, '立方米', 'm³', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2151, 121, '公顷', 'h㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2152, 122, '平方厘米', 'c㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2153, 123, '平方毫米', 'm㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2154, 124, '平方千米', 'k㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2155, 125, '平方米', '㎡', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2156, 126, '纳米', 'nm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2157, 127, '微米', 'μm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2158, 128, '毫米', 'mm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2159, 129, '厘米', 'cm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2160, 130, '分米', 'dm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2161, 131, '千米', 'km', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2162, 132, '米', 'm', 'iot_thing_model_unit', 0, '', '', '', '1', '2024-12-13 11:08:41', '1', '2025-03-17 09:40:46', 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 (2163, 1, '输入', '1', 'iot_data_bridge_direction_enum', 0, 'primary', '', '', '1', '2025-03-09 12:38:24', '1', '2025-03-17 09:40:46', 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 (2164, 2, '输出', '2', 'iot_data_bridge_direction_enum', 0, 'primary', '', '', '1', '2025-03-09 12:38:36', '1', '2025-03-17 09:40:46', 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 (2165, 1, 'HTTP', '1', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:39:54', '1', '2025-03-17 09:40:46', 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 (2166, 2, 'TCP', '2', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:06', '1', '2025-03-17 09:40:46', 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 (2167, 3, 'WEBSOCKET', '3', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:24', '1', '2025-03-17 09:40:46', 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 (2168, 10, 'MQTT', '10', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:40:37', '1', '2025-03-17 09:40:46', 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 (2169, 20, 'DATABASE', '20', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:05', '1', '2025-03-17 09:40:46', 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 (2170, 21, 'REDIS_STREAM', '21', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:18', '1', '2025-03-17 09:40:46', 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 (2171, 30, 'ROCKETMQ', '30', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:30', '1', '2025-03-17 09:40:46', 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 (2172, 31, 'RABBITMQ', '31', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:47', '1', '2025-03-17 09:40:46', 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 (2173, 32, 'KAFKA', '32', 'iot_data_bridge_type_enum', 0, 'primary', '', '', '1', '2025-03-09 12:41:59', '1', '2025-03-17 09:40:46', b'0'); COMMIT; -- ---------------------------- @@ -914,7 +1073,7 @@ CREATE TABLE `system_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 640 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; -- ---------------------------- -- Records of system_dict_type @@ -1013,14 +1172,21 @@ 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 (627, '写作格式', 'ai_write_format', 0, '', '1', '2024-07-07 15:14:34', '1', '2024-07-07 15:14:34', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (630, 'IOT 接入网关协议', 'iot_protocol_type', 0, '', '1', '2024-09-06 22:20:17', '1', '2024-09-06 22:20:17', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (631, 'IOT 设备状态', 'iot_device_status', 0, '', '1', '2024-09-21 08:12:55', '1', '2024-09-21 08:12:55', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (632, 'IOT 物模型功能类型', 'iot_product_function_type', 0, '', '1', '2024-09-29 20:02:36', '1', '2024-09-29 20:09:26', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (634, 'IOT 数据格式', 'iot_data_format', 0, '', '1', '2024-08-10 11:52:58', '1', '2024-09-06 14:30:14', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (635, 'IOT 产品设备类型', 'iot_product_device_type', 0, '', '1', '2024-08-10 11:54:30', '1', '2024-08-10 04:06:56', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (637, 'IOT 产品状态', 'iot_product_status', 0, '', '1', '2024-08-10 12:06:09', '1', '2024-08-10 12:06:09', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (638, 'IOT 数据校验级别', 'iot_validate_type', 0, '', '1', '2024-09-06 20:05:13', '1', '2024-09-06 20:05:13', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (639, 'IOT 联网方式', 'iot_net_type', 0, '', '1', '2024-09-06 22:04:13', '1', '2024-09-06 22:04:13', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (640, 'AI 模型类型', 'ai_model_type', 0, '', '1', '2025-03-03 12:24:07', '1', '2025-03-03 12:24:07', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1000, 'IoT 数据格式', 'iot_data_format', 0, '', '1', '2024-08-10 11:52:58', '1', '2025-03-17 09:25:06', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1001, 'IoT 产品设备类型', 'iot_product_device_type', 0, '', '1', '2024-08-10 11:54:30', '1', '2025-03-17 09:25:08', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1002, 'IoT 产品状态', 'iot_product_status', 0, '', '1', '2024-08-10 12:06:09', '1', '2025-03-17 09:25:10', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1003, 'IoT 数据校验级别', 'iot_validate_type', 0, '', '1', '2024-09-06 20:05:13', '1', '2025-03-17 09:25:12', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1004, 'IoT 联网方式', 'iot_net_type', 0, '', '1', '2024-09-06 22:04:13', '1', '2025-03-17 09:25:14', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1005, 'IoT 接入网关协议', 'iot_protocol_type', 0, '', '1', '2024-09-06 22:20:17', '1', '2025-03-17 09:25:16', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1006, 'IoT 设备状态', 'iot_device_state', 0, '', '1', '2024-09-21 08:12:55', '1', '2025-03-17 09:25:19', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1007, 'IoT 物模型功能类型', 'iot_thing_model_type', 0, '', '1', '2024-09-29 20:02:36', '1', '2025-03-17 09:25:24', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1008, 'IoT 插件部署方式', 'iot_plugin_deploy_type', 0, '', '1', '2024-12-13 10:55:13', '1', '2025-03-17 09:25:27', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1009, 'IoT 插件状态', 'iot_plugin_status', 0, '', '1', '2024-12-13 11:05:34', '1', '2025-03-17 09:25:30', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1010, 'IoT 插件类型', 'iot_plugin_type', 0, '', '1', '2024-12-13 11:08:19', '1', '2025-03-17 09:25:32', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1011, 'IoT 物模型单位', 'iot_thing_model_unit', 0, '', '1', '2024-12-25 17:36:46', '1', '2025-03-17 09:25:35', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1012, 'IoT 数据桥接的方向枚举', 'iot_data_bridge_direction_enum', 0, '', '1', '2025-03-09 12:37:40', '1', '2025-03-17 09:25:39', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1013, 'IoT 数据桥梁的类型枚举', 'iot_data_bridge_type_enum', 0, '', '1', '2025-03-09 12:39:36', '1', '2025-03-17 09:25:43', b'0', '1970-01-01 00:00:00'); COMMIT; -- ---------------------------- @@ -1044,7 +1210,7 @@ CREATE TABLE `system_login_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3415 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; +) ENGINE = InnoDB AUTO_INCREMENT = 3446 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; -- ---------------------------- -- Records of system_login_log @@ -1175,16 +1341,16 @@ CREATE TABLE `system_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2913 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; +) ENGINE = InnoDB AUTO_INCREMENT = 5000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; -- ---------------------------- -- Records of system_menu -- ---------------------------- BEGIN; -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 (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-06-18 01:19:41', 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 (1, '系统管理', '', 1, 10, 0, '/system', 'ep:tools', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:27', 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 (2, '基础设施', '', 1, 20, 0, '/infra', 'ep:monitor', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-03-01 08:28:40', 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 (5, 'OA 示例', '', 1, 40, 1185, 'oa', 'fa:road', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-09-20 16:26:19', '1', '2024-02-29 12:38:13', 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 (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:02:04', 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 (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'ep:avatar', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2025-03-15 21:30:41', 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 (101, '角色管理', '', 2, 2, 1, 'role', 'ep:user', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-05-01 18:35: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 (102, '菜单管理', '', 2, 3, 1, 'menu', 'ep:menu', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:03:50', 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 (103, '部门管理', '', 2, 4, 1, 'dept', 'fa:address-card', 'system/dept/index', 'SystemDept', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:28', b'0'); @@ -1373,7 +1539,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 (1241, '文件配置删除', 'infra:file-config:delete', 3, 4, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', 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 (1242, '文件配置导出', 'infra:file-config:export', 3, 5, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', 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 (1243, '文件管理', '', 2, 6, 2, 'file', 'ep:files', NULL, '', 0, b'1', b'1', b'1', '1', '2022-03-16 23:47:40', '1', '2024-04-23 00:02:11', 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 (1254, '作者动态', '', 1, 0, 0, 'https://www.iocoder.cn', 'ep:avatar', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-04-23 01:03:15', '1', '2024-09-06 09:19:42', 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 (1254, '作者动态', '', 1, 0, 0, 'https://www.iocoder.cn', 'ep:avatar', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-04-23 01:03:15', '104', '2025-01-04 10:59: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 (1255, '数据源配置', '', 2, 1, 2, 'data-source-config', 'ep:data-analysis', 'infra/dataSourceConfig/index', 'InfraDataSourceConfig', 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '1', '2024-02-29 08:51:25', 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 (1256, '数据源配置查询', 'infra:data-source-config:query', 3, 1, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', 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 (1257, '数据源配置创建', 'infra:data-source-config:create', 3, 2, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); @@ -1696,7 +1862,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 (2431, '回款计划更新', 'crm:receivable-plan:update', 3, 3, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', 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 (2432, '回款计划删除', 'crm:receivable-plan:delete', 3, 4, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', 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 (2433, '回款计划导出', 'crm:receivable-plan:export', 3, 5, 2428, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 11:18:09', '', '2023-10-29 11:18:09', 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 (2435, '商城装修', '', 2, 20, 2030, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', 'DiyTemplate', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', 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 (2435, '商城装修', '', 2, 20, 2030, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', '', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '1', '2025-03-15 21:34:33', 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 (2436, '装修模板', '', 2, 1, 2435, 'diy-template', 'fa6-solid:brush', 'mall/promotion/diy/template/index', 'DiyTemplate', 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', 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 (2437, '装修模板查询', 'promotion:diy-template:query', 3, 1, 2436, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', 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 (2438, '装修模板创建', 'promotion:diy-template:create', 3, 2, 2436, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-10-29 14:19:25', '', '2023-10-29 14:19:25', b'0'); @@ -1978,11 +2144,11 @@ 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 (2763, 'API 密钥创建', 'ai:api-key:create', 3, 2, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36: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 (2764, 'API 密钥更新', 'ai:api-key:update', 3, 3, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36:42', 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 (2765, 'API 密钥删除', 'ai:api-key:delete', 3, 4, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36:48', 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 (2767, '聊天模型', '', 2, 0, 2760, 'chat-model', 'fa-solid:abacus', 'ai/model/chatModel/index.vue', 'AiChatModel', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-10 22:44:16', 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 (2768, '聊天模型查询', 'ai:chat-model:query', 3, 1, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:02', 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 (2769, '聊天模型创建', 'ai:chat-model:create', 3, 2, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37: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 (2770, '聊天模型更新', 'ai:chat-model:update', 3, 3, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:18', 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 (2771, '聊天模型删除', 'ai:chat-model:delete', 3, 4, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:23', 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 (2767, '模型配置', '', 2, 0, 2760, 'model', 'fa-solid:abacus', 'ai/model/model/index.vue', 'AiModel', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:57:41', 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 (2768, '聊天模型查询', 'ai:model:query', 3, 1, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:19:46', 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 (2769, '聊天模型创建', 'ai:model:create', 3, 2, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:10', 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 (2770, '聊天模型更新', 'ai:model:update', 3, 3, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:14', 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 (2771, '聊天模型删除', 'ai:model:delete', 3, 4, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:27', 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 (2773, '聊天角色', '', 2, 0, 2760, 'chat-role', 'fa:user-secret', 'ai/model/chatRole/index.vue', 'AiChatRole', 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '1', '2024-05-13 20:41:45', 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 (2774, '聊天角色查询', 'ai:chat-role:query', 3, 1, 2773, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '', '2024-05-13 12:39:28', 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 (2775, '聊天角色创建', 'ai:chat-role:create', 3, 2, 2773, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '', '2024-05-13 12:39:28', b'0'); @@ -2008,7 +2174,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 (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24: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 (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-29 21:11: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 (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', 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 (2798, 'AI 思维导图', '', 2, 5, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2024-07-29 21:33:20', 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 (2798, 'AI 思维导图', '', 2, 6, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2025-03-02 18:57:31', 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 (2799, '导图管理', '', 2, 14, 2760, 'mind-map', 'fa:map', 'ai/mindmap/manager/index', 'AiMindMapManager', 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '1', '2024-08-10 17:24:28', 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 (2800, '思维导图查询', 'ai:mind-map:query', 3, 1, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', 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 (2801, '思维导图删除', 'ai:mind-map:delete', 3, 4, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0'); @@ -2024,27 +2190,70 @@ 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 (2811, '积分商城活动更新', 'promotion:point-activity:update', 3, 3, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49:10', 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 (2812, '积分商城活动删除', 'promotion:point-activity:delete', 3, 4, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49: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 (2813, '积分商城活动导出', 'promotion:point-activity:export', 3, 5, 2808, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-21 05:36:42', '1', '2024-09-22 14:49:27', 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 (2892, 'IOT 物联网', '', 1, 500, 0, '/iot', 'fa-solid:hdd', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:55:29', '1', '2024-08-10 09:55: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 (2893, '设备接入', '', 1, 1, 2892, 'device', 'ep:platform', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:57:56', '1', '2024-10-20 18:57:43', 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 (2894, '产品管理', '', 2, 0, 2893, 'product', '', 'iot/product/index', 'IoTProduct', 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '1', '2024-09-16 19:50:42', 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 (2895, '产品查询', 'iot:product:query', 3, 1, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', 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 (2896, '产品创建', 'iot:product:create', 3, 2, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', 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 (2897, '产品更新', 'iot:product:update', 3, 3, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', 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 (2898, '产品删除', 'iot:product:delete', 3, 4, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', 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 (2899, '产品导出', 'iot:product:export', 3, 5, 2894, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-08-10 02:38:02', 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 (2900, '设备管理', '', 2, 0, 2893, 'device', '', 'iot/device/index', 'IoTDevice', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:50: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 (2901, '设备查询', 'iot:device:query', 3, 1, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:00', 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 (2902, '设备创建', 'iot:device:create', 3, 2, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:09', 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 (2903, '设备更新', 'iot:device:update', 3, 3, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:18', 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 (2904, '设备删除', 'iot:device:delete', 3, 4, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:42', 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 (2905, '设备导出', 'iot:device:export', 3, 5, 2900, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-09-16 19:37:49', 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 (2906, 'IoT 产品物模型管理', '', 1, 0, 2893, 'think-model-function', '', '', '', 0, b'0', b'1', b'1', '', '2024-09-25 22:12:09', '1', '2024-09-29 20:52: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 (2907, 'IoT 产品物模型查询', 'iot:think-model-function:query', 3, 1, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', 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 (2908, 'IoT 产品物模型创建', 'iot:think-model-function:create', 3, 2, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', 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 (2909, 'IoT 产品物模型更新', 'iot:think-model-function:update', 3, 3, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', 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 (2910, 'IoT 产品物模型删除', 'iot:think-model-function:delete', 3, 4, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', 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 (2911, 'IoT 产品物模型导出', 'iot:think-model-function:export', 3, 5, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', 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 (2912, '创建推广员', 'trade:brokerage-user:create', 3, 7, 2346, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-12-01 14:32:39', '1', '2024-12-01 14:32:39', 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 (2913, '流程清理', 'bpm:model:clean', 3, 7, 1193, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-17 19:32:06', '1', '2025-01-17 19:32:06', 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 (2914, '积分商城活动关闭', 'promotion:point-activity:close', 3, 6, 2808, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-23 20:23:34', '1', '2025-01-23 20:23: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 (2915, 'AI 知识库', '', 2, 5, 2758, 'knowledge', 'ep:notebook', 'ai/knowledge/knowledge/index', 'AiKnowledge', 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '1', '2025-03-02 18:58: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 (2916, 'AI 知识库查询', 'ai:knowledge:query', 3, 1, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04: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 (2917, 'AI 知识库创建', 'ai:knowledge:create', 3, 2, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04: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 (2918, 'AI 知识库更新', 'ai:knowledge:update', 3, 3, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04: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 (2919, 'AI 知识库删除', 'ai:knowledge:delete', 3, 4, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04: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 (2920, '工具管理', '', 2, 0, 2760, 'tool', 'fa-solid:tools', 'ai/model/tool/index.vue', 'AiTool', 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '1', '2025-03-14 19:20:18', 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 (2921, '工具查询', 'ai:tool:query', 3, 1, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19: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 (2922, '工具创建', 'ai:tool:create', 3, 2, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19: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 (2923, '工具更新', 'ai:tool:update', 3, 3, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19: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 (2924, '工具删除', 'ai:tool:delete', 3, 4, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19: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 (4000, 'IoT 物联网', '', 1, 500, 0, '/iot', 'fa-solid:hdd', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:55:28', '1', '2024-12-07 15:58: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 (4001, '设备接入', '', 1, 2, 4000, 'device', 'ep:platform', '', '', 0, b'1', b'1', b'1', '1', '2024-08-10 09:57:56', '1', '2025-02-27 08:39:49', 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 (4002, '产品管理', '', 2, 2, 4001, 'product', 'fa-solid:tools', 'iot/product/product/index', 'IoTProduct', 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '1', '2024-12-07 18:47: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 (4003, '产品查询', 'iot:product:query', 3, 1, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:00', 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 (4004, '产品创建', 'iot:product:create', 3, 2, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:03', 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 (4005, '产品更新', 'iot:product:update', 3, 3, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:05', 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 (4006, '产品删除', 'iot:product:delete', 3, 4, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:06', 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 (4007, '产品导出', 'iot:product:export', 3, 5, 4002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 02:38:02', '', '2024-12-07 15:55:13', 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 (4008, '设备管理', '', 2, 4, 4001, 'device', 'fa:mobile', 'iot/device/device/index', 'IoTDevice', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-14 11:39: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 (4009, '设备查询', 'iot:device:query', 3, 1, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:40', 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 (4010, '设备创建', 'iot:device:create', 3, 2, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:41', 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 (4011, '设备更新', 'iot:device:update', 3, 3, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:42', 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 (4012, '设备删除', 'iot:device:delete', 3, 4, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:43', 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 (4013, '设备导出', 'iot:device:export', 3, 5, 4008, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-09-16 18:48:19', '1', '2024-12-07 15:55:44', 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 (4014, '产品分类', '', 2, 1, 4001, 'product-category', 'ep:notebook', 'iot/product/category/index', 'IotProductCategory', 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '1', '2024-12-07 16:31: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 (4015, '产品分类查询', 'iot:product-category:query', 3, 1, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01: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 (4016, '产品分类创建', 'iot:product-category:create', 3, 2, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01: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 (4017, '产品分类更新', 'iot:product-category:update', 3, 3, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01: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 (4018, '产品分类删除', 'iot:product-category:delete', 3, 4, 4014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-07 16:01:35', '', '2024-12-07 16:01: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 (4025, '插件管理', '', 2, 5, 4047, 'plugin-config', 'ep:folder-opened', 'iot/plugin/index', 'IoTPlugin', 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '1', '2025-02-05 22:23: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 (4026, '插件查询', 'iot:plugin-config:query', 3, 1, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:20', 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 (4027, '插件创建', 'iot:plugin-config:create', 3, 2, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:16', 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 (4028, '插件更新', 'iot:plugin-config:update', 3, 3, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23: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 (4029, '插件删除', 'iot:plugin-config:delete', 3, 4, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:09', 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 (4030, '插件导出', 'iot:plugin-config:export', 3, 5, 4025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-09 21:25:06', '', '2025-02-05 21:23:06', 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 (4031, '设备分组', '', 2, 3, 4001, 'device-group', 'fa-solid:layer-group', 'iot/device/group/index', 'IotDeviceGroup', 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '1', '2024-12-14 17:09:17', 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 (4032, '设备分组查询', 'iot:device-group:query', 3, 1, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08: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 (4033, '设备分组创建', 'iot:device-group:create', 3, 2, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08: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 (4034, '设备分组更新', 'iot:device-group:update', 3, 3, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08: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 (4035, '设备分组删除', 'iot:device-group:delete', 3, 4, 4031, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-14 17:08:29', '', '2024-12-14 17:08: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 (4036, '设备导入', 'iot:device:import', 3, 6, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-12-15 10:35:47', '1', '2024-12-15 10:35:47', 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 (4037, '产品物模型', '', 2, 2, 4001, 'thing-model', 'ep:mostly-cloudy', 'iot/thingmodel/index', 'IoTThingModel', 0, b'0', b'0', b'0', '', '2024-12-16 17:17:50', '1', '2024-12-27 11:03: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 (4038, '产品物模型功能查询', 'iot:thing-model:query', 3, 1, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:51', '', '2025-03-17 09:14:54', 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 (4039, '产品物模型功能创建', 'iot:thing-model:create', 3, 2, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:14:58', 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 (4040, '产品物模型功能更新', 'iot:thing-model:update', 3, 3, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:15:03', 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 (4041, '产品物模型功能删除', 'iot:thing-model:delete', 3, 4, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:52', '', '2025-03-17 09:15:06', 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 (4042, '产品物模型功能导出', 'iot:thing-model:export', 3, 5, 4037, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-12-16 17:17:53', '', '2025-03-17 09:15:09', 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 (4043, '设备上行', 'iot:device:upstream', 3, 7, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 04:40:16', '1', '2025-01-31 22:45: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 (4044, '设备属性查询', 'iot:device:property-query', 3, 10, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 11:52:54', '1', '2025-01-28 11:52:54', 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 (4045, '设备日志查询', 'iot:device:log-query', 3, 11, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-28 11:53:22', '1', '2025-01-28 11:53:22', 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 (4046, '设备下行', 'iot:device:downstream', 3, 8, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-31 22:46:11', '1', '2025-01-31 22:46:11', 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 (4047, '运维管理', '', 1, 2, 4000, 'operations', 'fa:cog', '', '', 0, b'1', b'1', b'1', '1', '2025-02-05 22:21:37', '1', '2025-02-05 22:22: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 (4048, '规则引擎', '', 1, 3, 4000, 'rule', 'fa-solid:cogs', '', '', 0, b'1', b'1', b'1', '1', '2025-02-11 14:10:54', '1', '2025-02-11 14:10:54', 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 (4049, '场景联动', '', 2, 1, 4048, 'scene', 'ep:link', 'iot/rule/scene/index', 'Scene', 0, b'1', b'1', b'1', '1', '2025-02-11 14:12:44', '1', '2025-02-12 10:15:36', 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 (4050, 'IoT首页', '', 2, 1, 4000, 'home', 'ep:home-filled', 'iot/home/index', 'IotHome', 0, b'1', b'1', b'1', '1', '2025-02-27 08:39:35', '1', '2025-02-27 08:40:28', 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 (4051, '数据桥梁', '', 2, 0, 4048, 'data-bridge', 'ep:guide', 'iot/rule/databridge/index', 'IotDataBridge', 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '1', '2025-03-09 13:47:51', 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 (4052, 'IoT 数据桥梁查询', 'iot:data-bridge:query', 3, 1, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', 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 (4053, 'IoT 数据桥梁创建', 'iot:data-bridge:create', 3, 2, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', 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 (4054, 'IoT 数据桥梁更新', 'iot:data-bridge:update', 3, 3, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:11', '', '2025-03-09 13:47:11', 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 (4055, 'IoT 数据桥梁删除', 'iot:data-bridge:delete', 3, 4, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:12', '', '2025-03-09 13:47: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 (4056, 'IoT 数据桥梁导出', 'iot:data-bridge:export', 3, 5, 4051, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-09 13:47:12', '', '2025-03-09 13:47:12', b'0'); COMMIT; -- ---------------------------- @@ -2166,7 +2375,7 @@ CREATE TABLE `system_oauth2_access_token` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 12055 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 13787 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; -- ---------------------------- -- Records of system_oauth2_access_token @@ -2288,7 +2497,7 @@ CREATE TABLE `system_oauth2_refresh_token` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1711 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 1735 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; -- ---------------------------- -- Records of system_oauth2_refresh_token @@ -2322,7 +2531,7 @@ CREATE TABLE `system_operate_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 9064 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; +) ENGINE = InnoDB AUTO_INCREMENT = 9065 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; -- ---------------------------- -- Records of system_operate_log @@ -2783,9 +2992,7 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2139, 2, 1011, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2140, 2, 1012, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2141, 2, 1013, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2142, 2, 1014, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2143, 2, 1015, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2144, 2, 1016, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2145, 2, 1017, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2146, 2, 1018, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2147, 2, 1019, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); @@ -3305,7 +3512,7 @@ CREATE TABLE `system_sms_code` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' -) ENGINE = InnoDB AUTO_INCREMENT = 645 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; +) ENGINE = InnoDB AUTO_INCREMENT = 649 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; -- ---------------------------- -- Records of system_sms_code @@ -3346,7 +3553,7 @@ CREATE TABLE `system_sms_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 1279 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; -- ---------------------------- -- Records of system_sms_log @@ -3376,7 +3583,7 @@ CREATE TABLE `system_sms_template` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; -- ---------------------------- -- Records of system_sms_template @@ -3395,6 +3602,7 @@ INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `cont INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 1, 0, 'user-update-password', '会员用户 - 修改密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-08-19 11:34:18', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 1, 0, 'user-reset-password', '会员用户 - 重置密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2023-08-19 18:58:01', '1', '2023-12-02 22:35:27', b'0'); INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 2, 0, 'bpm_task_timeout', '【工作流】任务审批超时', '您收到了一条超时的待办任务:{processInstanceName}-{taskName},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"detailUrl\"]', '', 'X', 4, 'DEBUG_DING_TALK', '1', '2024-08-16 21:59:15', '1', '2024-08-16 21:59:34', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (18, 1, 0, 'admin-reset-password', '后台用户 - 忘记密码', '您的验证码{code},该验证码 5 分钟内有效,请勿泄漏于他人!', '[\"code\"]', '', 'null', 4, 'DEBUG_DING_TALK', '1', '2025-03-16 14:19:34', '1', '2025-03-16 14:19:45', b'0'); COMMIT; -- ---------------------------- @@ -3588,7 +3796,7 @@ CREATE TABLE `system_user_role` ( `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; +) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; -- ---------------------------- -- Records of system_user_role @@ -3604,12 +3812,12 @@ INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_t INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (15, 111, 110, '110', '2022-02-23 13:14:38', '110', '2022-02-23 13:14:38', b'0', 121); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (16, 113, 111, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (18, 1, 2, '1', '2022-05-12 20:39:29', '1', '2022-05-12 20:39:29', b'0', 1); -INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (20, 104, 101, '1', '2022-05-28 15:43:57', '1', '2022-05-28 15:43:57', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (22, 115, 2, '1', '2022-07-21 22:08:30', '1', '2022-07-21 22:08:30', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (35, 112, 1, '1', '2024-03-15 20:00:24', '1', '2024-03-15 20:00:24', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (36, 118, 1, '1', '2024-03-17 09:12:08', '1', '2024-03-17 09:12:08', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (38, 114, 101, '1', '2024-03-24 22:23:03', '1', '2024-03-24 22:23:03', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (46, 117, 1, '1', '2024-10-02 10:16:11', '1', '2024-10-02 10:16:11', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (47, 104, 2, '1', '2025-01-04 10:40:33', '1', '2025-01-04 10:40:33', b'0', 1); COMMIT; -- ---------------------------- @@ -3644,10 +3852,10 @@ CREATE TABLE `system_users` ( -- Records of system_users -- ---------------------------- 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$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-12-28 20:29:58', 'admin', '2021-01-05 17:03:47', NULL, '2024-12-28 20:29:58', 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$Q3WCEQJbSZ0zT/7ryYTb3OgtrhwIZXu4ah5RQ5/YQDQ7DpW7N7oNa', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2025-03-16 14:20:16', 'admin', '2021-01-05 17:03:47', NULL, '2025-03-16 14:20:16', 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$IgUse/ibRzAZ3rngCThmtemJeoh15Ux1TQ2hIMe4iwt/K3LcFHEda', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-11-02 14:00:46', '', '2021-01-07 09:07:17', NULL, '2024-11-02 14:00:46', 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$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', 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$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-09-17 15:05:43', '', '2021-01-21 02:13:53', NULL, '2024-09-17 15:05:43', 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, '', 0, '0:0:0:0:0:0:0:1', '2025-01-04 10:40:49', '', '2021-01-21 02:13:53', NULL, '2025-01-04 10:40: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 (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', 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, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', 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, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120); diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 16b713a1b..d414e9415 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.4.0-SNAPSHOT + 2.4.1-SNAPSHOT 1.6.0 3.4.1 @@ -32,8 +32,9 @@ 8.1.3.140 8.6.0 5.1.0 + 3.3.3 - 2.3.1 + 2.3.2 2.2.7 @@ -53,25 +54,27 @@ 1.6.3 5.8.35 6.0.0-M19 - 4.0.3 + 4.0.3 2.4.1 1.2.83 33.4.0-jre 2.14.5 3.11.1 0.1.55 - 2.9.2 + 3.1.0 2.7.0 3.0.6 4.1.116.Final 1.2.5 + 0.9.0 + 4.5.13 2.17.0 1.27.1 1.12.777 2.0.5 1.8.1 - 4.6.0 + 4.7.2.B @@ -263,6 +266,12 @@ ${kingbase.jdbc.version} + + com.taosdata.jdbc + taos-jdbcdriver + ${taos.version} + + cn.iocoder.boot @@ -276,7 +285,6 @@ yudao-spring-boot-starter-mq ${revision} - org.apache.rocketmq rocketmq-spring-boot-starter @@ -471,7 +479,7 @@ com.alibaba easyexcel - ${easyexcel.verion} + ${easyexcel.version} commons-io @@ -591,6 +599,36 @@ + + + org.pf4j + pf4j-spring + ${pf4j-spring.version} + + + org.slf4j + slf4j-log4j12 + + + + + + + io.vertx + vertx-core + ${vertx.version} + + + io.vertx + vertx-web + ${vertx.version} + + + io.vertx + vertx-mqtt + ${vertx.version} + + org.eclipse.paho diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java new file mode 100644 index 000000000..b1c53dbfe --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/RpcConstants.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.framework.common.enums; + +/** + * RPC 相关的枚举 + * + * 虽然放在 yudao-spring-boot-starter-rpc 会相对合适,但是每个 API 模块需要使用到,所以暂时只好放在此处 + * + * @author 芋道源码 + */ +public class RpcConstants { + + /** + * RPC API 的前缀 + */ + public static final String RPC_API_PREFIX = "/rpc-api"; + +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 9e75113d0..e1e0ac08e 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -7,13 +7,15 @@ import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import jakarta.servlet.http.HttpServletRequest; import java.net.URI; +import java.net.URLEncoder; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.Map; /** @@ -23,6 +25,16 @@ import java.util.Map; */ public class HttpUtils { + /** + * 编码 URL 参数 + * + * @param value 参数 + * @return 编码后的参数 + */ + public static String encodeUtf8(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + @SuppressWarnings("unchecked") public static String replaceUrlQuery(String url, String key, String value) { UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java index c928e2fcf..c2787f46f 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.framework.common.util.number; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.math.BigDecimal; +import java.util.List; /** * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 @@ -20,6 +22,18 @@ public class NumberUtils { return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; } + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + /** * 通过经纬度获取地球上两点之间的距离 * diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java index 12731edad..905f5d541 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/servlet/ServletUtils.java @@ -98,4 +98,8 @@ public class ServletUtils { return JakartaServletUtil.getParamMap(request); } + public static Map getHeaderMap(HttpServletRequest request) { + return JakartaServletUtil.getHeaderMap(request); + } + } diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java index 069e89db3..abb1cece8 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java @@ -97,12 +97,26 @@ public class SpringExpressionUtils { * @return 执行界面 */ public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { if (StrUtil.isBlank(expressionString)) { return null; } Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } return expression.getValue(context); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java index 7ec9c69e3..b05b3c06b 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/util/TenantUtils.java @@ -45,6 +45,7 @@ public class TenantUtils { * * @param tenantId 租户编号 * @param callable 逻辑 + * @return 结果 */ public static V execute(Long tenantId, Callable callable) { Long oldTenantId = TenantContextHolder.getTenantId(); @@ -78,6 +79,25 @@ public class TenantUtils { } } + /** + * 忽略租户,执行对应的逻辑 + * + * @param callable 逻辑 + * @return 结果 + */ + public static V executeIgnore(Callable callable) { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + /** * 将多租户编号,添加到 header 中 * diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index f5c4b8313..eb037d9e1 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.excel.core.util; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.converters.longconverter.LongStringConverter; @@ -8,8 +9,6 @@ import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; /** @@ -40,7 +39,7 @@ public class ExcelUtils { .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 - response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml index 1f3c9144b..5a619c511 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -63,6 +63,11 @@ opengauss-jdbc true + + com.taosdata.jdbc + taos-jdbcdriver + true + com.alibaba diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index 01f214230..167a0fc4e 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -92,10 +92,36 @@ public interface BaseMapperX extends MPJBaseMapper { default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2, SFunction field3, Object value3) { - return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2) - .eq(field3, value3)); + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); } + /** + * 获取满足条件的第 1 条记录 + * + * 目的:解决并发场景下,插入多条记录后,使用 selectOne 会报错的问题 + * + * @param field 字段名 + * @param value 字段值 + * @return 实体 + */ + default T selectFirstOne(SFunction field, Object value) { + // 如果明确使用 MySQL 等场景,可以考虑使用 LIMIT 1 进行优化 + List list = selectList(new LambdaQueryWrapper().eq(field, value)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + return CollUtil.getFirst(list); + } + + default T selectFirstOne(SFunction field1, Object value1, SFunction field2, Object value2, + SFunction field3, Object value3) { + List list = selectList(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2).eq(field3, value3)); + return CollUtil.getFirst(list); + } + + default Long selectCount() { return selectCount(new QueryWrapper<>()); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java index 7ef0f4ece..9327ebbfe 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/EncryptTypeHandler.java @@ -13,7 +13,7 @@ import java.sql.ResultSet; import java.sql.SQLException; /** - * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 + * 字段字段的 TypeHandler 实现类,基于 {@link AES} 实现 * 可通过 jasypt.encryptor.password 配置项,设置密钥 * * @author 芋道源码 diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java new file mode 100644 index 000000000..58d82ecf3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/type/LongSetTypeHandler.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +/** + * Set 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 芋道源码 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class LongSetTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, Set strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public Set getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public Set getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public Set getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private Set getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToLongSet(value, COMMA); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java index 68ae1b30c..8c2d93b22 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -7,6 +7,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; +import jakarta.servlet.DispatcherType; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.ApplicationContext; @@ -142,7 +143,9 @@ public class YudaoWebSecurityConfigurerAdapter { // ②:每个项目的自定义规则 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) // ③:兜底规则,必须认证 - .authorizeHttpRequests(c -> c.anyRequest().authenticated()); + .authorizeHttpRequests(c -> c + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // WebFlux 异步请求,无需认证,目的:SSE 场景 + .anyRequest().authenticated()); // 添加 Token Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java index c8b0dbd66..d07c4aaed 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -62,9 +62,9 @@ public class BannerApplicationRunner implements ApplicationRunner { if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) { System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); } - // IOT 物联网 + // IoT 物联网 if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) { - System.out.println("[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + System.out.println("[IoT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); } }); } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java index b70251559..7b064e8d6 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/ApiRequestFilter.java @@ -20,8 +20,8 @@ public abstract class ApiRequestFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) { // 只过滤 API 请求的地址 - return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), - webProperties.getAppApi().getPrefix()); + String apiUri = request.getRequestURI().substring(request.getContextPath().length()); + return !StrUtil.startWithAny(apiUri, webProperties.getAdminApi().getPrefix(), webProperties.getAppApi().getPrefix()); } } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 41c5cead6..073824815 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.web.core.handler; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; @@ -27,6 +28,7 @@ import org.springframework.security.access.AccessDeniedException; import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -37,6 +39,7 @@ import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.Set; @@ -135,9 +138,23 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + // 获取 errorMessage + String errorMessage = null; FieldError fieldError = ex.getBindingResult().getFieldError(); - assert fieldError != null; // 断言,避免告警 - return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + if (fieldError == null) { + // 组合校验,参考自 https://t.zsxq.com/3HVTx + List allErrors = ex.getBindingResult().getAllErrors(); + if (CollUtil.isNotEmpty(allErrors)) { + errorMessage = allErrors.get(0).getDefaultMessage(); + } + } else { + errorMessage = fieldError.getDefaultMessage(); + } + // 转换 CommonResult + if (StrUtil.isEmpty(errorMessage)) { + return CommonResult.error(BAD_REQUEST); + } + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); } /** @@ -378,11 +395,11 @@ public class GlobalExceptionHandler { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } - // 9. IOT 物联网 + // 9. IoT 物联网 if (message.contains("iot_")) { - log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + "[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); } return null; } diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java index 6cb98c562..147927495 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java @@ -35,11 +35,7 @@ public enum AiChatRoleEnum { ### 微信 除此之外不要任何解释性语句。 """), - - AI_KNOWLEDGE_ROLE("知识库助手", """ - 给你提供一些数据参考:{info},请回答我的问题。 - 请你跟进数据参考与工具返回结果回复用户的请求。 - """); + ; /** * 角色名 diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index e1dd1a956..8b235f9ac 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -12,49 +12,53 @@ public interface ErrorCodeConstants { // ========== API 密钥 1-040-000-000 ========== ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); - ErrorCode API_KEY_MIDJOURNEY_NOT_FOUND = new ErrorCode(1_040_000_900, "Midjourney 模型不存在"); - ErrorCode API_KEY_SUNO_NOT_FOUND = new ErrorCode(1_040_000_901, "Suno 模型不存在"); - ErrorCode API_KEY_IMAGE_NODE_FOUND = new ErrorCode(1_040_000_902, "平台({}) 图片模型未配置"); - // ========== API 聊天模型 1-040-001-000 ========== - ErrorCode CHAT_MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); - ErrorCode CHAT_MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); - ErrorCode CHAT_MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天模型"); + // ========== API 模型 1-040-001-000 ========== + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); + ErrorCode MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); + ErrorCode MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认模型"); + ErrorCode MODEL_USE_TYPE_ERROR = new ErrorCode(1_040_001_003, "操作失败,该模型的模型类型不正确"); - // ========== API 聊天模型 1-040-002-000 ========== + // ========== API 聊天角色 1-040-002-000 ========== ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在"); ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!"); // ========== API 聊天会话 1-040-003-000 ========== - ErrorCode CHAT_CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_003_000, "对话不存在!"); ErrorCode CHAT_CONVERSATION_MODEL_ERROR = new ErrorCode(1_040_003_001, "操作失败,该聊天模型的配置不完整"); // ========== API 聊天消息 1-040-004-000 ========== - ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!"); // ========== API 绘画 1-040-005-000 ========== - - ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); - ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); - ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); - ErrorCode IMAGE_FAIL = new ErrorCode(1_022_005_002, "图片绘画失败! {}"); + ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_040_005_000, "图片不存在!"); + ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_040_005_001, "Midjourney 提交失败!原因:{}"); + ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_040_005_002, "Midjourney 按钮 customId 不存在! {}"); // ========== API 音乐 1-040-006-000 ========== - ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); + ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_040_006_000, "音乐不存在!"); - // ========== API 写作 1-022-007-000 ========== - ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); - ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!"); + // ========== API 写作 1-040-007-000 ========== + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_040_007_000, "作文不存在!"); + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_040_07_001, "写作生成异常!"); // ========== API 思维导图 1-040-008-000 ========== ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!"); - // ========== API 知识库 1-022-008-000 ========== - ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); - ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!"); - ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_002, "段落不存在!"); + // ========== API 知识库 1-040-009-000 ========== + ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_040_009_000, "知识库不存在!"); + + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_040_009_101, "文档不存在!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_040_009_102, "文档内容为空!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL = new ErrorCode(1_040_009_102, "文件下载失败!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_040_009_102, "文档加载失败!"); + + ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_040_009_202, "段落不存在!"); + ErrorCode KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG = new ErrorCode(1_040_009_203, "内容 Token 数为 {},超过最大限制 {}"); + + // ========== AI 工具 1-040-010-000 ========== + ErrorCode TOOL_NOT_EXISTS = new ErrorCode(1_040_010_000, "工具不存在"); + ErrorCode TOOL_NAME_NOT_EXISTS = new ErrorCode(1_040_010_001, "工具({})找不到 Bean"); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http index e75e0d333..4c4c8c089 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/ai/chat/message/send Content-Type: application/json Authorization: {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "conversationId": "1781604279872581724", @@ -13,7 +13,7 @@ tenant-id: {{adminTenentId}} POST {{baseUrl}}/ai/chat/message/send-stream Content-Type: application/json Authorization: {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "conversationId": "1781604279872581724", diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java index 357dbec5e..bfd1e41ca 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -12,15 +12,18 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessage import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -33,7 +36,7 @@ import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 聊天消息") @@ -48,6 +51,10 @@ public class AiChatMessageController { private AiChatConversationService chatConversationService; @Resource private AiChatRoleService chatRoleService; + @Resource + private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢") @PostMapping("/send") @@ -57,7 +64,6 @@ public class AiChatMessageController { @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId()); } @@ -71,8 +77,38 @@ public class AiChatMessageController { if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { return success(Collections.emptyList()); } + // 1. 获取消息列表 List messageList = chatMessageService.getChatMessageListByConversationId(conversationId); - return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class)); + if (CollUtil.isEmpty(messageList)) { + return success(Collections.emptyList()); + } + + // 2. 拼接数据,主要是知识库段落信息 + Map segmentMap = knowledgeSegmentService.getKnowledgeSegmentMap(convertListByFlatMap(messageList, + message -> CollUtil.isEmpty(message.getSegmentIds()) ? null : message.getSegmentIds().stream())); + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertList(segmentMap.values(), AiKnowledgeSegmentDO::getDocumentId)); + List messageVOList = BeanUtils.toBean(messageList, AiChatMessageRespVO.class); + for (int i = 0; i < messageList.size(); i++) { + AiChatMessageDO message = messageList.get(i); + if (CollUtil.isEmpty(message.getSegmentIds())) { + continue; + } + // 设置知识库段落信息 + messageVOList.get(i).setSegments(convertList(message.getSegmentIds(), segmentId -> { + AiKnowledgeSegmentDO segment = segmentMap.get(segmentId); + if (segment == null) { + return null; + } + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + if (document == null) { + return null; + } + return new AiChatMessageRespVO.KnowledgeSegment().setId(segment.getId()).setContent(segment.getContent()) + .setDocumentId(segment.getDocumentId()).setDocumentName(document.getName()); + })); + } + return success(messageVOList); } @Operation(summary = "删除消息") @@ -105,7 +141,8 @@ public class AiChatMessageController { Map roleMap = chatRoleService.getChatRoleMap( convertSet(pageResult.getList(), AiChatMessageDO::getRoleId)); return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class, - respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleName(role.getName())))); + respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), + role -> respVO.setRoleName(role.getName())))); } @Operation(summary = "管理员删除消息") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java index 66eb24db5..7da37ebc9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; @@ -31,7 +31,7 @@ public class AiChatConversationRespVO implements VO { private Long roleId; @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = "name", ref = "modelName") + @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = "name", ref = "modelName") private Long modelId; @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERNIE-Bot-turbo-0922") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java index 9b358df6f..5d44e4f96 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天消息 Response VO") @Data @@ -39,6 +40,12 @@ public class AiChatMessageRespVO { @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean useContext; + @Schema(description = "知识库段落编号数组", example = "[1,2,3]") + private List segmentIds; + + @Schema(description = "知识库段落数组") + private List segments; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") private LocalDateTime createTime; @@ -47,4 +54,22 @@ public class AiChatMessageRespVO { @Schema(description = "角色名字", example = "小黄") private String roleName; + @Schema(description = "知识库段落", example = "Java 开发手册") + @Data + public static class KnowledgeSegment { + + @Schema(description = "段落编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + private String content; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long documentId; + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册") + private String documentName; + + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java index 58ba05659..245a19f7c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天消息发送 Response VO") @Data @@ -28,6 +29,12 @@ public class AiChatMessageSendRespVO { @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") private String content; + @Schema(description = "知识库段落编号数组", example = "[1,2,3]") + private List segmentIds; + + @Schema(description = "知识库段落数组") + private List segments; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java index a38935ef7..02225ea49 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java @@ -14,18 +14,15 @@ import java.util.Map; @Data public class AiImageDrawReqVO { - @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") - private String platform; // 参见 AiPlatformEnum 枚举 + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "模型编号不能为空") + private Long modelId; @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城") @NotEmpty(message = "提示词不能为空") @Size(max = 1200, message = "提示词最大 1200") private String prompt; - @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6") - @NotEmpty(message = "模型不能为空") - private String model; - /** * 1. dall-e-2 模型:256x256、512x512、1024x1024 * 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java index b90882639..efb590615 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java @@ -13,9 +13,9 @@ public class AiMidjourneyImagineReqVO { @NotEmpty(message = "提示词不能为空!") private String prompt; - @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "midjourney") - @NotEmpty(message = "模型不能为空") - private String model; // 参考 MidjourneyApi.ModelEnum + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模型编号不能为空") + private Long modelId; @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "图片宽度不能为空") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http new file mode 100644 index 000000000..a0f127865 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http @@ -0,0 +1,35 @@ +### 创建知识库 +POST {{baseUrl}}/ai/knowledge/create +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenantId}} + +{ + "name": "测试标题", + "description": "测试描述", + "embeddingModelId": 30, + "topK": 3, + "similarityThreshold": 0.5, + "status": 0 +} + +### 更新知识库 +PUT {{baseUrl}}/ai/knowledge/update +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenantId}} + +{ + "id": 1, + "name": "测试标题(更新)", + "description": "测试描述", + "embeddingModelId": 30, + "topK": 5, + "similarityThreshold": 0.6, + "status": 0 +} + +### 获取知识库分页 +GET {{baseUrl}}/ai/knowledge/page?pageNo=1&pageSize=10 +Authorization: {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 3ffea5e80..c6e31f0e8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -1,23 +1,27 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - AI 知识库") @RestController @@ -30,21 +34,42 @@ public class AiKnowledgeController { @GetMapping("/page") @Operation(summary = "获取知识库分页") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) { - PageResult pageResult = knowledgeService.getKnowledgePage(getLoginUserId(), pageReqVO); + PageResult pageResult = knowledgeService.getKnowledgePage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获得知识库") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult getKnowledge(@RequestParam("id") Long id) { + AiKnowledgeDO knowledge = knowledgeService.getKnowledge(id); + return success(BeanUtils.toBean(knowledge, AiKnowledgeRespVO.class)); + } + @PostMapping("/create") @Operation(summary = "创建知识库") - public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) { - return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId())); + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) { + return success(knowledgeService.createKnowledge(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新知识库") - public CommonResult updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) { - knowledgeService.updateKnowledge(updateReqVO, getLoginUserId()); + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + public CommonResult updateKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO updateReqVO) { + knowledgeService.updateKnowledge(updateReqVO); return success(true); } + + @GetMapping("/simple-list") + @Operation(summary = "获得知识库的精简列表") + public CommonResult> getKnowledgeSimpleList() { + List list = knowledgeService.getKnowledgeSimpleListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, knowledge -> new AiKnowledgeRespVO() + .setId(knowledge.getId()).setName(knowledge.getName()))); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http new file mode 100644 index 000000000..1c858ed3e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http @@ -0,0 +1,35 @@ +### 创建知识文档 +POST {{baseUrl}}/ai/knowledge/document/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "knowledgeId": 2, + "name": "测试文档", + "url": "https://static.iocoder.cn/README.md", + "segmentMaxTokens": 800 +} + +### 批量创建知识文档 +POST {{baseUrl}}/ai/knowledge/document/create-list +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "knowledgeId": 1, + "list": [ + { + "name": "测试文档1", + "url": "https://static.iocoder.cn/README.md", + "segmentMaxTokens": 800 + }, + { + "name": "测试文档2", + "url": "https://static.iocoder.cn/README_yudao.md", + "segmentMaxTokens": 400 + } + ] +} + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index 75c4d805b..68fe49a8a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.*; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; @@ -13,9 +11,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - AI 知识库文档") @@ -27,25 +28,63 @@ public class AiKnowledgeDocumentController { @Resource private AiKnowledgeDocumentService documentService; - @PostMapping("/create") - @Operation(summary = "新建文档") - public CommonResult createKnowledgeDocument(@Valid AiKnowledgeDocumentCreateReqVO reqVO) { - Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); - return success(knowledgeDocumentId); - } - @GetMapping("/page") @Operation(summary = "获取文档分页") - public CommonResult> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) { + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult> getKnowledgeDocumentPage( + @Valid AiKnowledgeDocumentPageReqVO pageReqVO) { PageResult pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获取文档详情") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult getKnowledgeDocument(@RequestParam("id") Long id) { + AiKnowledgeDocumentDO document = documentService.getKnowledgeDocument(id); + return success(BeanUtils.toBean(document, AiKnowledgeDocumentRespVO.class)); + } + + @PostMapping("/create") + @Operation(summary = "新建文档(单个)") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { + Long id = documentService.createKnowledgeDocument(reqVO); + return success(id); + } + + @PostMapping("/create-list") + @Operation(summary = "新建文档(多个)") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + public CommonResult> createKnowledgeDocumentList( + @RequestBody @Valid AiKnowledgeDocumentCreateListReqVO reqVO) { + List ids = documentService.createKnowledgeDocumentList(reqVO); + return success(ids); + } + @PutMapping("/update") @Operation(summary = "更新文档") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") public CommonResult updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { documentService.updateKnowledgeDocument(reqVO); return success(true); } + @PutMapping("/update-status") + @Operation(summary = "更新文档状态") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + public CommonResult updateKnowledgeDocumentStatus( + @Valid @RequestBody AiKnowledgeDocumentUpdateStatusReqVO reqVO) { + documentService.updateKnowledgeDocumentStatus(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文档") + @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')") + public CommonResult deleteKnowledgeDocument(@RequestParam("id") Long id) { + documentService.deleteKnowledgeDocument(id); + return success(true); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http new file mode 100644 index 000000000..09018da3d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http @@ -0,0 +1,17 @@ +### 切片内容 +GET {{baseUrl}}/ai/knowledge/segment/split?url=https://static.iocoder.cn/README_yudao.md&segmentMaxTokens=800 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 搜索段落内容 +GET {{baseUrl}}/ai/knowledge/segment/search?knowledgeId=2&content=如何使用这个产品&topK=5&similarityThreshold=0.1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +### 获取文档处理列表 +GET {{baseUrl}}/ai/knowledge/segment/get-process-list?documentIds=1,2,3 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index d4ca7ca49..34f324491 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -1,22 +1,34 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.hibernate.validator.constraints.URL; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; @Tag(name = "管理后台 - AI 知识库段落") @RestController @@ -26,26 +38,93 @@ public class AiKnowledgeSegmentController { @Resource private AiKnowledgeSegmentService segmentService; + @Resource + private AiKnowledgeDocumentService documentService; + + @GetMapping("/get") + @Operation(summary = "获取段落详情") + @Parameter(name = "id", description = "段落编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult getKnowledgeSegment(@RequestParam("id") Long id) { + AiKnowledgeSegmentDO segment = segmentService.getKnowledgeSegment(id); + return success(BeanUtils.toBean(segment, AiKnowledgeSegmentRespVO.class)); + } @GetMapping("/page") @Operation(summary = "获取段落分页") - public CommonResult> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) { + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult> getKnowledgeSegmentPage( + @Valid AiKnowledgeSegmentPageReqVO pageReqVO) { PageResult pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); } + @PostMapping("/create") + @Operation(summary = "创建段落") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + public CommonResult createKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO createReqVO) { + return success(segmentService.createKnowledgeSegment(createReqVO)); + } + @PutMapping("/update") @Operation(summary = "更新段落内容") - public CommonResult updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) { + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + public CommonResult updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO reqVO) { segmentService.updateKnowledgeSegment(reqVO); return success(true); } @PutMapping("/update-status") @Operation(summary = "启禁用段落内容") - public CommonResult updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + public CommonResult updateKnowledgeSegmentStatus( + @Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { segmentService.updateKnowledgeSegmentStatus(reqVO); return success(true); } + @GetMapping("/split") + @Operation(summary = "切片内容") + @Parameters({ + @Parameter(name = "url", description = "文档 URL", required = true), + @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token 数", required = true) + }) + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult> splitContent( + @RequestParam("url") @URL String url, + @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) { + List segments = segmentService.splitContent(url, segmentMaxTokens); + return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class)); + } + + @GetMapping("/get-process-list") + @Operation(summary = "获取文档处理列表") + @Parameter(name = "documentIds", description = "文档编号列表", required = true, example = "1,2,3") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult> getKnowledgeSegmentProcessList( + @RequestParam("documentIds") List documentIds) { + List list = segmentService.getKnowledgeSegmentProcessList(documentIds); + return success(list); + } + + @GetMapping("/search") + @Operation(summary = "搜索段落内容") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult> searchKnowledgeSegment( + @Valid AiKnowledgeSegmentSearchReqVO reqVO) { + // 1. 搜索段落 + List segments = segmentService + .searchKnowledgeSegment(BeanUtils.toBean(reqVO, AiKnowledgeSegmentSearchReqBO.class)); + if (CollUtil.isEmpty(segments)) { + return success(Collections.emptyList()); + } + + // 2. 拼接 VO + Map documentMap = documentService.getKnowledgeDocumentMap(convertSet( + segments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); + return success(BeanUtils.toBean(segments, AiKnowledgeSegmentSearchRespVO.class, + segment -> MapUtils.findAndThen(documentMap, segment.getDocumentId(), + document -> segment.setDocumentName(document.getName())))); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java new file mode 100644 index 000000000..6545c0bc1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import java.util.List; + +@Schema(description = "管理后台 - AI 知识库文档批量创建 Request VO") +@Data +public class AiKnowledgeDocumentCreateListReqVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") + @NotNull(message = "知识库编号不能为空") + private Long knowledgeId; + + @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") + @NotNull(message = "分段的最大 Token 数不能为空") + private Integer segmentMaxTokens; + + @Schema(description = "文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "文档列表不能为空") + private List list; + + @Schema(description = "文档") + @Data + public static class Document { + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三方登陆") + @NotBlank(message = "文档名称不能为空") + private String name; + + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @URL(message = "文档 URL 格式不正确") + private String url; + + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java index 76c001bd3..15bb603c2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java @@ -8,6 +8,9 @@ import lombok.Data; @Data public class AiKnowledgeDocumentPageReqVO extends PageParam { + @Schema(description = "知识库编号", example = "1") + private Long knowledgeId; + @Schema(description = "文档名称", example = "Java 开发手册") private String name; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java index 96ca61b3d..7aef94a3f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java @@ -1,38 +1,45 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; -import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库-文档 Response VO") -@Data -public class AiKnowledgeDocumentRespVO extends PageParam { +import java.time.LocalDateTime; - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") +@Schema(description = "管理后台 - AI 知识库文档 Response VO") +@Data +public class AiKnowledgeDocumentRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") private Long id; @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") private Long knowledgeId; - @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String name; - @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....") - private String content; - - @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") private String url; - @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @Schema(description = "文档内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....") + private String content; + + @Schema(description = "文档内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Integer contentLength; + + @Schema(description = "文档 Token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") - private Integer wordCount; + @Schema(description = "分片最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "512") + private Integer segmentMaxTokens; - @Schema(description = "切片状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Integer sliceStatus; + @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer retrievalCount; - @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer status; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java index 2cc6a32f3..e6dbe5cbd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -1,26 +1,21 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; - -@Schema(description = "管理后台 - AI 更新 知识库-文档 Request VO") +@Schema(description = "管理后台 - AI 知识库文档更新 Request VO") @Data public class AiKnowledgeDocumentUpdateReqVO { - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") @NotNull(message = "编号不能为空") private Long id; - @Schema(description = "是否启用", example = "1") - @InEnum(CommonStatusEnum.class) - private Integer status; - @Schema(description = "名称", example = "Java 开发手册") private String name; + @Schema(description = "分片最大 Token 数", example = "1000") + private Integer segmentMaxTokens; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java new file mode 100644 index 000000000..93d393ab4 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库文档更新状态 Request VO") +@Data +public class AiKnowledgeDocumentUpdateStatusReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java index df6b6821d..1d2e49307 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java @@ -23,24 +23,8 @@ public class AiKnowledgeDocumentCreateReqVO { @URL(message = "文档 URL 格式不正确") private String url; - @Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") - @NotNull(message = "每个段落的目标 token 数不能为空") - private Integer defaultSegmentTokens; - - @Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350") - @NotNull(message = "每个段落的最小字符数不能为空") - private Integer minSegmentWordCount; - - @Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") - @NotNull(message = "丢弃阈值不能为空") - private Integer minChunkLengthToEmbed; - - @Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000") - @NotNull(message = "最大段落数不能为空") - private Integer maxNumSegments; - - @Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - @NotNull(message = "分块是否保留分隔符不能为空") - private Boolean keepSeparator; + @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") + @NotNull(message = "分段的最大 Token 数不能为空") + private Integer segmentMaxTokens; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java index 941732f1a..dc7943cf2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java @@ -1,14 +1,29 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +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 = "管理后台 - AI 知识库的分页 Request VO") @Data public class AiKnowledgePageReqVO extends PageParam { - @Schema(description = "知识库名称", example = "Java 开发手册") + @Schema(description = "知识库名称", example = "芋艿") private String name; + @Schema(description = "是否启用", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java index 3ff8a1c75..5e83b85a7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.time.LocalDateTime; @Schema(description = "管理后台 - AI 知识库 Response VO") @Data @@ -17,10 +18,22 @@ public class AiKnowledgeRespVO { @Schema(description = "知识库描述", example = "帮助你快速构建系统") private String description; - @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") - private Long modelId; + @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") + private Long embeddingModelId; - @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat") - private String model; + @Schema(description = "向量模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat") + private String embeddingModel; + + @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer topK; + + @Schema(description = "相似度阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.7") + private Double similarityThreshold; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java similarity index 60% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java index 00843665c..774b7234a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java @@ -1,15 +1,18 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; -import java.util.List; - -@Schema(description = "管理后台 - AI 知识库创建 Request VO") +@Schema(description = "管理后台 - AI 知识库新增/修改 Request VO") @Data -public class AiKnowledgeCreateReqVO { +public class AiKnowledgeSaveReqVO { + + @Schema(description = "对话编号", example = "1204") + private Long id; @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") @NotBlank(message = "知识库名称不能为空") @@ -18,19 +21,21 @@ public class AiKnowledgeCreateReqVO { @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档") private String description; - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]") - private List visibilityPermissions; - - @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "嵌入模型不能为空") - private Long modelId; - - @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5") - @NotNull(message = "相似性阈值不能为空") - private Double similarityThreshold; + @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "向量模型不能为空") + private Long embeddingModelId; @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") @NotNull(message = "topK 不能为空") private Integer topK; + @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5") + @NotNull(message = "相似性阈值不能为空") + private Double similarityThreshold; + + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "是否启用不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java deleted file mode 100644 index ba98bf0c7..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import java.util.List; - -@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO") -@Data -public class AiKnowledgeUpdateReqVO { - - @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") - @NotNull(message = "知识库编号不能为空") - private Long id; - - @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "") - @NotBlank(message = "知识库名称不能为空") - private String name; - - @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") - private String description; - - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") - private List visibilityPermissions; - - @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "嵌入模型不能为空") - private Long modelId; - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java index 8be3db501..f53d5be07 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,13 +10,14 @@ import lombok.Data; @Data public class AiKnowledgeSegmentPageReqVO extends PageParam { - @Schema(description = "分段状态", example = "1") - private Integer status; - @Schema(description = "文档编号", example = "1") private Integer documentId; @Schema(description = "分段内容关键字", example = "Java 开发") - private String keyword; + private String content; + + @Schema(description = "分段状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java new file mode 100644 index 000000000..a6b95265b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库段落向量进度 Response VO") +@Data +public class AiKnowledgeSegmentProcessRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long documentId; + + @Schema(description = "总段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long count; + + @Schema(description = "已向量化段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long embeddingCount; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java index 5e3f2d8cb..24c452621 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库-文档 Response VO") +@Schema(description = "管理后台 - AI 知识库文档分片 Response VO") @Data public class AiKnowledgeSegmentRespVO { @@ -22,13 +22,19 @@ public class AiKnowledgeSegmentRespVO { @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String content; + @Schema(description = "切片内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer contentLength; + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") - private Integer wordCount; + @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer retrievalCount; @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Long createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java new file mode 100644 index 000000000..0c5dad11d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - AI 新增/修改知识库段落 request VO") +@Data +public class AiKnowledgeSegmentSaveReqVO { + + @Schema(description = "编号", example = "24790") + private Long id; + + @Schema(description = "知识库文档编号", example = "1024") + private Long documentId; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + @NotEmpty(message = "切片内容不能为空") + private String content; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java index 75349df62..3b3cd984b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java @@ -3,15 +3,25 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; -@Schema(description = "管理后台 - AI 知识库段落召回 Request VO") +@Schema(description = "管理后台 - AI 知识库段落搜索 Request VO") @Data public class AiKnowledgeSegmentSearchReqVO { - @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") private Long knowledgeId; - @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线") + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "如何使用这个产品") + @NotEmpty(message = "内容不能为空") private String content; + @Schema(description = "最大返回数量", example = "5") + private Integer topK; + + @Schema(description = "相似度阈值", example = "0.7") + private Double similarityThreshold; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java new file mode 100644 index 000000000..50bbc5c86 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库段落搜索 Response VO") +@Data +public class AiKnowledgeSegmentSearchRespVO extends AiKnowledgeSegmentRespVO { + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册") + private String documentName; + + @Schema(description = "相似度分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.95") + private Double score; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java deleted file mode 100644 index 23b1461e2..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - - -@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO") -@Data -public class AiKnowledgeSegmentUpdateReqVO { - - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") - private Long id; - - @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") - private String content; - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java index f1c59b964..db015e514 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,7 +31,6 @@ public class AiMindMapController { @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Operation(summary = "导图生成(流式)", description = "流式返回,响应较快") - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) { return mindMapService.generateMindMap(generateReqVO, getLoginUserId()); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java index c123ab70e..f7769b4e6 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java @@ -13,8 +13,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @Schema(description = "管理后台 - AI 思维导图分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class AiMindMapPageReqVO extends PageParam { @Schema(description = "用户编号", example = "4325") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java index 2bc190051..c109b033c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java @@ -6,9 +6,8 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyRespVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -76,9 +75,9 @@ public class AiApiKeyController { @GetMapping("/simple-list") @Operation(summary = "获得 API 密钥分页列表") - public CommonResult> getApiKeySimpleList() { + public CommonResult> getApiKeySimpleList() { List list = apiKeyService.getApiKeyList(); - return success(convertList(list, key -> new AiChatModelRespVO().setId(key.getId()).setName(key.getName()))); + return success(convertList(list, key -> new AiModelRespVO().setId(key.getId()).setName(key.getName()))); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java deleted file mode 100644 index 08a53b286..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -@Tag(name = "管理后台 - AI 聊天模型") -@RestController -@RequestMapping("/ai/chat-model") -@Validated -public class AiChatModelController { - - @Resource - private AiChatModelService chatModelService; - - @PostMapping("/create") - @Operation(summary = "创建聊天模型") - @PreAuthorize("@ss.hasPermission('ai:chat-model:create')") - public CommonResult createChatModel(@Valid @RequestBody AiChatModelSaveReqVO createReqVO) { - return success(chatModelService.createChatModel(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新聊天模型") - @PreAuthorize("@ss.hasPermission('ai:chat-model:update')") - public CommonResult updateChatModel(@Valid @RequestBody AiChatModelSaveReqVO updateReqVO) { - chatModelService.updateChatModel(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除聊天模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('ai:chat-model:delete')") - public CommonResult deleteChatModel(@RequestParam("id") Long id) { - chatModelService.deleteChatModel(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得聊天模型") - @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") - public CommonResult getChatModel(@RequestParam("id") Long id) { - AiChatModelDO chatModel = chatModelService.getChatModel(id); - return success(BeanUtils.toBean(chatModel, AiChatModelRespVO.class)); - } - - @GetMapping("/page") - @Operation(summary = "获得聊天模型分页") - @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") - public CommonResult> getChatModelPage(@Valid AiChatModelPageReqVO pageReqVO) { - PageResult pageResult = chatModelService.getChatModelPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, AiChatModelRespVO.class)); - } - - @GetMapping("/simple-list") - @Operation(summary = "获得聊天模型列表") - @Parameter(name = "status", description = "状态", required = true, example = "1") - public CommonResult> getChatModelSimpleList(@RequestParam("status") Integer status) { - List list = chatModelService.getChatModelListByStatus(status); - return success(convertList(list, model -> new AiChatModelRespVO().setId(model.getId()) - .setName(model.getName()).setModel(model.getModel()))); - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java index 02f698b94..5714c5fed 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java @@ -75,7 +75,7 @@ public class AiChatRoleController { @GetMapping("/category-list") @Operation(summary = "获得聊天角色的分类列表") public CommonResult> getChatRoleCategoryList() { - return success(chatRoleService.getChatRoleCategoryList()); + return success(chatRoleService.getChatRoleCategoryList()); } // ========== 角色管理 ========== diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java new file mode 100644 index 000000000..86dd4d0a6 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - AI 模型") +@RestController +@RequestMapping("/ai/model") +@Validated +public class AiModelController { + + @Resource + private AiModelService modelService; + + @PostMapping("/create") + @Operation(summary = "创建模型") + @PreAuthorize("@ss.hasPermission('ai:model:create')") + public CommonResult createModel(@Valid @RequestBody AiModelSaveReqVO createReqVO) { + return success(modelService.createModel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新模型") + @PreAuthorize("@ss.hasPermission('ai:model:update')") + public CommonResult updateModel(@Valid @RequestBody AiModelSaveReqVO updateReqVO) { + modelService.updateModel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:model:delete')") + public CommonResult deleteModel(@RequestParam("id") Long id) { + modelService.deleteModel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:model:query')") + public CommonResult getModel(@RequestParam("id") Long id) { + AiModelDO model = modelService.getModel(id); + return success(BeanUtils.toBean(model, AiModelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得模型分页") + @PreAuthorize("@ss.hasPermission('ai:model:query')") + public CommonResult> getModelPage(@Valid AiModelPageReqVO pageReqVO) { + PageResult pageResult = modelService.getModelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiModelRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得模型列表") + @Parameter(name = "type", description = "类型", required = true, example = "1") + @Parameter(name = "platform", description = "平台", example = "midjourney") + public CommonResult> getModelSimpleList( + @RequestParam("type") Integer type, + @RequestParam(value = "platform", required = false) String platform) { + List list = modelService.getModelListByStatusAndType( + CommonStatusEnum.ENABLE.getStatus(), type, platform); + return success(convertList(list, model -> new AiModelRespVO().setId(model.getId()) + .setName(model.getName()).setModel(model.getModel()).setPlatform(model.getPlatform()))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java new file mode 100644 index 000000000..e98f87e0b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import cn.iocoder.yudao.module.ai.service.model.AiToolService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - AI 工具") +@RestController +@RequestMapping("/ai/tool") +@Validated +public class AiToolController { + + @Resource + private AiToolService toolService; + + @PostMapping("/create") + @Operation(summary = "创建工具") + @PreAuthorize("@ss.hasPermission('ai:tool:create')") + public CommonResult createTool(@Valid @RequestBody AiToolSaveReqVO createReqVO) { + return success(toolService.createTool(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新工具") + @PreAuthorize("@ss.hasPermission('ai:tool:update')") + public CommonResult updateTool(@Valid @RequestBody AiToolSaveReqVO updateReqVO) { + toolService.updateTool(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除工具") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:tool:delete')") + public CommonResult deleteTool(@RequestParam("id") Long id) { + toolService.deleteTool(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得工具") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:tool:query')") + public CommonResult getTool(@RequestParam("id") Long id) { + AiToolDO tool = toolService.getTool(id); + return success(BeanUtils.toBean(tool, AiToolRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得工具分页") + @PreAuthorize("@ss.hasPermission('ai:tool:query')") + public CommonResult> getToolPage(@Valid AiToolPageReqVO pageReqVO) { + PageResult pageResult = toolService.getToolPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiToolRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得工具列表") + public CommonResult> getToolSimpleList() { + List list = toolService.getToolListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, tool -> new AiToolRespVO() + .setId(tool.getId()).setName(tool.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java index eb34da274..51e44ed76 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天角色 Response VO") @Data @@ -20,7 +21,7 @@ public class AiChatRoleRespVO implements VO { private Long userId; @Schema(description = "模型编号", example = "17640") - @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"}) + @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = { "name", "model" }, refs = { "modelName", "model" }) private Long modelId; @Schema(description = "模型名字", example = "张三") private String modelName; @@ -45,6 +46,12 @@ public class AiChatRoleRespVO implements VO { @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED) private String systemMessage; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; + + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java index 4673901d3..009e8d8af 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.hibernate.validator.constraints.URL; +import java.util.List; + @Schema(description = "管理后台 - AI 聊天角色新增/修改【我的】 Request VO") @Data public class AiChatRoleSaveMyReqVO { @@ -29,4 +31,10 @@ public class AiChatRoleSaveMyReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; + + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java index bdda027ef..3c72cf983 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java @@ -7,6 +7,8 @@ import lombok.*; import jakarta.validation.constraints.*; import org.hibernate.validator.constraints.URL; +import java.util.List; + @Schema(description = "管理后台 - AI 聊天角色新增/修改 Request VO") @Data public class AiChatRoleSaveReqVO { @@ -42,6 +44,12 @@ public class AiChatRoleSaveReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; + + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "是否公开不能为空") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java similarity index 67% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java index ce2f83b4b..af8d1121a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java @@ -1,12 +1,12 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; import cn.iocoder.yudao.framework.common.pojo.PageParam; -@Schema(description = "管理后台 - API 聊天模型分页 Request VO") +@Schema(description = "管理后台 - API 模型分页 Request VO") @Data -public class AiChatModelPageReqVO extends PageParam { +public class AiModelPageReqVO extends PageParam { @Schema(description = "模型名字", example = "张三") private String name; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java similarity index 83% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java index 681dabe68..b50b70a08 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java @@ -1,13 +1,13 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -@Schema(description = "管理后台 - AI 聊天模型 Response VO") +@Schema(description = "管理后台 - AI 模型 Response VO") @Data -public class AiChatModelRespVO { +public class AiModelRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2630") private Long id; @@ -24,6 +24,9 @@ public class AiChatModelRespVO { @Schema(description = "模型平台", example = "OpenAI") private String platform; + @Schema(description = "模型类型", example = "1") + private Integer type; + @Schema(description = "排序", example = "1") private Integer sort; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java similarity index 71% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java index 4fad5a1fc..281bcd673 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java @@ -1,14 +1,17 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; -@Schema(description = "管理后台 - API 聊天模型新增/修改 Request VO") +@Schema(description = "管理后台 - API 模型新增/修改 Request VO") @Data -public class AiChatModelSaveReqVO { +public class AiModelSaveReqVO { @Schema(description = "编号", example = "2630") private Long id; @@ -27,8 +30,14 @@ public class AiChatModelSaveReqVO { @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") @NotEmpty(message = "模型平台不能为空") + @InEnum(AiPlatformEnum.class) private String platform; + @Schema(description = "模型类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模型类型不能为空") + @InEnum(AiModelTypeEnum.class) + private Integer type; + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "排序不能为空") private Integer sort; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java new file mode 100644 index 000000000..dc8b04c50 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +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 = "管理后台 - AI 工具分页 Request VO") +@Data +public class AiToolPageReqVO extends PageParam { + + @Schema(description = "工具名称", example = "王五") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java new file mode 100644 index 000000000..6d5a02e68 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 工具 Response VO") +@Data +public class AiToolRespVO { + + @Schema(description = "工具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19661") + private Long id; + + @Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java new file mode 100644 index 000000000..c85cfc33e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - AI 工具新增/修改 Request VO") +@Data +public class AiToolSaveReqVO { + + @Schema(description = "工具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19661") + private Long id; + + @Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "工具名称不能为空") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java index d27204d21..7ef208b9a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,7 +31,6 @@ public class AiWriteController { @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { return writeService.generateWriteContent(generateReqVO, getLoginUserId()); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java index 047380e42..04f99ae13 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java @@ -13,8 +13,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @Schema(description = "管理后台 - AI 写作分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class AiWritePageReqVO extends PageParam { @Schema(description = "用户编号", example = "28404") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index 7d9625f58..23aec276d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -1,9 +1,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -22,7 +21,6 @@ import java.time.LocalDateTime; @TableName("ai_chat_conversation") @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -65,21 +63,16 @@ public class AiChatConversationDO extends BaseDO { */ private Long roleId; - /** - * 知识库编号 - *

- * 关联 {@link AiKnowledgeDO#getId()} - */ - private Long knowledgeId; - /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; /** * 模型标志 + * + * 冗余 {@link AiModelDO#getModel()} 字段 */ private String model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index ecd10609f..2364d750c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -1,14 +1,14 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import org.springframework.ai.chat.messages.MessageType; @@ -20,10 +20,9 @@ import java.util.List; * @since 2024/4/14 17:35 * @since 2024/4/14 17:35 */ -@TableName("ai_chat_message") +@TableName(value = "ai_chat_message", autoResultMap = true) @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -71,23 +70,16 @@ public class AiChatMessageDO extends BaseDO { */ private Long roleId; - - /** - * 段落编号数组 - * - * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段 - */ - @TableField(typeHandler = JacksonTypeHandler.class) - private List segmentIds; - /** * 模型标志 + * + * 冗余 {@link AiModelDO#getModel()} */ private String model; /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; @@ -101,4 +93,12 @@ public class AiChatMessageDO extends BaseDO { */ private Boolean useContext; + /** + * 知识库段落编号数组 + * + * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List segmentIds; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java index 56749a1d0..a18904c02 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.image; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.baomidou.mybatisplus.annotation.KeySequence; @@ -53,9 +53,15 @@ public class AiImageDO extends BaseDO { */ private String platform; /** - * 模型 + * 模型编号 * - * 冗余 {@link AiChatModelDO#getModel()} + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; + /** + * 模型标识 + * + * 冗余 {@link AiModelDO#getModel()} */ private String model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java index 638a8ba50..e1327a50e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java @@ -2,15 +2,12 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import java.util.List; - /** * AI 知识库 DO * @@ -26,12 +23,6 @@ public class AiKnowledgeDO extends BaseDO { */ @TableId private Long id; - /** - * 用户编号 - *

- * 关联 AdminUserDO 的 userId 字段 - */ - private Long userId; /** * 知识库名称 */ @@ -42,20 +33,17 @@ public class AiKnowledgeDO extends BaseDO { private String description; /** - * 可见权限,选择哪些人可见 - *

- * -1 所有人可见,其他为各自用户编号 + * 向量模型编号 + * + * 关联 {@link AiModelDO#getId()} */ - @TableField(typeHandler = LongListTypeHandler.class) - private List visibilityPermissions; - /** - * 嵌入模型编号 - */ - private Long modelId; + private Long embeddingModelId; /** * 模型标识 + * + * 冗余 {@link AiModelDO#getModel()} */ - private String model; + private String embeddingModel; /** * topK diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index ee8bfd5aa..ac014e926 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -30,57 +29,35 @@ public class AiKnowledgeDocumentDO extends BaseDO { */ private Long knowledgeId; /** - * 文件名称 + * 文档名称 */ private String name; - /** - * 内容 - */ - private String content; /** * 文件 URL */ private String url; + /** + * 内容 + */ + private String content; + /** + * 文档长度 + */ + private Integer contentLength; + /** * 文档 token 数量 */ private Integer tokens; /** - * 文档字符数 + * 分片最大 Token 数 */ - private Integer wordCount; - - - // ========== 自定义分段所用参数 ========== - // TODO @新:3)defaultChunkSize、defaultChunkSize、minChunkSizeChars、maxNumChunks 这几个字段的命名,可能要微信一起讨论下。尽量命名保持风格统一哈。 - /** - * 每个文本块的目标 token 数 - */ - private Integer defaultSegmentTokens; - /** - * 每个文本块的最小字符数 - */ - private Integer minSegmentWordCount; - /** - * 低于此值的块会被丢弃 - */ - private Integer minChunkLengthToEmbed; - /** - * 最大块数 - */ - private Integer maxNumSegments; - /** - * 分块是否保留分隔符 - */ - private Boolean keepSeparator; - // =================================== + private Integer segmentMaxTokens; /** - * 切片状态 - *

- * 枚举 {@link AiKnowledgeDocumentStatusEnum} + * 召回次数 */ - private Integer sliceStatus; + private Integer retrievalCount; /** * 状态 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index b08e960d1..cccbd6846 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -17,17 +17,16 @@ import lombok.Data; @Data public class AiKnowledgeSegmentDO extends BaseDO { - public static final String FIELD_KNOWLEDGE_ID = "knowledgeId"; + /** + * 向量库的编号 - 空值 + */ + public static final String VECTOR_ID_EMPTY = ""; /** * 编号 */ @TableId private Long id; - /** - * 向量库的编号 - */ - private String vectorId; /** * 知识库编号 *

@@ -45,13 +44,24 @@ public class AiKnowledgeSegmentDO extends BaseDO { */ private String content; /** - * 字符数 + * 切片内容长度 */ - private Integer wordCount; + private Integer contentLength; + + /** + * 向量库的编号 + */ + private String vectorId; /** * token 数量 */ private Integer tokens; + + /** + * 召回次数 + */ + private Integer retrievalCount; + /** * 状态 *

diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java index b9768529f..6dd5d4430 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.mindmap; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -36,6 +37,12 @@ public class AiMindMapDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + /** + * 模型编号 + * + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java index e251d55c8..346811f0d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java @@ -16,7 +16,6 @@ import lombok.*; @TableName("ai_api_key") @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index f5ed533a9..bb6a3ca48 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -2,11 +2,16 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; +import java.util.List; + /** * AI 聊天角色 DO * @@ -16,7 +21,6 @@ import lombok.*; @TableName(value = "ai_chat_role", autoResultMap = true) @KeySequence("ai_chat_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor @@ -58,10 +62,25 @@ public class AiChatRoleDO extends BaseDO { /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; + /** + * 引用的知识库编号列表 + * + * 关联 {@link AiKnowledgeDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List knowledgeIds; + /** + * 引用的工具编号列表 + * + * 关联 {@link AiToolDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List toolIds; + /** * 是否公开 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java similarity index 76% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java index 7197f8b58..b39320291 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; @@ -9,21 +10,20 @@ import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** - * AI 聊天模型 DO + * AI 模型 DO * - * 默认聊天模型:{@link #status} 为开启,并且 {@link #sort} 排序第一 + * 默认模型:{@link #status} 为开启,并且 {@link #sort} 排序第一 * * @author fansili * @since 2024/4/24 19:39 */ -@TableName("ai_chat_model") -@KeySequence("ai_chat_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@TableName("ai_model") +@KeySequence("ai_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class AiChatModelDO extends BaseDO { +public class AiModelDO extends BaseDO { /** * 编号 @@ -50,6 +50,12 @@ public class AiChatModelDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + /** + * 类型 + * + * 枚举 {@link AiModelTypeEnum} + */ + private Integer type; /** * 排序值 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java new file mode 100644 index 000000000..7773e978c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.model; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction; +import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * AI 工具 DO + * + * @author 芋道源码 + */ +@TableName("ai_tool") +@KeySequence("ai_tool_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiToolDO extends BaseDO { + + /** + * 工具编号 + */ + @TableId + private Long id; + /** + * 工具名称 + * + * 对应 Bean 的名字,例如说: + * 1. {@link DirectoryListToolFunction} 的 Bean 名字是 directory_list + * 2. {@link WeatherQueryToolFunction} 的 Bean 名字是 weather_query + */ + private String name; + /** + * 工具描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java index e03d62c16..bfa7394dd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java @@ -84,6 +84,7 @@ public class AiMusicDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + // TODO @芋艿:modelId? /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java index 0d6f9c5e6..e07f994aa 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.write; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.enums.DictTypeConstants; import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; @@ -44,6 +46,12 @@ public class AiWriteDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + /** + * 模型编号 + * + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; /** * 模型 */ @@ -66,25 +74,25 @@ public class AiWriteDO extends BaseDO { /** * 长度提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LENGTH} + * 字典:{@link DictTypeConstants#AI_WRITE_LENGTH} */ private Integer length; /** * 格式提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_FORMAT} + * 字典:{@link DictTypeConstants#AI_WRITE_FORMAT} */ private Integer format; /** * 语气提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_TONE} + * 字典:{@link DictTypeConstants#AI_WRITE_TONE} */ private Integer tone; /** * 语言提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LANGUAGE} + * 字典:{@link DictTypeConstants#AI_WRITE_LANGUAGE} */ private Integer language; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index 7692d1ced..11a76cc57 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -5,10 +5,14 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; +import java.util.Collection; +import java.util.List; + /** - * AI 知识库-文档 Mapper + * AI 知识库文档 Mapper * * @author xiaoxin */ @@ -17,8 +21,19 @@ public interface AiKnowledgeDocumentMapper extends BaseMapperX selectPage(AiKnowledgeDocumentPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiKnowledgeDocumentDO::getKnowledgeId, reqVO.getKnowledgeId()) .likeIfPresent(AiKnowledgeDocumentDO::getName, reqVO.getName()) .orderByDesc(AiKnowledgeDocumentDO::getId)); } + default void updateRetrievalCountIncr(Collection ids) { + update(new LambdaUpdateWrapper() + .setSql(" retrieval_count = retrieval_count + 1") + .in(AiKnowledgeDocumentDO::getId, ids)); + } + + default List selectListByStatus(Integer status) { + return selectList(AiKnowledgeDocumentDO::getStatus, status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index f07a9a2af..3433c0b97 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -8,19 +7,26 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnow import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** - * AI 知识库基础信息 Mapper + * AI 知识库 Mapper * * @author xiaoxin */ @Mapper public interface AiKnowledgeMapper extends BaseMapperX { - default PageResult selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) { + default PageResult selectPage(AiKnowledgePageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() - .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName()) - .and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)"))) + .eqIfPresent(AiKnowledgeDO::getStatus, pageReqVO.getStatus()) + .betweenIfPresent(AiKnowledgeDO::getCreateTime, pageReqVO.getCreateTime()) .orderByDesc(AiKnowledgeDO::getId)); } + + default List selectListByStatus(Integer status) { + return selectList(AiKnowledgeDO::getStatus, status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 094f19b52..00bacd966 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -3,14 +3,19 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.MPJLambdaWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; +import java.util.Collection; import java.util.List; /** - * AI 知识库-分片 Mapper + * AI 知识库分片 Mapper * * @author xiaoxin */ @@ -20,8 +25,8 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectPage(AiKnowledgeSegmentPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId()) + .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getContent()) .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus()) - .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword()) .orderByDesc(AiKnowledgeSegmentDO::getId)); } @@ -31,4 +36,32 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectListByDocumentId(Long documentId) { + return selectList(new LambdaQueryWrapperX() + .eq(AiKnowledgeSegmentDO::getDocumentId, documentId) + .orderByDesc(AiKnowledgeSegmentDO::getId)); + } + + default List selectListByKnowledgeIdAndStatus(Long knowledgeId, Integer status) { + return selectList(AiKnowledgeSegmentDO::getKnowledgeId, knowledgeId, + AiKnowledgeSegmentDO::getStatus, status); + } + + default List selectProcessList(Collection documentIds) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapperX() + .selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId) + .selectCount(AiKnowledgeSegmentDO::getId, "count") + .select("COUNT(CASE WHEN vector_id > '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + + "' THEN 1 ELSE NULL END) AS embeddingCount") + .in(AiKnowledgeSegmentDO::getDocumentId, documentIds) + .groupBy(AiKnowledgeSegmentDO::getDocumentId); + return selectJoinList(AiKnowledgeSegmentProcessRespVO.class, wrapper); + } + + default void updateRetrievalCountIncrByIds(List ids) { + update(new LambdaUpdateWrapper() + .setSql(" retrieval_count = retrieval_count + 1") + .in(AiKnowledgeSegmentDO::getId, ids)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java new file mode 100644 index 000000000..bfe2caf52 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +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.QueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import org.apache.ibatis.annotations.Mapper; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * API 模型 Mapper + * + * @author fansili + */ +@Mapper +public interface AiChatMapper extends BaseMapperX { + + default AiModelDO selectFirstByStatus(Integer type, Integer status) { + return selectOne(new QueryWrapperX() + .eq("type", type) + .eq("status", status) + .limitN(1) + .orderByAsc("sort")); + } + + default PageResult selectPage(AiModelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiModelDO::getName, reqVO.getName()) + .eqIfPresent(AiModelDO::getModel, reqVO.getModel()) + .eqIfPresent(AiModelDO::getPlatform, reqVO.getPlatform()) + .orderByAsc(AiModelDO::getSort)); + } + + default List selectListByStatusAndType(Integer status, Integer type, + @Nullable String platform) { + return selectList(new LambdaQueryWrapperX() + .eq(AiModelDO::getStatus, status) + .eq(AiModelDO::getType, type) + .eqIfPresent(AiModelDO::getPlatform, platform) + .orderByAsc(AiModelDO::getSort)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java deleted file mode 100644 index a3136fa9f..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.module.ai.dal.mysql.model; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -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.QueryWrapperX; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.Collection; -import java.util.List; - -/** - * API 聊天模型 Mapper - * - * @author fansili - */ -@Mapper -public interface AiChatModelMapper extends BaseMapperX { - - default AiChatModelDO selectFirstByStatus(Integer status) { - return selectOne(new QueryWrapperX() - .eq("status", status) - .limitN(1) - .orderByAsc("sort")); - } - - default PageResult selectPage(AiChatModelPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .likeIfPresent(AiChatModelDO::getName, reqVO.getName()) - .eqIfPresent(AiChatModelDO::getModel, reqVO.getModel()) - .eqIfPresent(AiChatModelDO::getPlatform, reqVO.getPlatform()) - .orderByAsc(AiChatModelDO::getSort)); - } - - default List selectList(Integer status) { - return selectList(new LambdaQueryWrapperX() - .eq(AiChatModelDO::getStatus, status) - .orderByAsc(AiChatModelDO::getSort)); - } - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java new file mode 100644 index 000000000..d5d296692 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * AI 工具 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface AiToolMapper extends BaseMapperX { + + default PageResult selectPage(AiToolPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiToolDO::getName, reqVO.getName()) + .eqIfPresent(AiToolDO::getDescription, reqVO.getDescription()) + .eqIfPresent(AiToolDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AiToolDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AiToolDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(AiToolDO::getStatus, status) + .orderByDesc(AiToolDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java index 09de7263c..e979056d4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java @@ -1,4 +1,4 @@ /** - * crm 模块的 web 拓展封装 + * ai 模块的 web 拓展封装 */ -package cn.iocoder.yudao.module.crm.framework.web; +package cn.iocoder.yudao.module.ai.framework.web; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java index 8f094087f..6c35571c8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java @@ -4,17 +4,18 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -44,7 +45,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService private AiChatConversationMapper chatConversationMapper; @Resource - private AiChatModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @Resource @@ -54,9 +55,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) { // 1.1 获得 AiChatRoleDO 聊天角色 AiChatRoleDO role = createReqVO.getRoleId() != null ? chatRoleService.validateChatRole(createReqVO.getRoleId()) : null; - // 1.2 获得 AiChatModelDO 聊天模型 - AiChatModelDO model = role != null && role.getModelId() != null ? chatModalService.validateChatModel(role.getModelId()) - : chatModalService.getRequiredDefaultChatModel(); + // 1.2 获得 AiModelDO 聊天模型 + AiModelDO model = role != null && role.getModelId() != null ? modalService.validateModel(role.getModelId()) + : modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); Assert.notNull(model, "必须找到默认模型"); validateChatModel(model); @@ -67,7 +68,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService // 2. 创建 AiChatConversationDO 聊天对话 AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false) - .setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId()) + .setModelId(model.getId()).setModel(model.getModel()) .setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts()); if (role != null) { conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage()); @@ -86,9 +87,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService throw exception(CHAT_CONVERSATION_NOT_EXISTS); } // 1.2 校验模型是否存在(修改模型的情况) - AiChatModelDO model = null; + AiModelDO model = null; if (updateReqVO.getModelId() != null) { - model = chatModalService.validateChatModel(updateReqVO.getModelId()); + model = modalService.validateModel(updateReqVO.getModelId()); } // 1.3 校验知识库是否存在 @@ -139,10 +140,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService chatConversationMapper.deleteById(id); } - private void validateChatModel(AiChatModelDO model) { + private void validateChatModel(AiModelDO model) { if (ObjectUtil.isAllNotEmpty(model.getTemperature(), model.getMaxTokens(), model.getMaxContexts())) { return; } + Assert.equals(model.getType(), AiModelTypeEnum.CHAT.getType(), "模型类型不正确:" + model); throw exception(CHAT_CONVERSATION_MODEL_ERROR); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index d332fbf1a..f310ba69f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -10,19 +10,24 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; -import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; +import cn.iocoder.yudao.module.ai.service.model.AiToolService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; @@ -34,18 +39,19 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; 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.convertSet; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST; @@ -58,138 +64,199 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N @Slf4j public class AiChatMessageServiceImpl implements AiChatMessageService { + /** + * 知识库转 {@link UserMessage} 的内容模版 + */ + private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 标记中的内容作为本次对话的参考:\n\n" + + "%s\n\n" + // 多个 的拼接 + "回答要求:\n- 避免提及你是从 获取的知识。"; + @Resource private AiChatMessageMapper chatMessageMapper; @Resource private AiChatConversationService chatConversationService; @Resource - private AiChatModelService chatModalService; + private AiChatRoleService chatRoleService; @Resource - private AiApiKeyService apiKeyService; + private AiModelService modalService; @Resource private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; + @Resource + private AiToolService toolService; @Transactional(rollbackFor = Exception.class) public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { // 1.1 校验对话存在 - AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + AiChatConversationDO conversation = chatConversationService + .validateChatConversationExists(sendReqVO.getConversationId()); if (ObjUtil.notEqual(conversation.getUserId(), userId)) { throw exception(CHAT_CONVERSATION_NOT_EXISTS); } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 - AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + AiModelDO model = modalService.validateModel(conversation.getModelId()); + ChatModel chatModel = modalService.getChatModel(model.getId()); - // 2. 插入 user 发送消息 + // 2. 知识库找回 + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), + conversation); + + // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, - userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), + null); // 3.1 插入 assistant 接收消息 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, - userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), + knowledgeSegments); - // 3.2 召回段落 - List segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId()); - - // 3.3 创建 chat 需要的 Prompt - Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO); + // 3.2 创建 chat 需要的 Prompt + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); ChatResponse chatResponse = chatModel.call(prompt); - // 3.4 段式返回 - String newContent = chatResponse.getResult().getOutput().getContent(); - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent)); - return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); + // 3.3 更新响应内容 + String newContent = chatResponse.getResult().getOutput().getText(); + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); + // 3.4 响应结果 + List segments = BeanUtils.toBean(knowledgeSegments, + AiChatMessageRespVO.KnowledgeSegment.class, + segment -> { + AiKnowledgeDocumentDO document = knowledgeDocumentService + .getKnowledgeDocument(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); + return new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(newContent).setSegments(segments)); } @Override - public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) { + public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, + Long userId) { // 1.1 校验对话存在 - AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + AiChatConversationDO conversation = chatConversationService + .validateChatConversationExists(sendReqVO.getConversationId()); if (ObjUtil.notEqual(conversation.getUserId(), userId)) { throw exception(CHAT_CONVERSATION_NOT_EXISTS); } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 - AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + AiModelDO model = modalService.validateModel(conversation.getModelId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getId()); - // 2. 插入 user 发送消息 + // 2. 知识库找回 + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), + conversation); + + // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, - userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), + null); - // 3.1 插入 assistant 接收消息 + // 4.1 插入 assistant 接收消息 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, - userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), + knowledgeSegments); - - // 3.2 召回段落 - List segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId()); - - // 3.3 构建 Prompt,并进行调用 - Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO); + // 4.2 构建 Prompt,并进行调用 + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); Flux streamResponse = chatModel.stream(prompt); - // 3.4 流式返回 - // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 + // 4.3 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + // 处理知识库的返回,只有首次才有 + List segments = null; + if (StrUtil.isEmpty(contentBuffer)) { + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, + segment -> TenantUtils.executeIgnore(() -> { + AiKnowledgeDocumentDO document = knowledgeDocumentService + .getKnowledgeDocument(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + })); + } + // 响应结果 + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); - // 响应结果 - return success(new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent))); + return success(new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(newContent).setSegments(segments))); }).doOnComplete(() -> { // 忽略租户,因为 Flux 异步无法透传租户 - TenantUtils.executeIgnore(() -> - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)) - .setContent(contentBuffer.toString()))); + TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); }).doOnError(throwable -> { log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); // 忽略租户,因为 Flux 异步无法透传租户 - TenantUtils.executeIgnore(() -> - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); + TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); } - private List recallSegment(String content, Long knowledgeId) { - if (Objects.isNull(knowledgeId)) { + private List recallKnowledgeSegment(String content, + AiChatConversationDO conversation) { + // 1. 查询聊天角色 + if (conversation == null || conversation.getRoleId() == null) { return Collections.emptyList(); } - return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content)); - } - - private Prompt buildPrompt(AiChatConversationDO conversation, List messages,List segmentList, - AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { - // 1. 构建 Prompt Message 列表 - List chatMessages = new ArrayList<>(); - - // 1.1 召回内容消息构建 - if (CollUtil.isNotEmpty(segmentList)) { - PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage()); - StringBuilder infoBuilder = StrUtil.builder(); - segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent())); - Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString())); - chatMessages.add(message); + AiChatRoleDO role = chatRoleService.getChatRole(conversation.getRoleId()); + if (role == null || CollUtil.isEmpty(role.getKnowledgeIds())) { + return Collections.emptyList(); } - // 1.2 system context 角色设定 + // 2. 遍历找回 + List knowledgeSegments = new ArrayList<>(); + for (Long knowledgeId : role.getKnowledgeIds()) { + knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() + .setKnowledgeId(knowledgeId).setContent(content))); + } + return knowledgeSegments; + } + + private Prompt buildPrompt(AiChatConversationDO conversation, List messages, + List knowledgeSegments, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { + List chatMessages = new ArrayList<>(); + // 1.1 System Context 角色设定 if (StrUtil.isNotBlank(conversation.getSystemMessage())) { chatMessages.add(new SystemMessage(conversation.getSystemMessage())); } - // 1.3 history message 历史消息 + + // 1.2 历史 history message 历史消息 List contextMessages = filterContextMessages(messages, conversation, sendReqVO); - contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); - // 1.4 user message 新发送消息 + contextMessages + .forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); + + // 1.3 当前 user message 新发送消息 chatMessages.add(new UserMessage(sendReqVO.getContent())); - // 2. 构建 ChatOptions 对象 + // 1.4 知识库,通过 UserMessage 实现 + if (CollUtil.isNotEmpty(knowledgeSegments)) { + String reference = knowledgeSegments.stream() + .map(segment -> "" + segment.getContent() + "") + .collect(Collectors.joining("\n\n")); + chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); + } + + // 2.1 查询 tool 工具 + Set toolNames = null; + if (conversation.getRoleId() != null) { + AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId()); + if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) { + toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); + } + } + // 2.2 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), - conversation.getTemperature(), conversation.getMaxTokens()); + conversation.getTemperature(), conversation.getMaxTokens(), toolNames); return new Prompt(chatMessages, chatOptions); } @@ -204,8 +271,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { * @return 消息上下文 */ private List filterContextMessages(List messages, - AiChatConversationDO conversation, - AiChatMessageSendReqVO sendReqVO) { + AiChatConversationDO conversation, + AiChatMessageSendReqVO sendReqVO) { if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { return Collections.emptyList(); } @@ -216,7 +283,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { continue; } AiChatMessageDO userMessage = CollUtil.get(messages, i - 1); - if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) + if (userMessage == null + || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) || StrUtil.isEmpty(assistantMessage.getContent())) { continue; } @@ -233,11 +301,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, - AiChatModelDO model, Long userId, Long roleId, - MessageType messageType, String content, Boolean useContext) { + AiModelDO model, Long userId, Long roleId, + MessageType messageType, String content, Boolean useContext, + List knowledgeSegments) { AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) - .setType(messageType.getValue()).setContent(content).setUseContext(useContext); + .setType(messageType.getValue()).setContent(content).setUseContext(useContext) + .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)); message.setCreateTime(LocalDateTime.now()); chatMessageMapper.insert(message); return message; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index e8532a576..60ca9ac99 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.service.image; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; @@ -12,15 +13,19 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.image.ImageModel; @@ -54,15 +59,15 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; @Slf4j public class AiImageServiceImpl implements AiImageService { + @Resource + private AiModelService modelService; + @Resource private AiImageMapper imageMapper; @Resource private FileApi fileApi; - @Resource - private AiApiKeyService apiKeyService; - @Override public PageResult getImagePageMy(Long userId, AiImagePageReqVO pageReqVO) { return imageMapper.selectPageMy(userId, pageReqVO); @@ -88,23 +93,31 @@ public class AiImageServiceImpl implements AiImageService { @Override public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) { - // 1. 保存数据库 - AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) - .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); + // 1. 校验模型 + AiModelDO model = modelService.validateModel(drawReqVO.getModelId()); + + // 2. 保存数据库 + AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId) + .setPlatform(model.getPlatform()).setModelId(model.getId()).setModel(model.getModel()) + .setPublicStatus(false).setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); imageMapper.insert(image); - // 2. 异步绘制,后续前端通过返回的 id 进行轮询结果 - getSelf().executeDrawImage(image, drawReqVO); + + // 3. 异步绘制,后续前端通过返回的 id 进行轮询结果 + getSelf().executeDrawImage(image, drawReqVO, model); return image.getId(); } @Async - public void executeDrawImage(AiImageDO image, AiImageDrawReqVO req) { + public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) { try { // 1.1 构建请求 - ImageOptions request = buildImageOptions(req); + ImageOptions request = buildImageOptions(reqVO, model); // 1.2 执行请求 - ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform())); - ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request)); + ImageModel imageModel = modelService.getImageModel(model.getId()); + ImageResponse response = imageModel.call(new ImagePrompt(reqVO.getPrompt(), request)); + if (response.getResult() == null) { + throw new IllegalArgumentException("生成结果为空"); + } // 2. 上传到文件服务 String b64Json = response.getResult().getOutput().getB64Json(); @@ -116,49 +129,49 @@ public class AiImageServiceImpl implements AiImageService { imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(AiImageStatusEnum.SUCCESS.getStatus()) .setPicUrl(filePath).setFinishTime(LocalDateTime.now())); } catch (Exception ex) { - log.error("[doDall][image({}) 生成异常]", image, ex); + log.error("[executeDrawImage][image({}) 生成异常]", image, ex); imageMapper.updateById(new AiImageDO().setId(image.getId()) .setStatus(AiImageStatusEnum.FAIL.getStatus()) .setErrorMessage(ex.getMessage()).setFinishTime(LocalDateTime.now())); } } - private static ImageOptions buildImageOptions(AiImageDrawReqVO draw) { - if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { + private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { + if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { // https://platform.openai.com/docs/api-reference/images/create - return OpenAiImageOptions.builder().withModel(draw.getModel()) + return OpenAiImageOptions.builder().withModel(model.getModel()) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 .withResponseFormat("b64_json") .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage - return StabilityAiImageOptions.builder().withModel(draw.getModel()) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) - .withSeed(Long.valueOf(draw.getOptions().get("seed"))) - .withCfgScale(Float.valueOf(draw.getOptions().get("scale"))) - .withSteps(Integer.valueOf(draw.getOptions().get("steps"))) - .withSampler(String.valueOf(draw.getOptions().get("sampler"))) - .withStylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) - .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) + return StabilityAiImageOptions.builder().model(model.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .seed(Long.valueOf(draw.getOptions().get("seed"))) + .cfgScale(Float.valueOf(draw.getOptions().get("scale"))) + .steps(Integer.valueOf(draw.getOptions().get("steps"))) + .sampler(String.valueOf(draw.getOptions().get("sampler"))) + .stylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) + .clipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { - return TongYiImagesOptions.builder() - .withModel(draw.getModel()).withN(1) + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { + return DashScopeImageOptions.builder() + .withModel(model.getModel()).withN(1) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { return QianFanImageOptions.builder() - .withModel(draw.getModel()).withN(1) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .model(model.getModel()).N(1) + .height(draw.getHeight()).width(draw.getWidth()) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { return ZhiPuAiImageOptions.builder() - .withModel(draw.getModel()) + .model(model.getModel()) .build(); } - throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform()); + throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform()); } @Override @@ -205,52 +218,56 @@ public class AiImageServiceImpl implements AiImageService { @Override @Transactional(rollbackFor = Exception.class) - public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); - // 1. 保存数据库 - AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) + public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO drawReqVO) { + // 1. 校验模型 + AiModelDO model = modelService.validateModel(drawReqVO.getModelId()); + Assert.equals(model.getPlatform(), AiPlatformEnum.MIDJOURNEY.getPlatform(), "平台不匹配"); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(model.getId()); + + // 2. 保存数据库 + AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) - .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()); + .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()).setModelId(model.getId()).setModel(model.getName()); imageMapper.insert(image); - // 2. 调用 Midjourney Proxy 提交任务 - List base64Array = StrUtil.isBlank(reqVO.getReferImageUrl()) ? null : - Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(reqVO.getReferImageUrl())))); + // 3. 调用 Midjourney Proxy 提交任务 + List base64Array = StrUtil.isBlank(drawReqVO.getReferImageUrl()) ? null : + Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(drawReqVO.getReferImageUrl())))); MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest( - base64Array, reqVO.getPrompt(),null, - MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(), - reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel())); + base64Array, drawReqVO.getPrompt(),null, + MidjourneyApi.ImagineRequest.buildState(drawReqVO.getWidth(), + drawReqVO.getHeight(), drawReqVO.getVersion(), model.getModel())); MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest); - // 3. 情况一【失败】:抛出业务异常 + // 4.1 情况一【失败】:抛出业务异常 if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) { String description = imagineResponse.description().contains("quota_not_enough") ? "账户余额不足" : imagineResponse.description(); throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); } - // 4. 情况二【成功】:更新 taskId 和参数 + // 4.2 情况二【成功】:更新 taskId 和参数 imageMapper.updateById(new AiImageDO().setId(image.getId()) - .setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(reqVO))); + .setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(drawReqVO))); return image.getId(); } @Override public Integer midjourneySync() { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image - List imageList = imageMapper.selectListByStatusAndPlatform( + List images = imageMapper.selectListByStatusAndPlatform( AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform()); - if (CollUtil.isEmpty(imageList)) { + if (CollUtil.isEmpty(images)) { return 0; } // 1.2 调用 Midjourney Proxy 获取任务进展 - List taskList = midjourneyApi.getTaskList(convertSet(imageList, AiImageDO::getTaskId)); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(images.get(0).getModelId()); + List taskList = midjourneyApi.getTaskList(convertSet(images, AiImageDO::getTaskId)); Map taskMap = convertMap(taskList, MidjourneyApi.Notify::id); // 2. 逐个处理,更新进展 int count = 0; - for (AiImageDO image : imageList) { + for (AiImageDO image : images) { MidjourneyApi.Notify notify = taskMap.get(image.getTaskId()); if (notify == null) { log.error("[midjourneySync][image({}) 查询不到进展]", image); @@ -308,12 +325,12 @@ public class AiImageServiceImpl implements AiImageService { @Override public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); // 1.1 检查 image AiImageDO image = validateImageExists(reqVO.getId()); if (ObjUtil.notEqual(userId, image.getUserId())) { throw exception(IMAGE_NOT_EXISTS); } + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(image.getModelId()); // 1.2 检查 customId MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(), buttonX -> buttonX.customId().equals(reqVO.getCustomId())); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 3de0ac01d..8ff137b33 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -1,26 +1,41 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + /** - * AI 知识库-文档 Service 接口 + * AI 知识库文档 Service 接口 * * @author xiaoxin */ public interface AiKnowledgeDocumentService { /** - * 创建文档 + * 创建文档(单个) * * @param createReqVO 文档创建 Request VO * @return 文档编号 */ Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); + /** + * 创建文档(多个) + * + * @param createListReqVO 批量创建 Request VO + * @return 文档编号列表 + */ + List createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO); /** * 获取文档分页 @@ -30,10 +45,74 @@ public interface AiKnowledgeDocumentService { */ PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO); + /** + * 获取文档详情 + * + * @param id 文档编号 + * @return 文档详情 + */ + AiKnowledgeDocumentDO getKnowledgeDocument(Long id); + /** * 更新文档 * * @param reqVO 更新信息 */ void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO); + + /** + * 更新文档状态 + * + * @param reqVO 更新状态信息 + */ + void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO); + + /** + * 更新文档检索次数(增加 +1) + * + * @param ids 文档编号列表 + */ + void updateKnowledgeDocumentRetrievalCountIncr(Collection ids); + + /** + * 删除文档 + * + * @param id 文档编号 + */ + void deleteKnowledgeDocument(Long id); + + /** + * 校验文档是否存在 + * + * @param id 文档编号 + * @return 文档信息 + */ + AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id); + + /** + * 读取 URL 内容 + * + * @param url URL + * @return 内容 + */ + String readUrl(String url); + + /** + * 获取文档列表 + * + * @param ids 文档编号列表 + * @return 文档列表 + */ + List getKnowledgeDocumentList(Collection ids); + + /** + * 获取文档 Map + * + * @param ids 文档编号列表 + * @return 文档 Map + */ + default Map getKnowledgeDocumentMap(Collection ids) { + return convertMap(getKnowledgeDocumentList(ids), AiKnowledgeDocumentDO::getId); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index ff475f92c..2d78f94f3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -1,34 +1,37 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; -import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; -import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.ai.tokenizer.TokenCountEstimator; -import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.context.annotation.Lazy; import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 知识库文档 Service 实现类 @@ -40,91 +43,172 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCU public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService { @Resource - private AiKnowledgeDocumentMapper documentMapper; - @Resource - private AiKnowledgeSegmentMapper segmentMapper; + private AiKnowledgeDocumentMapper knowledgeDocumentMapper; @Resource private TokenCountEstimator tokenCountEstimator; + @Resource + private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + @Lazy // 延迟加载,避免循环依赖 private AiKnowledgeService knowledgeService; @Override - @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { - // 0. 校验并获取向量存储实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(createReqVO.getKnowledgeId()); + // 1. 校验参数 + knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); - // 1.1 下载文档 - TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl())); - List documents = loader.get(); - Document document = CollUtil.getFirst(documents); - // 1.2 文档记录入库 - String content = document.getContent(); + // 2. 下载文档 + String content = readUrl(createReqVO.getUrl()); + + // 3. 文档记录入库 AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) - .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); - documentMapper.insert(documentDO); - Long documentId = documentDO.getId(); - if (CollUtil.isEmpty(documents)) { - return documentId; + .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content)) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + knowledgeDocumentMapper.insert(documentDO); + + // 4. 文档切片入库(异步) + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), content); + return documentDO.getId(); + } + + @Override + public List createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO) { + // 1. 校验参数 + knowledgeService.validateKnowledgeExists(createListReqVO.getKnowledgeId()); + + // 2. 下载文档 + List contents = convertList(createListReqVO.getList(), document -> readUrl(document.getUrl())); + + // 3. 文档记录入库 + List documentDOs = new ArrayList<>(createListReqVO.getList().size()); + for (int i = 0; i < createListReqVO.getList().size(); i++) { + AiKnowledgeDocumentCreateListReqVO.Document documentVO = createListReqVO.getList().get(i); + String content = contents.get(i); + documentDOs.add(BeanUtils.toBean(documentVO, AiKnowledgeDocumentDO.class) + .setKnowledgeId(createListReqVO.getKnowledgeId()) + .setContent(content).setContentLength(content.length()) + .setTokens(tokenCountEstimator.estimate(content)) + .setSegmentMaxTokens(createListReqVO.getSegmentMaxTokens()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); } + knowledgeDocumentMapper.insertBatch(documentDOs); - // 2 构造文本分段器 - TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(), - createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator()); - // 2.1 文档分段 - List segments = tokenTextSplitter.apply(documents); - // 2.2 分段内容入库 - List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) - .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) - .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length()) - .setStatus(CommonStatusEnum.ENABLE.getStatus())); - segmentMapper.insertBatch(segmentDOList); - - // 3. 向量化并存储 - segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId())); - vectorStore.add(segments); - return documentId; + // 4. 批量创建文档切片(异步) + documentDOs.forEach(documentDO -> knowledgeSegmentService + .createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), documentDO.getContent())); + return convertList(documentDOs, AiKnowledgeDocumentDO::getId); } @Override public PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) { - return documentMapper.selectPage(pageReqVO); + return knowledgeDocumentMapper.selectPage(pageReqVO); + } + + @Override + public AiKnowledgeDocumentDO getKnowledgeDocument(Long id) { + return knowledgeDocumentMapper.selectById(id); } @Override public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) { // 1. 校验文档是否存在 - validateKnowledgeDocumentExists(reqVO.getId()); + AiKnowledgeDocumentDO oldDocument = validateKnowledgeDocumentExists(reqVO.getId()); + // 2. 更新文档 AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class); - documentMapper.updateById(document); + knowledgeDocumentMapper.updateById(document); + + // 3. 如果处于开启状态,并且最大 tokens 发生变化,则 segment 需要重新索引 + if (CommonStatusEnum.isEnable(oldDocument.getStatus()) + && reqVO.getSegmentMaxTokens() != null + && ObjUtil.notEqual(reqVO.getSegmentMaxTokens(), oldDocument.getSegmentMaxTokens())) { + // 删除旧的文档切片 + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId()); + // 重新创建文档切片 + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), oldDocument.getContent()); + } } - /** - * 校验文档是否存在 - * - * @param id 文档编号 - * @return 文档信息 - */ - private AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { - AiKnowledgeDocumentDO knowledgeDocument = documentMapper.selectById(id); + @Override + public void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO) { + // 1. 校验存在 + AiKnowledgeDocumentDO document = validateKnowledgeDocumentExists(reqVO.getId()); + + // 2. 更新状态 + knowledgeDocumentMapper.updateById(new AiKnowledgeDocumentDO() + .setId(reqVO.getId()).setStatus(reqVO.getStatus())); + + // 3. 处理文档切片 + if (CommonStatusEnum.isEnable(reqVO.getStatus())) { + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), document.getContent()); + } else { + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteKnowledgeDocument(Long id) { + // 1. 校验存在 + validateKnowledgeDocumentExists(id); + + // 2. 删除 + knowledgeDocumentMapper.deleteById(id); + + // 3. 删除对应的段落 + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(id); + } + + @Override + public void updateKnowledgeDocumentRetrievalCountIncr(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + knowledgeDocumentMapper.updateRetrievalCountIncr(ids); + } + + @Override + public AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { + AiKnowledgeDocumentDO knowledgeDocument = knowledgeDocumentMapper.selectById(id); if (knowledgeDocument == null) { throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS); } return knowledgeDocument; } - private org.springframework.core.io.Resource downloadFile(String url) { + @Override + public String readUrl(String url) { + // 下载文件 + ByteArrayResource resource; try { byte[] bytes = HttpUtil.downloadBytes(url); - return new ByteArrayResource(bytes); + if (bytes.length == 0) { + throw exception(KNOWLEDGE_DOCUMENT_FILE_EMPTY); + } + resource = new ByteArrayResource(bytes); } catch (Exception e) { - log.error("[downloadFile][url({}) 下载失败]", url, e); - throw new RuntimeException(e); + log.error("[readUrl][url({}) 读取失败]", url, e); + throw exception(KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL); } + + // 读取文件 + TikaDocumentReader loader = new TikaDocumentReader(resource); + List documents = loader.get(); + Document document = CollUtil.getFirst(documents); + if (document == null || StrUtil.isEmpty(document.getText())) { + throw exception(KNOWLEDGE_DOCUMENT_FILE_READ_FAIL); + } + return document.getText(); + } + + @Override + public List getKnowledgeDocumentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return knowledgeDocumentMapper.selectBatchIds(ids); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 91bffc276..54f721705 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -2,12 +2,19 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; +import org.springframework.scheduling.annotation.Async; +import java.util.Collection; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * AI 知识库段落 Service 接口 @@ -16,6 +23,32 @@ import java.util.List; */ public interface AiKnowledgeSegmentService { + /** + * 获取知识库段落详情 + * + * @param id 段落编号 + * @return 段落详情 + */ + AiKnowledgeSegmentDO getKnowledgeSegment(Long id); + + /** + * 获取知识库段落列表 + * + * @param ids 段落编号列表 + * @return 段落列表 + */ + List getKnowledgeSegmentList(Collection ids); + + /** + * 获取知识库段落 Map + * + * @param ids 段落编号列表 + * @return 段落 Map + */ + default Map getKnowledgeSegmentMap(Collection ids) { + return convertMap(getKnowledgeSegmentList(ids), AiKnowledgeSegmentDO::getId); + } + /** * 获取段落分页 * @@ -24,12 +57,39 @@ public interface AiKnowledgeSegmentService { */ PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO); + /** + * 基于 content 内容,切片创建多个段落 + * + * @param documentId 知识库文档编号 + * @param content 文档内容 + */ + void createKnowledgeSegmentBySplitContent(Long documentId, String content); + + /** + * 【异步】基于 content 内容,切片创建多个段落 + * + * @param documentId 知识库文档编号 + * @param content 文档内容 + */ + @Async + default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) { + createKnowledgeSegmentBySplitContent(documentId, content); + } + + /** + * 创建知识库段落 + * + * @param createReqVO 创建信息 + * @return 段落编号 + */ + Long createKnowledgeSegment(AiKnowledgeSegmentSaveReqVO createReqVO); + /** * 更新段落的内容 * * @param reqVO 更新内容 */ - void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO); + void updateKnowledgeSegment(AiKnowledgeSegmentSaveReqVO reqVO); /** * 更新段落的状态 @@ -39,11 +99,52 @@ public interface AiKnowledgeSegmentService { void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); /** - * 召回段落 + * 重新索引知识库下的所有文档段落 * - * @param reqVO 召回请求信息 - * @return 召回的段落 + * @param knowledgeId 知识库编号 */ - List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO); + void reindexKnowledgeSegmentByKnowledgeId(Long knowledgeId); + + /** + * 【异步】重新索引知识库下的所有文档段落 + * + * @param knowledgeId 知识库编号 + */ + @Async + default void reindexByKnowledgeIdAsync(Long knowledgeId) { + reindexKnowledgeSegmentByKnowledgeId(knowledgeId); + } + + /** + * 根据文档编号删除段落 + * + * @param documentId 文档编号 + */ + void deleteKnowledgeSegmentByDocumentId(Long documentId); + + /** + * 搜索知识库段落,并返回结果 + * + * @param reqBO 搜索请求信息 + * @return 搜索结果段落列表 + */ + List searchKnowledgeSegment(AiKnowledgeSegmentSearchReqBO reqBO); + + /** + * 根据 URL 内容,切片创建多个段落 + * + * @param url URL 地址 + * @param segmentMaxTokens 段落最大 Token 数 + * @return 切片后的段落列表 + */ + List splitContent(String url, Integer segmentMaxTokens); + + /** + * 获取文档处理进度(多个) + * + * @param documentIds 文档编号列表 + * @return 文档处理列表 + */ + List getKnowledgeSegmentProcessList(List documentIds); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 5523fe278..20f881cf1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -2,31 +2,39 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; +import org.springframework.ai.tokenizer.TokenCountEstimator; +import org.springframework.ai.transformer.splitter.TextSplitter; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Objects; +import java.util.*; 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.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; /** @@ -38,15 +46,28 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGM @Slf4j public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { + private static final String VECTOR_STORE_METADATA_KNOWLEDGE_ID = "knowledgeId"; + private static final String VECTOR_STORE_METADATA_DOCUMENT_ID = "documentId"; + private static final String VECTOR_STORE_METADATA_SEGMENT_ID = "segmentId"; + + private static final Map> VECTOR_STORE_METADATA_TYPES = Map.of( + VECTOR_STORE_METADATA_KNOWLEDGE_ID, String.class, + VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, + VECTOR_STORE_METADATA_SEGMENT_ID, String.class); + @Resource private AiKnowledgeSegmentMapper segmentMapper; @Resource private AiKnowledgeService knowledgeService; @Resource - private AiChatModelService chatModelService; + @Lazy // 延迟加载,避免循环依赖 + private AiKnowledgeDocumentService knowledgeDocumentService; @Resource - private AiApiKeyService apiKeyService; + private AiModelService modelService; + + @Resource + private TokenCountEstimator tokenCountEstimator; @Override public PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { @@ -54,67 +75,198 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } @Override - public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) { + public void createKnowledgeSegmentBySplitContent(Long documentId, String content) { // 1. 校验 - AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId()); + AiKnowledgeDocumentDO documentDO = knowledgeDocumentService.validateKnowledgeDocumentExists(documentId); + AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId()); + VectorStore vectorStore = getVectorStoreById(knowledgeDO); - // 2.1 获取知识库向量实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId()); - // 2.2 删除原向量 - vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId())); - // 2.3 重新向量化 - Document document = new Document(reqVO.getContent()); - document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId()); - vectorStore.add(List.of(document)); + // 2. 文档切片 + List documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens()); - // 3. 更新段落内容 - AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); - knowledgeSegment.setVectorId(document.getId()); - segmentMapper.updateById(knowledgeSegment); + // 3.1 存储切片 + List segmentDOs = convertList(documentSegments, segment -> { + if (StrUtil.isEmpty(segment.getText())) { + return null; + } + return new AiKnowledgeSegmentDO().setKnowledgeId(documentDO.getKnowledgeId()).setDocumentId(documentId) + .setContent(segment.getText()).setContentLength(segment.getText().length()) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY) + .setTokens(tokenCountEstimator.estimate(segment.getText())) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + segmentMapper.insertBatch(segmentDOs); + // 3.2 切片向量化 + for (int i = 0; i < documentSegments.size(); i++) { + Document segment = documentSegments.get(i); + AiKnowledgeSegmentDO segmentDO = segmentDOs.get(i); + writeVectorStore(vectorStore, segmentDO, segment); + } + } + + @Override + public void updateKnowledgeSegment(AiKnowledgeSegmentSaveReqVO reqVO) { + // 1. 校验 + AiKnowledgeSegmentDO oldSegment = validateKnowledgeSegmentExists(reqVO.getId()); + + // 2. 删除向量 + VectorStore vectorStore = getVectorStoreById(oldSegment.getKnowledgeId()); + deleteVectorStore(vectorStore, oldSegment); + + // 3.1 更新切片 + AiKnowledgeSegmentDO newSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); + segmentMapper.updateById(newSegment); + // 3.2 重新向量化,必须开启状态 + if (CommonStatusEnum.isEnable(oldSegment.getStatus())) { + newSegment.setKnowledgeId(oldSegment.getKnowledgeId()).setDocumentId(oldSegment.getDocumentId()); + writeVectorStore(vectorStore, newSegment, new Document(newSegment.getContent())); + } + } + + @Override + public void deleteKnowledgeSegmentByDocumentId(Long documentId) { + // 1. 查询需要删除的段落 + List segments = segmentMapper.selectListByDocumentId(documentId); + if (CollUtil.isEmpty(segments)) { + return; + } + + // 2. 批量删除段落记录 + segmentMapper.deleteByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); + + // 3. 删除向量存储中的段落 + VectorStore vectorStore = getVectorStoreById(segments.get(0).getKnowledgeId()); + vectorStore.delete(convertList(segments, AiKnowledgeSegmentDO::getVectorId)); } @Override public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) { - // 0 校验 - AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId()); - // 1 获取知识库向量实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId()); - AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); + // 1. 校验 + AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(reqVO.getId()); - if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { - // 2.1 启用重新向量化 - Document document = new Document(oldKnowledgeSegment.getContent()); - document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId()); - vectorStore.add(List.of(document)); - knowledgeSegment.setVectorId(document.getId()); + // 2. 获取知识库向量实例 + VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId()); + + // 3. 更新状态 + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(reqVO.getId()).setStatus(reqVO.getStatus())); + + // 4. 更新向量 + if (CommonStatusEnum.isEnable(reqVO.getStatus())) { + writeVectorStore(vectorStore, segment, new Document(segment.getContent())); } else { - // 2.2 禁用删除向量 - vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId())); - knowledgeSegment.setVectorId(""); + deleteVectorStore(vectorStore, segment); } - // 3 更新段落状态 - segmentMapper.updateById(knowledgeSegment); } @Override - public List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) { + public void reindexKnowledgeSegmentByKnowledgeId(Long knowledgeId) { + // 1.1 校验知识库存在 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(knowledgeId); + // 1.2 获取知识库向量实例 + VectorStore vectorStore = getVectorStoreById(knowledge); + + // 2.1 查询知识库下的所有启用状态的段落 + List segments = segmentMapper.selectListByKnowledgeIdAndStatus( + knowledgeId, CommonStatusEnum.ENABLE.getStatus()); + if (CollUtil.isEmpty(segments)) { + return; + } + // 2.2 遍历所有段落,重新索引 + for (AiKnowledgeSegmentDO segment : segments) { + // 删除旧的向量 + deleteVectorStore(vectorStore, segment); + // 重新创建向量 + writeVectorStore(vectorStore, segment, new Document(segment.getContent())); + } + log.info("[reindexKnowledgeSegmentByKnowledgeId][知识库({}) 重新索引完成,共处理 {} 个段落]", + knowledgeId, segments.size()); + } + + private void writeVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO, Document segment) { + // 1. 向量存储 + // 为什么要 toString 呢?因为部分 VectorStore 实现,不支持 Long 类型,例如说 QdrantVectorStore + segment.getMetadata().put(VECTOR_STORE_METADATA_KNOWLEDGE_ID, segmentDO.getKnowledgeId().toString()); + segment.getMetadata().put(VECTOR_STORE_METADATA_DOCUMENT_ID, segmentDO.getDocumentId().toString()); + segment.getMetadata().put(VECTOR_STORE_METADATA_SEGMENT_ID, segmentDO.getId().toString()); + vectorStore.add(List.of(segment)); + + // 2. 更新向量 ID + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(segmentDO.getId()).setVectorId(segment.getId())); + } + + private void deleteVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO) { + // 1. 更新向量 ID + if (StrUtil.isEmpty(segmentDO.getVectorId())) { + return; + } + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(segmentDO.getId()) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY)); + + // 2. 删除向量 + vectorStore.delete(List.of(segmentDO.getVectorId())); + } + + @Override + public List searchKnowledgeSegment(AiKnowledgeSegmentSearchReqBO reqBO) { // 1. 校验 - AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId()); - AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); - // 2. 获取向量存储实例 - VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); - - // 3.1 向量检索 - List documentList = vectorStore.similaritySearch(SearchRequest.query(reqVO.getContent()) - .withTopK(knowledge.getTopK()) - .withSimilarityThreshold(knowledge.getSimilarityThreshold()) - .withFilterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build())); - if (CollUtil.isEmpty(documentList)) { + // 2.1 向量检索 + VectorStore vectorStore = getVectorStoreById(knowledge); + List documents = vectorStore.similaritySearch(SearchRequest.builder() + .query(reqBO.getContent()) + .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK())) + .similarityThreshold( + ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) + .filterExpression(new FilterExpressionBuilder() + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()) + .build()) + .build()); + if (CollUtil.isEmpty(documents)) { return ListUtil.empty(); } - // 3.2 段落召回 - return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class)); + // 2.2 段落召回 + List segments = segmentMapper + .selectListByVectorIds(convertList(documents, Document::getId)); + if (CollUtil.isEmpty(segments)) { + return ListUtil.empty(); + } + + // 3. 增加召回次数 + segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); + + // 4. 构建结果 + List result = convertList(segments, segment -> { + Document document = CollUtil.findOne(documents, // 找到对应的文档 + doc -> Objects.equals(doc.getId(), segment.getVectorId())); + if (document == null) { + return null; + } + return BeanUtils.toBean(segment, AiKnowledgeSegmentSearchRespBO.class) + .setScore(document.getScore()); + }); + result.sort((o1, o2) -> Double.compare(o2.getScore(), o1.getScore())); // 按照分数降序排序 + return result; + } + + @Override + public List splitContent(String url, Integer segmentMaxTokens) { + // 1. 读取 URL 内容 + String content = knowledgeDocumentService.readUrl(url); + + // 2. 文档切片 + List documentSegments = splitContentByToken(content, segmentMaxTokens); + + // 3. 转换为段落对象 + return convertList(documentSegments, segment -> { + if (StrUtil.isEmpty(segment.getText())) { + return null; + } + return new AiKnowledgeSegmentDO() + .setContent(segment.getText()) + .setContentLength(segment.getText().length()) + .setTokens(tokenCountEstimator.estimate(segment.getText())); + }); } /** @@ -131,4 +283,75 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return knowledgeSegment; } + private VectorStore getVectorStoreById(AiKnowledgeDO knowledge) { + return modelService.getOrCreateVectorStore(knowledge.getEmbeddingModelId(), VECTOR_STORE_METADATA_TYPES); + } + + private VectorStore getVectorStoreById(Long knowledgeId) { + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(knowledgeId); + return getVectorStoreById(knowledge); + } + + private static List splitContentByToken(String content, Integer segmentMaxTokens) { + TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens); + return textSplitter.apply(Collections.singletonList(new Document(content))); + } + + private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) { + return TokenTextSplitter.builder() + .withChunkSize(segmentMaxTokens) + .withMinChunkSizeChars(Integer.MAX_VALUE) // 忽略字符的截断 + .withMinChunkLengthToEmbed(1) // 允许的最小有效分段长度 + .withMaxNumChunks(Integer.MAX_VALUE) + .withKeepSeparator(true) // 保留分隔符 + .build(); + } + + @Override + public List getKnowledgeSegmentProcessList(List documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return Collections.emptyList(); + } + return segmentMapper.selectProcessList(documentIds); + } + + @Override + public Long createKnowledgeSegment(AiKnowledgeSegmentSaveReqVO createReqVO) { + // 1.1 校验文档是否存在 + AiKnowledgeDocumentDO document = knowledgeDocumentService + .validateKnowledgeDocumentExists(createReqVO.getDocumentId()); + // 1.2 获取知识库信息 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(document.getKnowledgeId()); + // 1.3 校验 token 熟练 + Integer tokens = tokenCountEstimator.estimate(createReqVO.getContent()); + if (tokens > document.getSegmentMaxTokens()) { + throw exception(KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG, tokens, document.getSegmentMaxTokens()); + } + + // 2. 保存段落 + AiKnowledgeSegmentDO segment = BeanUtils.toBean(createReqVO, AiKnowledgeSegmentDO.class) + .setKnowledgeId(knowledge.getId()).setDocumentId(document.getId()) + .setContentLength(createReqVO.getContent().length()).setTokens(tokens) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY) + .setRetrievalCount(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); + segmentMapper.insert(segment); + + // 3. 向量化 + writeVectorStore(getVectorStoreById(knowledge), segment, new Document(segment.getContent())); + return segment.getId(); + } + + @Override + public AiKnowledgeSegmentDO getKnowledgeSegment(Long id) { + return segmentMapper.selectById(id); + } + + @Override + public List getKnowledgeSegmentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return segmentMapper.selectBatchIds(ids); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index 7060076a4..5336570d2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import org.springframework.ai.vectorstore.VectorStore; + +import java.util.List; /** * AI 知识库-基础信息 Service 接口 @@ -18,18 +18,24 @@ public interface AiKnowledgeService { * 创建知识库 * * @param createReqVO 创建信息 - * @param userId 用户编号 * @return 编号 */ - Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId); + Long createKnowledge(AiKnowledgeSaveReqVO createReqVO); /** * 更新知识库 * * @param updateReqVO 更新信息 - * @param userId 用户编号 */ - void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId); + void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO); + + /** + * 获得知识库 + * + * @param id 编号 + * @return 知识库 + */ + AiKnowledgeDO getKnowledge(Long id); /** * 校验知识库是否存在 @@ -41,18 +47,17 @@ public interface AiKnowledgeService { /** * 获得知识库分页 * - * @param userId 用户编号 * @param pageReqVO 分页查询 * @return 知识库分页 */ - PageResult getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO); + PageResult getKnowledgePage(AiKnowledgePageReqVO pageReqVO); /** - * 根据知识库编号获取向量存储实例 + * 获得指定状态的知识库列表 * - * @param id 知识库编号 - * @return 向量存储实例 + * @param status 状态 + * @return 知识库列表 */ - VectorStore getVectorStoreById(Long id); + List getKnowledgeSimpleListByStatus(Integer status); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 1a000c19d..59afd7d7b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -4,19 +4,19 @@ import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS; @@ -33,36 +33,43 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiKnowledgeMapper knowledgeMapper; @Resource - private AiChatModelService chatModelService; + private AiModelService modelService; @Resource - private AiApiKeyService apiKeyService; + private AiKnowledgeSegmentService knowledgeSegmentService; @Override - public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) { + public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { // 1. 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId()); + AiModelDO model = modelService.validateModel(createReqVO.getEmbeddingModelId()); // 2. 插入知识库 - AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) - .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); - knowledgeMapper.insert(knowledgeBase); - return knowledgeBase.getId(); + AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) + .setEmbeddingModel(model.getModel()); + knowledgeMapper.insert(knowledge); + return knowledge.getId(); } @Override - public void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId) { + public void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO) { // 1.1 校验知识库存在 - AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); - if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { - throw exception(KNOWLEDGE_NOT_EXISTS); - } + AiKnowledgeDO oldKnowledge = validateKnowledgeExists(updateReqVO.getId()); // 1.2 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId()); + AiModelDO model = modelService.validateModel(updateReqVO.getEmbeddingModelId()); // 2. 更新知识库 - AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class); - updateDO.setModel(model.getModel()); - knowledgeMapper.updateById(updateDO); + AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class) + .setEmbeddingModel(model.getModel()); + knowledgeMapper.updateById(updateObj); + + // 3. 如果模型变化,需要 reindex 所有的文档 + if (ObjUtil.notEqual(oldKnowledge.getEmbeddingModelId(), updateReqVO.getEmbeddingModelId())) { + knowledgeSegmentService.reindexByKnowledgeIdAsync(updateReqVO.getId()); + } + } + + @Override + public AiKnowledgeDO getKnowledge(Long id) { + return knowledgeMapper.selectById(id); } @Override @@ -75,16 +82,13 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } @Override - public PageResult getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) { - return knowledgeMapper.selectPage(userId, pageReqVO); + public PageResult getKnowledgePage(AiKnowledgePageReqVO pageReqVO) { + return knowledgeMapper.selectPage(pageReqVO); } @Override - public VectorStore getVectorStoreById(Long id) { - AiKnowledgeDO knowledge = validateKnowledgeExists(id); - AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); - // 创建或获取 VectorStore 对象 - return apiKeyService.getOrCreateVectorStore(model.getKeyId()); + public List getKnowledgeSimpleListByStatus(Integer status) { + return knowledgeMapper.selectListByStatus(status); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java new file mode 100644 index 000000000..9ff63b646 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.service.knowledge.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +import jakarta.validation.constraints.NotEmpty; + +/** + * AI 知识库段落搜索 Request BO + * + * @author 芋道源码 + */ +@Data +public class AiKnowledgeSegmentSearchReqBO { + + /** + * 知识库编号 + */ + @NotNull(message = "知识库编号不能为空") + private Long knowledgeId; + + /** + * 内容 + */ + @NotEmpty(message = "内容不能为空") + private String content; + + /** + * 最大返回数量 + */ + private Integer topK; + + /** + * 相似度阈值 + */ + private Double similarityThreshold; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java new file mode 100644 index 000000000..72eb84624 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.service.knowledge.bo; + +import lombok.Data; + +/** + * AI 知识库段落搜索 Response BO + * + * @author 芋道源码 + */ +@Data +public class AiKnowledgeSegmentSearchRespBO { + + /** + * 段落编号 + */ + private Long id; + /** + * 文档编号 + */ + private Long documentId; + /** + * 知识库编号 + */ + private Long knowledgeId; + + /** + * 内容 + */ + private String content; + /** + * 内容长度 + */ + private Integer contentLength; + + /** + * Token 数量 + */ + private Integer tokens; + + /** + * 相似度分数 + */ + private Double score; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index b34bd6348..0dc851c21 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -1,8 +1,9 @@ package cn.iocoder.yudao.module.ai.service.mindmap; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -12,14 +13,13 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.mindmap.AiMindMapMapper; import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; @@ -38,7 +38,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 思维导图 Service 实现类 @@ -50,9 +50,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_E public class AiMindMapServiceImpl implements AiMindMapService { @Resource - private AiApiKeyService apiKeyService; - @Resource - private AiChatModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @@ -65,17 +63,17 @@ public class AiMindMapServiceImpl implements AiMindMapService { AiChatRoleDO role = CollUtil.getFirst( chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName())); // 1.1 获取导图执行模型 - AiChatModelDO model = getModel(role); + AiModelDO model = getModel(role); // 1.2 获取角色设定消息 String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage()) ? role.getSystemMessage() : AiChatRoleEnum.AI_MIND_MAP_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + ChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 插入思维导图信息 - AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class, - mindMap -> mindMap.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); + AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class, mindMap -> mindMap.setUserId(userId) + .setPlatform(platform.getPlatform()).setModelId(model.getId()).setModel(model.getModel())); mindMapMapper.insert(mindMapDO); // 3.1 构建 Prompt,并进行调用 @@ -85,7 +83,7 @@ public class AiMindMapServiceImpl implements AiMindMapService { // 3.2 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 @@ -103,7 +101,7 @@ public class AiMindMapServiceImpl implements AiMindMapService { } - private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiModelDO model, String systemMessage) { // 1. 构建 message 列表 List chatMessages = buildMessages(generateReqVO, systemMessage); // 2. 构建 options 对象 @@ -123,15 +121,21 @@ public class AiMindMapServiceImpl implements AiMindMapService { return chatMessages; } - private AiChatModelDO getModel(AiChatRoleDO role) { - AiChatModelDO model = null; + private AiModelDO getModel(AiChatRoleDO role) { + AiModelDO model = null; if (role != null && role.getModelId() != null) { - model = chatModalService.getChatModel(role.getModelId()); + model = modalService.getModel(role.getModelId()); } if (model == null) { - model = chatModalService.getRequiredDefaultChatModel(); + model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); + } + // 校验模型存在、且合法 + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + if (ObjUtil.notEqual(model.getType(), AiModelTypeEnum.CHAT.getType())) { + throw exception(MODEL_USE_TYPE_ERROR); } - Assert.notNull(model, "[AI] 获取不到模型"); return model; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index f5f881349..44da80041 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -1,17 +1,10 @@ package cn.iocoder.yudao.module.ai.service.model; -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import jakarta.validation.Valid; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.vectorstore.VectorStore; import java.util.List; @@ -75,58 +68,13 @@ public interface AiApiKeyService { */ List getApiKeyList(); - // ========== 与 spring-ai 集成 ========== - /** - * 获得 ChatModel 对象 - * - * @param id 编号 - * @return ChatModel 对象 - */ - ChatModel getChatModel(Long id); - - /** - * 获得 ImageModel 对象 - * - * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择 + * 获得默认的 API 密钥 * * @param platform 平台 - * @return ImageModel 对象 + * @param status 状态 + * @return API 密钥 */ - ImageModel getImageModel(AiPlatformEnum platform); - - /** - * 获得 MidjourneyApi 对象 - * - * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择 - * - * @return MidjourneyApi 对象 - */ - MidjourneyApi getMidjourneyApi(); - - /** - * 获得 SunoApi 对象 - * - * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择 - * - * @return SunoApi 对象 - */ - SunoApi getSunoApi(); - - /** - * 获得 EmbeddingModel 对象 - * - * @param id 编号 - * @return EmbeddingModel 对象 - */ - EmbeddingModel getEmbeddingModel(Long id); - - /** - * 获得 VectorStore 对象 - * - * @param id 编号 - * @return VectorStore 对象 - */ - VectorStore getOrCreateVectorStore(Long id); + AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status); } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 50e1fbd7a..f0bac8a6d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -1,9 +1,5 @@ package cn.iocoder.yudao.module.ai.service.model; -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; -import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -12,17 +8,14 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; import jakarta.annotation.Resource; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_DISABLE; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS; /** * AI API 密钥 Service 实现类 @@ -36,9 +29,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @Resource private AiApiKeyMapper apiKeyMapper; - @Resource - private AiModelFactory modelFactory; - @Override public Long createApiKey(AiApiKeySaveReqVO createReqVO) { // 插入 @@ -97,57 +87,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { return apiKeyMapper.selectList(); } - // ========== 与 spring-ai 集成 ========== - @Override - public ChatModel getChatModel(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public ImageModel getImageModel(AiPlatformEnum platform) { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + public AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) { + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status); if (apiKey == null) { - throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName()); + throw exception(API_KEY_NOT_EXISTS); } - return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public MidjourneyApi getMidjourneyApi() { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( - AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); - if (apiKey == null) { - throw exception(API_KEY_MIDJOURNEY_NOT_FOUND); - } - return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public SunoApi getSunoApi() { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( - AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); - if (apiKey == null) { - throw exception(API_KEY_SUNO_NOT_FOUND); - } - return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public EmbeddingModel getEmbeddingModel(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public VectorStore getOrCreateVectorStore(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - // 创建或获取 VectorStore 对象 - return modelFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl()); + return apiKey; } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java deleted file mode 100644 index f83ac73c9..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java +++ /dev/null @@ -1,92 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.model; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import jakarta.validation.Valid; - -import java.util.Collection; -import java.util.List; - -import java.util.Set; - -/** - * AI 聊天模型 Service 接口 - * - * @author fansili - * @since 2024/4/24 19:42 - */ -public interface AiChatModelService { - - /** - * 创建聊天模型 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createChatModel(@Valid AiChatModelSaveReqVO createReqVO); - - /** - * 更新聊天模型 - * - * @param updateReqVO 更新信息 - */ - void updateChatModel(@Valid AiChatModelSaveReqVO updateReqVO); - - /** - * 删除聊天模型 - * - * @param id 编号 - */ - void deleteChatModel(Long id); - - /** - * 获得聊天模型 - * - * @param id 编号 - * @return 聊天模型 - */ - AiChatModelDO getChatModel(Long id); - - /** - * 获得默认的聊天模型 - * - * 如果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常 - * - * @return 聊天模型 - */ - AiChatModelDO getRequiredDefaultChatModel(); - - /** - * 获得聊天模型分页 - * - * @param pageReqVO 分页查询 - * @return 聊天模型分页 - */ - PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO); - - /** - * 校验聊天模型 - * - * @param id 编号 - * @return 聊天模型 - */ - AiChatModelDO validateChatModel(Long id); - - /** - * 获得聊天模型列表 - * - * @param status 状态 - * @return 聊天模型列表 - */ - List getChatModelListByStatus(Integer status); - - /** - * 获得聊天模型列表 - * - * @param ids 编号数组 - * @return 模型列表 - */ - List getChatModelList(Collection ids); -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java deleted file mode 100644 index 4b11602f5..000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java +++ /dev/null @@ -1,114 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.model; - -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatModelMapper; -import jakarta.annotation.Resource; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.util.Collection; -import java.util.List; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; - -/** - * AI 聊天模型 Service 实现类 - * - * @author fansili - */ -@Service -@Validated -public class AiChatModelServiceImpl implements AiChatModelService { - - @Resource - private AiApiKeyService apiKeyService; - - @Resource - private AiChatModelMapper chatModelMapper; - - @Override - public Long createChatModel(AiChatModelSaveReqVO createReqVO) { - // 1. 校验 - AiPlatformEnum.validatePlatform(createReqVO.getPlatform()); - apiKeyService.validateApiKey(createReqVO.getKeyId()); - - // 2. 插入 - AiChatModelDO chatModel = BeanUtils.toBean(createReqVO, AiChatModelDO.class); - chatModelMapper.insert(chatModel); - return chatModel.getId(); - } - - @Override - public void updateChatModel(AiChatModelSaveReqVO updateReqVO) { - // 1. 校验 - validateChatModelExists(updateReqVO.getId()); - AiPlatformEnum.validatePlatform(updateReqVO.getPlatform()); - apiKeyService.validateApiKey(updateReqVO.getKeyId()); - - // 2. 更新 - AiChatModelDO updateObj = BeanUtils.toBean(updateReqVO, AiChatModelDO.class); - chatModelMapper.updateById(updateObj); - } - - @Override - public void deleteChatModel(Long id) { - // 校验存在 - validateChatModelExists(id); - // 删除 - chatModelMapper.deleteById(id); - } - - private AiChatModelDO validateChatModelExists(Long id) { - AiChatModelDO model = chatModelMapper.selectById(id); - if (chatModelMapper.selectById(id) == null) { - throw exception(CHAT_MODEL_NOT_EXISTS); - } - return model; - } - - @Override - public AiChatModelDO getChatModel(Long id) { - return chatModelMapper.selectById(id); - } - - @Override - public AiChatModelDO getRequiredDefaultChatModel() { - AiChatModelDO model = chatModelMapper.selectFirstByStatus(CommonStatusEnum.ENABLE.getStatus()); - if (model == null) { - throw exception(CHAT_MODEL_DEFAULT_NOT_EXISTS); - } - return model; - } - - @Override - public PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO) { - return chatModelMapper.selectPage(pageReqVO); - } - - @Override - public AiChatModelDO validateChatModel(Long id) { - AiChatModelDO model = validateChatModelExists(id); - if (CommonStatusEnum.isDisable(model.getStatus())) { - throw exception(CHAT_MODEL_DISABLE); - } - return model; - } - - @Override - public List getChatModelListByStatus(Integer status) { - return chatModelMapper.selectList(status); - } - - @Override - public List getChatModelList(Collection ids) { - return chatModelMapper.selectBatchIds(ids); - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java index 81c8d259b..e7fecf6ac 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java @@ -32,7 +32,7 @@ public interface AiChatRoleService { * 创建【我的】聊天角色 * * @param createReqVO 创建信息 - * @param userId 用户编号 + * @param userId 用户编号 * @return 编号 */ Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId); @@ -48,7 +48,7 @@ public interface AiChatRoleService { * 创建【我的】聊天角色 * * @param updateReqVO 更新信息 - * @param userId 用户编号 + * @param userId 用户编号 */ void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId); @@ -62,7 +62,7 @@ public interface AiChatRoleService { /** * 删除【我的】聊天角色 * - * @param id 编号 + * @param id 编号 * @param userId 用户编号 */ void deleteChatRoleMy(Long id, Long userId); @@ -106,7 +106,7 @@ public interface AiChatRoleService { * 获得【我的】聊天角色分页 * * @param pageReqVO 分页查询 - * @param userId 用户编号 + * @param userId 用户编号 * @return 聊天角色分页 */ PageResult getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java index 2cf4d46d1..b0005c3af 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleS import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatRoleMapper; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -21,7 +22,8 @@ import java.util.List; 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.module.ai.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_ROLE_DISABLE; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_ROLE_NOT_EXISTS; /** * AI 聊天角色 Service 实现类 @@ -35,8 +37,19 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Resource private AiChatRoleMapper chatRoleMapper; + @Resource + private AiKnowledgeService knowledgeService; + @Resource + private AiToolService toolService; + @Override public Long createChatRole(AiChatRoleSaveReqVO createReqVO) { + // 校验文档 + validateDocuments(createReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(createReqVO.getToolIds()); + + // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class); chatRoleMapper.insert(chatRole); return chatRole.getId(); @@ -44,6 +57,12 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Override public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) { + // 校验文档 + validateDocuments(createReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(createReqVO.getToolIds()); + + // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setPublicStatus(false); chatRoleMapper.insert(chatRole); @@ -54,7 +73,12 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { public void updateChatRole(AiChatRoleSaveReqVO updateReqVO) { // 校验存在 validateChatRoleExists(updateReqVO.getId()); - // 更新 + // 校验文档 + validateDocuments(updateReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(updateReqVO.getToolIds()); + + // 更新角色 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); chatRoleMapper.updateById(updateObj); } @@ -66,12 +90,42 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) { throw exception(CHAT_ROLE_NOT_EXISTS); } + // 校验文档 + validateDocuments(updateReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(updateReqVO.getToolIds()); // 更新 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); chatRoleMapper.updateById(updateObj); } + /** + * 校验知识库是否存在 + * + * @param knowledgeIds 知识库编号列表 + */ + private void validateDocuments(List knowledgeIds) { + if (CollUtil.isEmpty(knowledgeIds)) { + return; + } + // 校验文档是否存在 + knowledgeIds.forEach(knowledgeService::validateKnowledgeExists); + } + + /** + * 校验工具是否存在 + * + * @param toolIds 工具编号列表 + */ + private void validateTools(List toolIds) { + if (CollUtil.isEmpty(toolIds)) { + return; + } + // 遍历校验每个工具是否存在 + toolIds.forEach(toolService::validateToolExists); + } + @Override public void deleteChatRole(Long id) { // 校验存在 @@ -134,7 +188,8 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Override public List getChatRoleCategoryList() { List list = chatRoleMapper.selectListGroupByCategory(CommonStatusEnum.ENABLE.getStatus()); - return convertList(list, AiChatRoleDO::getCategory, role -> role != null && StrUtil.isNotBlank(role.getCategory())); + return convertList(list, AiChatRoleDO::getCategory, + role -> role != null && StrUtil.isNotBlank(role.getCategory())); } @Override @@ -143,4 +198,3 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { } } - diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java new file mode 100644 index 000000000..127f72cc4 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import jakarta.validation.Valid; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.VectorStore; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; + +/** + * AI 模型 Service 接口 + * + * @author fansili + * @since 2024/4/24 19:42 + */ +public interface AiModelService { + + /** + * 创建模型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModel(@Valid AiModelSaveReqVO createReqVO); + + /** + * 更新模型 + * + * @param updateReqVO 更新信息 + */ + void updateModel(@Valid AiModelSaveReqVO updateReqVO); + + /** + * 删除模型 + * + * @param id 编号 + */ + void deleteModel(Long id); + + /** + * 获得模型 + * + * @param id 编号 + * @return 模型 + */ + AiModelDO getModel(Long id); + + /** + * 获得默认的模型 + * + * 如果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常 + * + * @return 模型 + */ + AiModelDO getRequiredDefaultModel(Integer type); + + /** + * 获得模型分页 + * + * @param pageReqVO 分页查询 + * @return 模型分页 + */ + PageResult getModelPage(AiModelPageReqVO pageReqVO); + + /** + * 校验模型是否可使用 + * + * @param id 编号 + * @return 模型 + */ + AiModelDO validateModel(Long id); + + /** + * 获得模型列表 + * + * @param status 状态 + * @param type 类型 + * @param platform 平台,允许空 + * @return 模型列表 + */ + List getModelListByStatusAndType(Integer status, Integer type, + @Nullable String platform); + + // ========== 与 Spring AI 集成 ========== + + /** + * 获得 ChatModel 对象 + * + * @param id 编号 + * @return ChatModel 对象 + */ + ChatModel getChatModel(Long id); + + /** + * 获得 ImageModel 对象 + * + * @param id 编号 + * @return ImageModel 对象 + */ + ImageModel getImageModel(Long id); + + /** + * 获得 MidjourneyApi 对象 + * + * @param id 编号 + * @return MidjourneyApi 对象 + */ + MidjourneyApi getMidjourneyApi(Long id); + + /** + * 获得 SunoApi 对象 + * + * @return SunoApi 对象 + */ + SunoApi getSunoApi(); + + /** + * 获得 VectorStore 对象 + * + * @param id 编号 + * @param metadataFields 元数据的定义 + * @return VectorStore 对象 + */ + VectorStore getOrCreateVectorStore(Long id, Map> metadataFields); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java new file mode 100644 index 000000000..b0e9e9717 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -0,0 +1,171 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatMapper; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; + +/** + * AI 模型 Service 实现类 + * + * @author fansili + */ +@Service +@Validated +public class AiModelServiceImpl implements AiModelService { + + @Resource + private AiApiKeyService apiKeyService; + + @Resource + private AiChatMapper modelMapper; + + @Resource + private AiModelFactory modelFactory; + + @Override + public Long createModel(AiModelSaveReqVO createReqVO) { + // 1. 校验 + AiPlatformEnum.validatePlatform(createReqVO.getPlatform()); + apiKeyService.validateApiKey(createReqVO.getKeyId()); + + // 2. 插入 + AiModelDO model = BeanUtils.toBean(createReqVO, AiModelDO.class); + modelMapper.insert(model); + return model.getId(); + } + + @Override + public void updateModel(AiModelSaveReqVO updateReqVO) { + // 1. 校验 + validateModelExists(updateReqVO.getId()); + AiPlatformEnum.validatePlatform(updateReqVO.getPlatform()); + apiKeyService.validateApiKey(updateReqVO.getKeyId()); + + // 2. 更新 + AiModelDO updateObj = BeanUtils.toBean(updateReqVO, AiModelDO.class); + modelMapper.updateById(updateObj); + } + + @Override + public void deleteModel(Long id) { + // 校验存在 + validateModelExists(id); + // 删除 + modelMapper.deleteById(id); + } + + private AiModelDO validateModelExists(Long id) { + AiModelDO model = modelMapper.selectById(id); + if (modelMapper.selectById(id) == null) { + throw exception(MODEL_NOT_EXISTS); + } + return model; + } + + @Override + public AiModelDO getModel(Long id) { + return modelMapper.selectById(id); + } + + @Override + public AiModelDO getRequiredDefaultModel(Integer type) { + AiModelDO model = modelMapper.selectFirstByStatus(type, CommonStatusEnum.ENABLE.getStatus()); + if (model == null) { + throw exception(MODEL_DEFAULT_NOT_EXISTS); + } + return model; + } + + @Override + public PageResult getModelPage(AiModelPageReqVO pageReqVO) { + return modelMapper.selectPage(pageReqVO); + } + + @Override + public AiModelDO validateModel(Long id) { + AiModelDO model = validateModelExists(id); + if (CommonStatusEnum.isDisable(model.getStatus())) { + throw exception(MODEL_DISABLE); + } + return model; + } + + @Override + public List getModelListByStatusAndType(Integer status, Integer type, String platform) { + return modelMapper.selectListByStatusAndType(status, type, platform); + } + + // ========== 与 Spring AI 集成 ========== + + @Override + public ChatModel getChatModel(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public ImageModel getImageModel(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public MidjourneyApi getMidjourneyApi(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public SunoApi getSunoApi() { + AiApiKeyDO apiKey = apiKeyService.getRequiredDefaultApiKey( + AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public VectorStore getOrCreateVectorStore(Long id, Map> metadataFields) { + // 获取模型 + 密钥 + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + + // 创建或获取 EmbeddingModel 对象 + EmbeddingModel embeddingModel = modelFactory.getOrCreateEmbeddingModel( + platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); + + // 创建或获取 VectorStore 对象 + return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java new file mode 100644 index 000000000..fb23224a8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +/** + * AI 工具 Service 接口 + * + * @author 芋道源码 + */ +public interface AiToolService { + + /** + * 创建工具 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTool(@Valid AiToolSaveReqVO createReqVO); + + /** + * 更新工具 + * + * @param updateReqVO 更新信息 + */ + void updateTool(@Valid AiToolSaveReqVO updateReqVO); + + /** + * 删除工具 + * + * @param id 编号 + */ + void deleteTool(Long id); + + /** + * 校验工具是否存在 + * + * @param id 编号 + */ + void validateToolExists(Long id); + + /** + * 获得工具 + * + * @param id 编号 + * @return 工具 + */ + AiToolDO getTool(Long id); + + /** + * 获得工具列表 + * + * @param ids 编号列表 + * @return 工具列表 + */ + List getToolList(Collection ids); + + /** + * 获得工具分页 + * + * @param pageReqVO 分页查询 + * @return 工具分页 + */ + PageResult getToolPage(AiToolPageReqVO pageReqVO); + + /** + * 获得工具列表 + * + * @param status 状态 + * @return 工具列表 + */ + List getToolListByStatus(Integer status); + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java new file mode 100644 index 000000000..59f8f74d1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NAME_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NOT_EXISTS; + +/** + * AI 工具 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AiToolServiceImpl implements AiToolService { + + @Resource + private AiToolMapper toolMapper; + + @Override + public Long createTool(AiToolSaveReqVO createReqVO) { + // 校验名称是否存在 + validateToolNameExists(createReqVO.getName()); + + // 插入 + AiToolDO tool = BeanUtils.toBean(createReqVO, AiToolDO.class); + toolMapper.insert(tool); + return tool.getId(); + } + + @Override + public void updateTool(AiToolSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateToolExists(updateReqVO.getId()); + // 1.2 校验名称是否存在 + validateToolNameExists(updateReqVO.getName()); + + // 2. 更新 + AiToolDO updateObj = BeanUtils.toBean(updateReqVO, AiToolDO.class); + toolMapper.updateById(updateObj); + } + + @Override + public void deleteTool(Long id) { + // 校验存在 + validateToolExists(id); + // 删除 + toolMapper.deleteById(id); + } + + @Override + public void validateToolExists(Long id) { + if (toolMapper.selectById(id) == null) { + throw exception(TOOL_NOT_EXISTS); + } + } + + private void validateToolNameExists(String name) { + try { + SpringUtil.getBean(name); + } catch (NoSuchBeanDefinitionException e) { + throw exception(TOOL_NAME_NOT_EXISTS, name); + } + } + + @Override + public AiToolDO getTool(Long id) { + return toolMapper.selectById(id); + } + + @Override + public List getToolList(Collection ids) { + return toolMapper.selectBatchIds(ids); + } + + @Override + public PageResult getToolPage(AiToolPageReqVO pageReqVO) { + return toolMapper.selectPage(pageReqVO); + } + + @Override + public List getToolListByStatus(Integer status) { + return toolMapper.selectListByStatus(status); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java new file mode 100644 index 000000000..787b2e772 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.ai.service.model.tool; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 工具:列出指定目录的文件列表 + * + * @author 芋道源码 + */ +@Component("directory_list") +public class DirectoryListToolFunction implements Function { + + @Data + @JsonClassDescription("列出指定目录的文件列表") + public static class Request { + + /** + * 目录路径 + */ + @JsonProperty(required = true, value = "path") + @JsonPropertyDescription("目录路径,例如说:/Users/yunai") + private String path; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 文件列表 + */ + private List files; + + @Data + public static class File { + + /** + * 是否为目录 + */ + private Boolean directory; + + /** + * 名称 + */ + private String name; + + /** + * 大小,仅对文件有效 + */ + private String size; + + /** + * 最后修改时间 + */ + private String lastModified; + + } + + } + + @Override + public Response apply(Request request) { + // 校验目录存在 + String path = StrUtil.blankToDefault(request.getPath(), "/"); + if (!FileUtil.exist(path) || !FileUtil.isDirectory(path)) { + return new Response(Collections.emptyList()); + } + // 列出目录 + File[] files = FileUtil.ls(path); + if (ArrayUtil.isEmpty(files)) { + return new Response(Collections.emptyList()); + } + return new Response(convertList(Arrays.asList(files), file -> + new Response.File().setDirectory(file.isDirectory()).setName(file.getName()) + .setLastModified(LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java new file mode 100644 index 000000000..99262fafa --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java @@ -0,0 +1,118 @@ +package cn.iocoder.yudao.module.ai.service.model.tool; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.function.Function; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; + +/** + * 工具:查询指定城市的天气信息 + * + * @author 芋道源码 + */ +@Component("weather_query") +public class WeatherQueryToolFunction + implements Function { + + private static final String[] WEATHER_CONDITIONS = { "晴朗", "多云", "阴天", "小雨", "大雨", "雷雨", "小雪", "大雪" }; + + @Data + @JsonClassDescription("查询指定城市的天气信息") + public static class Request { + + /** + * 城市名称 + */ + @JsonProperty(required = true, value = "city") + @JsonPropertyDescription("城市名称,例如:北京、上海、广州") + private String city; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 城市名称 + */ + private String city; + + /** + * 天气信息 + */ + private WeatherInfo weatherInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class WeatherInfo { + + /** + * 温度(摄氏度) + */ + private Integer temperature; + + /** + * 天气状况 + */ + private String condition; + + /** + * 湿度百分比 + */ + private Integer humidity; + + /** + * 风速(km/h) + */ + private Integer windSpeed; + + /** + * 查询时间 + */ + private String queryTime; + + } + + } + + @Override + public Response apply(Request request) { + // 检查城市名称是否为空 + if (StrUtil.isBlank(request.getCity())) { + return new Response("未知城市", null); + } + + // 获取天气数据 + String city = request.getCity(); + Response.WeatherInfo weatherInfo = generateMockWeatherInfo(); + return new Response(city, weatherInfo); + } + + /** + * 生成模拟的天气数据 + * 在实际应用中,应替换为真实 API 调用 + */ + private Response.WeatherInfo generateMockWeatherInfo() { + int temperature = RandomUtil.randomInt(-5, 30); + int humidity = RandomUtil.randomInt(1, 100); + int windSpeed = RandomUtil.randomInt(1, 30); + String condition = RandomUtil.randomEle(WEATHER_CONDITIONS); + return new Response.WeatherInfo(temperature, condition, humidity, windSpeed, + LocalDateTimeUtil.format(LocalDateTime.now(), NORM_DATETIME_PATTERN)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java index 3f10ec840..e4ff81a47 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java @@ -16,7 +16,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -41,7 +41,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXIS public class AiMusicServiceImpl implements AiMusicService { @Resource - private AiApiKeyService apiKeyService; + private AiModelService modelService; @Resource private AiMusicMapper musicMapper; @@ -53,7 +53,7 @@ public class AiMusicServiceImpl implements AiMusicService { @Transactional(rollbackFor = Exception.class) public List generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { // 1. 调用 Suno 生成音乐 - SunoApi sunoApi = apiKeyService.getSunoApi(); + SunoApi sunoApi = modelService.getSunoApi(); List musicDataList; if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { // 1.1 描述模式 @@ -88,7 +88,7 @@ public class AiMusicServiceImpl implements AiMusicService { log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); // GET 请求,为避免参数过长,分批次处理 - SunoApi sunoApi = apiKeyService.getSunoApi(); + SunoApi sunoApi = modelService.getSunoApi(); CollUtil.split(streamingTask, 36).forEach(chunkList -> { Map taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId); List musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index b8f22a2fa..787f8d204 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -1,8 +1,9 @@ package cn.iocoder.yudao.module.ai.service.write; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -11,17 +12,16 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper; import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.DictTypeConstants; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.system.api.dict.DictDataApi; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -42,7 +42,7 @@ import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 写作 Service 实现类 @@ -54,17 +54,15 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXIS public class AiWriteServiceImpl implements AiWriteService { @Resource - private AiApiKeyService apiKeyService; - @Resource - private AiChatModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @Resource - private DictDataApi dictDataApi; + private AiWriteMapper writeMapper; @Resource - private AiWriteMapper writeMapper; + private DictDataApi dictDataApi; @Override public Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { @@ -72,17 +70,17 @@ public class AiWriteServiceImpl implements AiWriteService { AiChatRoleDO writeRole = CollUtil.getFirst( chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_WRITE_ROLE.getName())); // 1.1 获取写作执行模型 - AiChatModelDO model = getModel(writeRole); + AiModelDO model = getModel(writeRole); // 1.2 获取角色设定消息 String systemMessage = Objects.nonNull(writeRole) && StrUtil.isNotBlank(writeRole.getSystemMessage()) ? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getKeyId()); // 2. 插入写作信息 - AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, - write -> write.setUserId(userId).setPlatform(platform.getPlatform()).setModel(model.getModel())); + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, write -> write.setUserId(userId) + .setPlatform(platform.getPlatform()).setModelId(model.getId()).setModel(model.getModel())); writeMapper.insert(writeDO); // 3.1 构建 Prompt,并进行调用 @@ -92,7 +90,7 @@ public class AiWriteServiceImpl implements AiWriteService { // 3.2 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 @@ -109,19 +107,25 @@ public class AiWriteServiceImpl implements AiWriteService { }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); } - private AiChatModelDO getModel(AiChatRoleDO writeRole) { - AiChatModelDO model = null; + private AiModelDO getModel(AiChatRoleDO writeRole) { + AiModelDO model = null; if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) { - model = chatModalService.getChatModel(writeRole.getModelId()); + model = modalService.getModel(writeRole.getModelId()); } if (model == null) { - model = chatModalService.getRequiredDefaultChatModel(); + model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); + } + // 校验模型存在、且合法 + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + if (ObjUtil.notEqual(model.getType(), AiModelTypeEnum.CHAT.getType())) { + throw exception(MODEL_USE_TYPE_ERROR); } - Assert.notNull(model, "[AI] 获取不到模型"); return model; } - private Prompt buildPrompt(AiWriteGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + private Prompt buildPrompt(AiWriteGenerateReqVO generateReqVO, AiModelDO model, String systemMessage) { // 1. 构建 message 列表 List chatMessages = buildMessages(generateReqVO, systemMessage); // 2. 构建 options 对象 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index a270f7655..f37f3709c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -14,47 +14,94 @@ ${project.artifactId} AI 大模型拓展,接入国内外大模型 - group.springframework.ai - 1.1.0 + 1.0.0-M6 - ${spring-ai.groupId} - spring-ai-zhipuai-spring-boot-starter - ${spring-ai.version} + cn.iocoder.boot + yudao-common + + - ${spring-ai.groupId} + org.springframework.ai spring-ai-openai-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-azure-openai-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-ollama-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} - - - - - - - - - - ${spring-ai.groupId} + + com.alibaba.cloud.ai + spring-ai-alibaba-starter + ${spring-ai.version}.1 + + + + org.springframework.ai + spring-ai-qianfan-spring-boot-starter + ${spring-ai.version} + + + + org.springframework.ai + spring-ai-zhipuai-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-minimax-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-moonshot-spring-boot-starter + ${spring-ai.version} + + + + + + org.springframework.ai + spring-ai-qdrant-store + ${spring-ai.version} + + + + + org.springframework.ai + spring-ai-redis-store + ${spring-ai.version} + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + org.springframework.ai + spring-ai-milvus-store + ${spring-ai.version} + + + + + org.springframework.ai spring-ai-tika-document-reader ${spring-ai.version} @@ -69,41 +116,6 @@ - - ${spring-ai.groupId} - spring-ai-redis-store - ${spring-ai.version} - - - - cn.iocoder.boot - yudao-spring-boot-starter-redis - - - - cn.iocoder.boot - yudao-common - - - - ${spring-ai.groupId} - spring-ai-qianfan-spring-boot-starter - ${spring-ai.version} - - - - - - com.alibaba - dashscope-sdk-java - 2.14.0 - - - org.slf4j - slf4j-simple - - - diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 0d2620b0c..ef3314a48 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -1,24 +1,33 @@ package cn.iocoder.yudao.framework.ai.config; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; +import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; +import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; +import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; +import org.springframework.ai.embedding.BatchingStrategy; +import org.springframework.ai.embedding.TokenCountBatchingStrategy; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; -import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Lazy; /** * 芋道 AI 自动配置 @@ -26,9 +35,12 @@ import org.springframework.context.annotation.Lazy; * @author fansili */ @AutoConfiguration -@EnableConfigurationProperties(YudaoAiProperties.class) +@EnableConfigurationProperties({ YudaoAiProperties.class, + QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 + RedisVectorStoreProperties.class, // 解析 Redis 配置 + MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 +}) @Slf4j -@Import(TongYiAutoConfiguration.class) public class YudaoAiAutoConfiguration { @Bean @@ -36,33 +48,148 @@ public class YudaoAiAutoConfiguration { return new AiModelFactoryImpl(); } - // ========== 各种 AI Client 创建 ========== @Bean @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topP(properties.getTopP()) + YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek(); + return buildDeepSeekChatModel(properties); + } + + public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(DeepSeekChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DeepSeekChatModel.BASE_URL) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) .build(); - return new DeepSeekChatModel(properties.getApiKey(), options); + return new DeepSeekChatModel(openAiChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") + public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); + return buildDouBaoChatClient(properties); + } + + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(DouBaoChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DouBaoChatModel.BASE_URL) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); + return new DouBaoChatModel(openAiChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") + public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); + return buildSiliconFlowChatClient(properties); + } + + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(SiliconFlowChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(SiliconFlowChatModel.BASE_URL) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); + return new SiliconFlowChatModel(openAiChatModel); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") + public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); + return buildHunYuanChatClient(properties); + } + + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(HunYuanChatModel.MODEL_DEFAULT); + } + // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL + if (StrUtil.isEmpty(properties.getBaseUrl())) { + properties.setBaseUrl( + StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL + : HunYuanChatModel.BASE_URL); + } + // 创建 OpenAiChatModel、HunYuanChatModel 对象 + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(properties.getBaseUrl()) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) + .build(); + return new HunYuanChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); - XingHuoChatOptions options = XingHuoChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topK(properties.getTopK()) + return buildXingHuoChatClient(properties); + } + + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(XingHuoChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(XingHuoChatModel.BASE_URL) + .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) + .toolCallingManager(getToolCallingManager()) .build(); - return new XingHuoChatModel(properties.getAppKey(), properties.getSecretKey(), options); + return new XingHuoChatModel(openAiChatModel); } @Bean @@ -78,44 +205,20 @@ public class YudaoAiAutoConfiguration { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } - // ========== rag 相关 ========== - // TODO @xin 免费版本 -// @Bean -// @Lazy // TODO 芋艿:临时注释,避免无法启动」 -// public TransformersEmbeddingModel transformersEmbeddingClient() { -// return new TransformersEmbeddingModel(MetadataMode.EMBED); -// } - - /** - * TODO @xin 默认版本先不弄,目前都先取对应的 EmbeddingModel - */ -// @Bean -// @Lazy // TODO 芋艿:临时注释,避免无法启动 -// public RedisVectorStore vectorStore(TransformersEmbeddingModel embeddingModel, RedisVectorStoreProperties properties, -// RedisProperties redisProperties) { -// var config = RedisVectorStore.RedisVectorStoreConfig.builder() -// .withIndexName(properties.getIndex()) -// .withPrefix(properties.getPrefix()) -// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) -// .build(); -// -// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, -// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), -// properties.isInitializeSchema()); -// redisVectorStore.afterPropertiesSet(); -// return redisVectorStore; -// } - @Bean - @Lazy // TODO 芋艿:临时注释,避免无法启动 - public TokenTextSplitter tokenTextSplitter() { - //TODO @xin 配置提取 - return new TokenTextSplitter(500, 100, 5, 10000, true); - } + // ========== RAG 相关 ========== @Bean - @Lazy // TODO 芋艿:临时注释,避免无法启动 public TokenCountEstimator tokenCountEstimator() { return new JTokkitTokenCountEstimator(); } + @Bean + public BatchingStrategy batchingStrategy() { + return new TokenCountBatchingStrategy(); + } + + private static ToolCallingManager getToolCallingManager() { + return SpringUtil.getBean(ToolCallingManager.class); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 82c74b0c6..296e0af8b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -16,11 +16,31 @@ public class YudaoAiProperties { /** * DeepSeek */ - private DeepSeekProperties deepSeek; + @SuppressWarnings("SpellCheckingInspection") + private DeepSeekProperties deepseek; + + /** + * 字节豆包 + */ + @SuppressWarnings("SpellCheckingInspection") + private DouBaoProperties doubao; + + /** + * 腾讯混元 + */ + @SuppressWarnings("SpellCheckingInspection") + private HunYuanProperties hunyuan; + + /** + * 硅基流动 + */ + @SuppressWarnings("SpellCheckingInspection") + private SiliconFlowProperties siliconflow; /** * 讯飞星火 */ + @SuppressWarnings("SpellCheckingInspection") private XingHuoProperties xinghuo; /** @@ -31,8 +51,62 @@ public class YudaoAiProperties { /** * Suno 音乐 */ + @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; + @Data + public static class DeepSeekProperties { + + private String enable; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class DouBaoProperties { + + private String enable; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class HunYuanProperties { + + private String enable; + private String baseUrl; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + + @Data + public static class SiliconFlowProperties { + + private String enable; + private String apiKey; + + private String model; + private Double temperature; + private Integer maxTokens; + private Double topP; + + } + @Data public static class XingHuoProperties { @@ -42,22 +116,9 @@ public class YudaoAiProperties { private String secretKey; private String model; - private Float temperature; + private Double temperature; private Integer maxTokens; - private Integer topK; - - } - - @Data - public static class DeepSeekProperties { - - private String enable; - private String apiKey; - - private String model; - private Float temperature; - private Integer maxTokens; - private Float topP; + private Double topP; } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java new file mode 100644 index 000000000..4f7a4e462 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.ai.core.enums; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * AI 模型类型的枚举 + * + * @author 芋道源码 + */ +@Getter +@RequiredArgsConstructor +public enum AiModelTypeEnum implements ArrayValuable { + + CHAT(1, "对话"), + IMAGE(2, "图片"), + VOICE(3, "语音"), + VIDEO(4, "视频"), + EMBEDDING(5, "向量"), + RERANK(6, "重排序"); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index 1922e9a2c..5a8a5c453 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -1,8 +1,11 @@ package cn.iocoder.yudao.framework.ai.core.enums; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; + /** * AI 模型平台 * @@ -10,7 +13,7 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum AiPlatformEnum { +public enum AiPlatformEnum implements ArrayValuable { // ========== 国内平台 ========== @@ -19,6 +22,11 @@ public enum AiPlatformEnum { DEEP_SEEK("DeepSeek", "DeepSeek"), // DeepSeek ZHI_PU("ZhiPu", "智谱"), // 智谱 AI XING_HUO("XingHuo", "星火"), // 讯飞 + DOU_BAO("DouBao", "豆包"), // 字节 + HUN_YUAN("HunYuan", "混元"), // 腾讯 + SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动 + MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 + MOONSHOT("Moonshot", "月之暗灭"), // KIMI // ========== 国外平台 ========== @@ -41,6 +49,8 @@ public enum AiPlatformEnum { */ private final String name; + public static final String[] ARRAYS = Arrays.stream(values()).map(AiPlatformEnum::getPlatform).toArray(String[]::new); + public static AiPlatformEnum validatePlatform(String platform) { for (AiPlatformEnum platformEnum : AiPlatformEnum.values()) { if (platformEnum.getPlatform().equals(platform)) { @@ -50,4 +60,9 @@ public enum AiPlatformEnum { throw new IllegalArgumentException("非法平台: " + platform); } + @Override + public String[] array() { + return ARRAYS; + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index 243c4ae4b..66ab41def 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -8,6 +8,8 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; import org.springframework.ai.vectorstore.VectorStore; +import java.util.Map; + /** * AI Model 模型工厂的接口类 * @@ -89,21 +91,23 @@ public interface AiModelFactory { * @param platform 平台 * @param apiKey API KEY * @param url API URL + * @param model 模型 * @return ChatModel 对象 */ - EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); + EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model); /** * 基于指定配置,获得 VectorStore 对象 - *

+ * * 如果不存在,则进行创建 * - * @param embeddingModel 嵌入模型 - * @param platform 平台 - * @param apiKey API KEY - * @param url API URL + * @param type 向量存储类型 + * @param embeddingModel 向量模型 + * @param metadataFields 元数据字段 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url); + VectorStore getOrCreateVectorStore(Class type, + EmbeddingModel embeddingModel, + Map> metadataFields); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 7acd24769..356715be2 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -1,74 +1,117 @@ package cn.iocoder.yudao.framework.ai.core.factory; +import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Singleton; import cn.hutool.core.lang.func.Func0; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; +import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; -import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.azure.ai.openai.OpenAIClient; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.azure.ai.openai.OpenAIClientBuilder; +import io.micrometer.observation.ObservationRegistry; +import io.milvus.client.MilvusServiceClient; +import io.qdrant.client.QdrantClient; +import io.qdrant.client.QdrantGrpcClient; +import lombok.SneakyThrows; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties; +import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration; +import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; -import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; -import org.springframework.ai.autoconfigure.qianfan.QianFanImageProperties; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration; +import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; +import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration; +import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; +import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration; +import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; +import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.document.MetadataMode; +import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.model.function.FunctionCallbackContext; +import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.MiniMaxChatOptions; +import org.springframework.ai.minimax.MiniMaxEmbeddingModel; +import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; +import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.moonshot.MoonshotChatModel; +import org.springframework.ai.moonshot.MoonshotChatOptions; +import org.springframework.ai.moonshot.api.MoonshotApi; import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiEmbeddingOptions; import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.ApiUtils; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.openai.api.common.OpenAiApiConstants; import org.springframework.ai.qianfan.QianFanChatModel; +import org.springframework.ai.qianfan.QianFanEmbeddingModel; +import org.springframework.ai.qianfan.QianFanEmbeddingOptions; import org.springframework.ai.qianfan.QianFanImageModel; import org.springframework.ai.qianfan.api.QianFanApi; import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.api.StabilityAiApi; -import org.springframework.ai.vectorstore.RedisVectorStore; +import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; +import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; +import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; +import org.springframework.ai.vectorstore.redis.RedisVectorStore; +import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.search.Schema; +import java.io.File; +import java.time.Duration; import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; /** * AI Model 模型工厂的实现类 @@ -81,7 +124,7 @@ public class AiModelFactoryImpl implements AiModelFactory { public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiChatModel(apiKey); @@ -89,8 +132,18 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildYiYanChatModel(apiKey); case DEEP_SEEK: return buildDeepSeekChatModel(apiKey); + case DOU_BAO: + return buildDouBaoChatModel(apiKey); + case HUN_YUAN: + return buildHunYuanChatModel(apiKey, url); + case SILICON_FLOW: + return buildSiliconFlowChatModel(apiKey); case ZHI_PU: return buildZhiPuChatModel(apiKey, url); + case MINI_MAX: + return buildMiniMaxChatModel(apiKey, url); + case MOONSHOT: + return buildMoonshotChatModel(apiKey, url); case XING_HUO: return buildXingHuoChatModel(apiKey); case OPENAI: @@ -107,16 +160,26 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ChatModel getDefaultChatModel(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiChatModel.class); + return SpringUtil.getBean(DashScopeChatModel.class); case YI_YAN: return SpringUtil.getBean(QianFanChatModel.class); case DEEP_SEEK: return SpringUtil.getBean(DeepSeekChatModel.class); + case DOU_BAO: + return SpringUtil.getBean(DouBaoChatModel.class); + case HUN_YUAN: + return SpringUtil.getBean(HunYuanChatModel.class); + case SILICON_FLOW: + return SpringUtil.getBean(SiliconFlowChatModel.class); case ZHI_PU: return SpringUtil.getBean(ZhiPuAiChatModel.class); + case MINI_MAX: + return SpringUtil.getBean(MiniMaxChatModel.class); + case MOONSHOT: + return SpringUtil.getBean(MoonshotChatModel.class); case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case OPENAI: @@ -132,10 +195,10 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ImageModel getDefaultImageModel(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiImagesModel.class); + return SpringUtil.getBean(DashScopeImageModel.class); case YI_YAN: return SpringUtil.getBean(QianFanImageModel.class); case ZHI_PU: @@ -151,7 +214,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiImagesModel(apiKey); @@ -170,9 +233,11 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, + url); return Singleton.get(cacheKey, (Func0) () -> { - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney(); + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) + .getMidjourney(); return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); }); } @@ -184,13 +249,25 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url); + @SuppressWarnings("EnhancedSwitchMigration") + public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) { + String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model); return Singleton.get(cacheKey, (Func0) () -> { - // TODO @xin 先测试一个 switch (platform) { case TONG_YI: - return buildTongYiEmbeddingModel(apiKey); + return buildTongYiEmbeddingModel(apiKey, model); + case YI_YAN: + return buildYiYanEmbeddingModel(apiKey, model); + case ZHI_PU: + return buildZhiPuEmbeddingModel(apiKey, url, model); + case MINI_MAX: + return buildMiniMaxEmbeddingModel(apiKey, url, model); + case OPENAI: + return buildOpenAiEmbeddingModel(apiKey, url, model); + case AZURE_OPENAI: + return buildAzureOpenAiEmbeddingModel(apiKey, url, model); + case OLLAMA: + return buildOllamaEmbeddingModel(url, model); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -198,21 +275,24 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); + public VectorStore getOrCreateVectorStore(Class type, + EmbeddingModel embeddingModel, + Map> metadataFields) { + String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type); return Singleton.get(cacheKey, (Func0) () -> { - String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); - var config = RedisVectorStore.RedisVectorStoreConfig.builder() - .withIndexName(cacheKey) - .withPrefix(prefix) - .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) - .build(); - RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, - new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), - true); - redisVectorStore.afterPropertiesSet(); - return redisVectorStore; + if (type == SimpleVectorStore.class) { + return buildSimpleVectorStore(embeddingModel); + } + if (type == QdrantVectorStore.class) { + return buildQdrantVectorStore(embeddingModel); + } + if (type == RedisVectorStore.class) { + return buildRedisVectorStore(embeddingModel, metadataFields); + } + if (type == MilvusVectorStore.class) { + return buildMilvusVectorStore(embeddingModel); + } + throw new IllegalArgumentException(StrUtil.format("未知类型({})", type)); }); } @@ -226,29 +306,25 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 */ - private static TongYiChatModel buildTongYiChatModel(String key) { - com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); - TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); - // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 - // TODO @芋艿:貌似阿里云不是增量返回的 - // 该 issue 进行跟进中 https://github.com/alibaba/spring-cloud-alibaba/issues/3790 - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); - } - - private static TongYiImagesModel buildTongYiImagesModel(String key) { - ImageSynthesis imageSynthesis = SpringUtil.getBean(ImageSynthesis.class); - TongYiImagesProperties imagesOptions = SpringUtil.getBean(TongYiImagesProperties.class); - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiImagesClient(imageSynthesis, imagesOptions, connectionProperties); + private static DashScopeChatModel buildTongYiChatModel(String key) { + DashScopeApi dashScopeApi = new DashScopeApi(key); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) + .withTemperature(0.7).build(); + return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 + */ + private static DashScopeImageModel buildTongYiImagesModel(String key) { + DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); + return new DashScopeImageModel(dashScopeImageApi); + } + + /** + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { List keys = StrUtil.split(key, '|'); @@ -260,7 +336,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanImageModel(QianFanConnectionProperties, QianFanImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { List keys = StrUtil.split(key, '|'); @@ -275,47 +351,98 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - return new DeepSeekChatModel(apiKey); + YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildDeepSeekChatModel(properties); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link YudaoAiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} + */ + private ChatModel buildDouBaoChatModel(String apiKey) { + YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildDouBaoChatClient(properties); + } + + /** + * 可参考 {@link YudaoAiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} + */ + private ChatModel buildHunYuanChatModel(String apiKey, String url) { + YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() + .setBaseUrl(url).setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildHunYuanChatClient(properties); + } + + /** + * 可参考 {@link YudaoAiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} + */ + private ChatModel buildSiliconFlowChatModel(String apiKey) { + YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildSiliconFlowChatClient(properties); + } + + /** + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { - url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); - ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); - return new ZhiPuAiChatModel(zhiPuAiApi); + ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) + : new ZhiPuAiApi(url, apiKey); + ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { - url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); - ZhiPuAiImageApi zhiPuAiApi = new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); + ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) + : new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); return new ZhiPuAiImageModel(zhiPuAiApi); } + /** + * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法 + */ + private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { + MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) + : new MiniMaxApi(url, apiKey); + MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + } + + /** + * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 + */ + private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { + MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) + : new MoonshotApi(url, apiKey); + MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); + return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + } + /** * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} */ private static XingHuoChatModel buildXingHuoChatModel(String key) { List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); - String appKey = keys.get(1); - String secretKey = keys.get(2); - return new XingHuoChatModel(appKey, secretKey); + Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); + YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() + .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); + return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties); } /** - * 可参考 {@link OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 */ private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); - return new OpenAiChatModel(openAiApi); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); + return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); } + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** * 可参考 {@link AzureOpenAiAutoConfiguration} */ @@ -325,27 +452,28 @@ public class AiModelFactoryImpl implements AiModelFactory { AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); connectionProperties.setApiKey(apiKey); connectionProperties.setEndpoint(url); - OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties); + OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null); + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, + getToolCallingManager(), null, null); } /** - * 可参考 {@link OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build(); return new OpenAiImageModel(openAiApi); } /** - * 可参考 {@link OllamaAutoConfiguration} + * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { OllamaApi ollamaApi = new OllamaApi(url); - return new OllamaChatModel(ollamaApi); + return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); } private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { @@ -357,12 +485,234 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== /** - * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ - private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); + private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { + DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); + return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); + } + + /** + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 + */ + private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { + ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) + : new ZhiPuAiApi(url, apiKey); + ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build(); + return new ZhiPuAiEmbeddingModel(zhiPuAiApi, MetadataMode.EMBED, zhiPuAiEmbeddingOptions); + } + + /** + * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法 + */ + private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { + MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) + : new MiniMaxApi(url, apiKey); + MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build(); + return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions); + } + + /** + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 + */ + private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); + String appKey = keys.get(0); + String secretKey = keys.get(1); + QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); + QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build(); + return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions); + } + + private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { + OllamaApi ollamaApi = new OllamaApi(url); + OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); + return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); + } + + /** + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法 + */ + private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); + OpenAiEmbeddingOptions openAiEmbeddingProperties = OpenAiEmbeddingOptions.builder().model(model).build(); + return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); + } + + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 + /** + * 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 + */ + private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { + AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); + // 创建 OpenAIClient 对象 + AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); connectionProperties.setApiKey(apiKey); - return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties); + connectionProperties.setEndpoint(url); + OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); + // 获取 AzureOpenAiChatProperties 对象 + AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); + return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties, + null, null); + } + + // ========== 各种创建 VectorStore 的方法 ========== + + /** + * 注意:仅适合本地测试使用,生产建议还是使用 Qdrant、Milvus 等 + */ + @SneakyThrows + @SuppressWarnings("ResultOfMethodCallIgnored") + private SimpleVectorStore buildSimpleVectorStore(EmbeddingModel embeddingModel) { + SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build(); + // 启动加载 + File file = new File(StrUtil.format("{}/vector_store/simple_{}.json", + FileUtil.getUserHomePath(), embeddingModel.getClass().getSimpleName())); + if (!file.exists()) { + FileUtil.mkParentDirs(file); + file.createNewFile(); + } else if (file.length() > 0) { + vectorStore.load(file); + } + // 定时持久化,每分钟一次 + Timer timer = new Timer("SimpleVectorStoreTimer-" + file.getAbsolutePath()); + timer.scheduleAtFixedRate(new TimerTask() { + + @Override + public void run() { + vectorStore.save(file); + } + + }, Duration.ofMinutes(1).toMillis(), Duration.ofMinutes(1).toMillis()); + // 关闭时,进行持久化 + RuntimeUtil.addShutdownHook(() -> vectorStore.save(file)); + return vectorStore; + } + + /** + * 参考 {@link QdrantVectorStoreAutoConfiguration} 的 vectorStore 方法 + */ + @SneakyThrows + private QdrantVectorStore buildQdrantVectorStore(EmbeddingModel embeddingModel) { + QdrantVectorStoreAutoConfiguration configuration = new QdrantVectorStoreAutoConfiguration(); + QdrantVectorStoreProperties properties = SpringUtil.getBean(QdrantVectorStoreProperties.class); + // 参考 QdrantVectorStoreAutoConfiguration 实现,创建 QdrantClient 对象 + QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder( + properties.getHost(), properties.getPort(), properties.isUseTls()); + if (StrUtil.isNotEmpty(properties.getApiKey())) { + grpcClientBuilder.withApiKey(properties.getApiKey()); + } + QdrantClient qdrantClient = new QdrantClient(grpcClientBuilder.build()); + // 创建 QdrantVectorStore 对象 + QdrantVectorStore vectorStore = configuration.vectorStore(embeddingModel, properties, qdrantClient, + getObservationRegistry(), getCustomObservationConvention(), getBatchingStrategy()); + // 初始化索引 + vectorStore.afterPropertiesSet(); + return vectorStore; + } + + /** + * 参考 {@link RedisVectorStoreAutoConfiguration} 的 vectorStore 方法 + */ + private RedisVectorStore buildRedisVectorStore(EmbeddingModel embeddingModel, + Map> metadataFields) { + // 创建 JedisPooled 对象 + RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); + JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); + // 创建 RedisVectorStoreProperties 对象 + RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); + RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); + RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) + .indexName(properties.getIndex()).prefix(properties.getPrefix()) + .initializeSchema(properties.isInitializeSchema()) + .metadataFields(convertList(metadataFields.entrySet(), entry -> { + String fieldName = entry.getKey(); + Class fieldType = entry.getValue(); + if (Number.class.isAssignableFrom(fieldType)) { + return RedisVectorStore.MetadataField.numeric(fieldName); + } + if (Boolean.class.isAssignableFrom(fieldType)) { + return RedisVectorStore.MetadataField.tag(fieldName); + } + return RedisVectorStore.MetadataField.text(fieldName); + })) + .observationRegistry(getObservationRegistry().getObject()) + .customObservationConvention(getCustomObservationConvention().getObject()) + .batchingStrategy(getBatchingStrategy()) + .build(); + // 初始化索引 + redisVectorStore.afterPropertiesSet(); + return redisVectorStore; + } + + /** + * 参考 {@link MilvusVectorStoreAutoConfiguration} 的 vectorStore 方法 + */ + @SneakyThrows + private MilvusVectorStore buildMilvusVectorStore(EmbeddingModel embeddingModel) { + MilvusVectorStoreAutoConfiguration configuration = new MilvusVectorStoreAutoConfiguration(); + // 获取配置属性 + MilvusVectorStoreProperties serverProperties = SpringUtil.getBean(MilvusVectorStoreProperties.class); + MilvusServiceClientProperties clientProperties = SpringUtil.getBean(MilvusServiceClientProperties.class); + + // 创建 MilvusServiceClient 对象 + MilvusServiceClient milvusClient = configuration.milvusClient(serverProperties, clientProperties, + new MilvusServiceClientConnectionDetails() { + + @Override + public String getHost() { + return clientProperties.getHost(); + } + + @Override + public int getPort() { + return clientProperties.getPort(); + } + + } + ); + // 创建 MilvusVectorStore 对象 + MilvusVectorStore vectorStore = configuration.vectorStore(milvusClient, embeddingModel, serverProperties, + getBatchingStrategy(), getObservationRegistry(), getCustomObservationConvention()); + + // 初始化索引 + vectorStore.afterPropertiesSet(); + return vectorStore; + } + + private static ObjectProvider getObservationRegistry() { + return new ObjectProvider<>() { + + @Override + public ObservationRegistry getObject() throws BeansException { + return SpringUtil.getBean(ObservationRegistry.class); + } + + }; + } + + private static ObjectProvider getCustomObservationConvention() { + return new ObjectProvider<>() { + @Override + public VectorStoreObservationConvention getObject() throws BeansException { + return new DefaultVectorStoreObservationConvention(); + } + }; + } + + private static BatchingStrategy getBatchingStrategy() { + return SpringUtil.getBean(BatchingStrategy.class); + } + + private static ToolCallingManager getToolCallingManager() { + return SpringUtil.getBean(ToolCallingManager.class); + } + + private static FunctionCallbackResolver getFunctionCallbackResolver() { + return SpringUtil.getBean(FunctionCallbackResolver.class); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java index e3097b83a..a136b5a2b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java @@ -1,166 +1,45 @@ package cn.iocoder.yudao.framework.ai.core.model.deepseek; -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.lang.Assert; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.retry.support.RetryTemplate; +import org.springframework.ai.openai.OpenAiChatModel; import reactor.core.publisher.Flux; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT; - /** * DeepSeek {@link ChatModel} 实现类 * * @author fansili */ @Slf4j +@RequiredArgsConstructor public class DeepSeekChatModel implements ChatModel { - private static final String BASE_URL = "https://api.deepseek.com"; + public static final String BASE_URL = "https://api.deepseek.com"; - private final DeepSeekChatOptions defaultOptions; - private final RetryTemplate retryTemplate; + public static final String MODEL_DEFAULT = "deepseek-chat"; /** - * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 - * - * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + * 兼容 OpenAI 接口,进行复用 */ - private final OpenAiApi openAiApi; - - public DeepSeekChatModel(String apiKey) { - this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); - } - - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) { - this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); - } - - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) { - Assert.notEmpty(apiKey, "apiKey 不能为空"); - Assert.notNull(options, "options 不能为空"); - Assert.notNull(retryTemplate, "retryTemplate 不能为空"); - this.openAiApi = new OpenAiApi(BASE_URL, apiKey); - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - } + private final OpenAiChatModel openAiChatModel; @Override public ChatResponse call(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { - // 1.1 发起调用 - ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); - // 1.2 校验结果 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); - if (chatCompletion == null) { - log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - List choices = chatCompletion.choices(); - if (choices == null) { - log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - - // 2. 转换 ChatResponse 返回 - List generations = choices.stream().map(choice -> { - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); - if (choice.finishReason() != null) { - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations, - OpenAiChatResponseMetadata.from(completionEntity.getBody())); - }); - } - - private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { - Map map = new HashMap<>(); - OpenAiApi.ChatCompletionMessage message = choice.message(); - if (message.role() != null) { - map.put("role", message.role().name()); - } - if (choice.finishReason() != null) { - map.put("finishReason", choice.finishReason().name()); - } - map.put("id", id); - return map; + return openAiChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); - return this.retryTemplate.execute(ctx -> { - // 1. 发起调用 - Flux response = this.openAiApi.chatCompletionStream(request); - return response.map(chatCompletion -> { - String id = chatCompletion.id(); - // 2. 转换 ChatResponse 返回 - List generations = chatCompletion.choices().stream().map(choice -> { - String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); - String role = (choice.delta().role() != null ? choice.delta().role().name() : ""); - if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) { - // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况 - role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name(); - } - Generation generation = new Generation(choice.delta().content(), - Map.of("id", id, "role", role, "finishReason", finish)); - if (choice.finishReason() != null) { - generation = generation.withGenerationMetadata( - ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations); - }); - }); - } - - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - // 1. 构建 ChatCompletionMessage 对象 - List chatCompletionMessages = prompt.getInstructions().stream().map(m -> - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); - - // 2.1 补充 prompt 内置的 options - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, OpenAiChatOptions.class); - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); - } else { - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " - + prompt.getOptions().getClass().getSimpleName()); - } - } - // 2.2 补充默认 options - if (this.defaultOptions != null) { - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); - } - return request; + return openAiChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return DeepSeekChatOptions.fromOptions(defaultOptions); + return openAiChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java deleted file mode 100644 index e07e3f086..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.deepseek; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.chat.prompt.ChatOptions; - -/** - * DeepSeek {@link ChatOptions} 实现类 - * - * 参考文档:快速开始 - * - * @author fansili - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class DeepSeekChatOptions implements ChatOptions { - - public static final String MODEL_DEFAULT = "deepseek-chat"; - - /** - * 模型 - */ - private String model; - /** - * 温度 - */ - private Float temperature; - /** - * 最大 Token - */ - private Integer maxTokens; - /** - * topP - */ - private Float topP; - - @Override - public Integer getTopK() { - return null; - } - - public static DeepSeekChatOptions fromOptions(DeepSeekChatOptions fromOptions) { - return DeepSeekChatOptions.builder() - .model(fromOptions.getModel()) - .temperature(fromOptions.getTemperature()) - .maxTokens(fromOptions.getMaxTokens()) - .topP(fromOptions.getTopP()) - .build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java new file mode 100644 index 000000000..b6b17effe --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.ai.core.model.doubao; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * 字节豆包 {@link ChatModel} 实现类 + * + * @author fansili + */ +@Slf4j +@RequiredArgsConstructor +public class DouBaoChatModel implements ChatModel { + + public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; + + public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java new file mode 100644 index 000000000..f6f598d0a --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.ai.core.model.hunyuan; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * 腾云混元 {@link ChatModel} 实现类 + * + * 1. 混元大模型:基于 知识引擎原子能力 实现 + * 2. 知识引擎原子能力:基于 知识引擎原子能力 实现 + * + * @author fansili + */ +@Slf4j +@RequiredArgsConstructor +public class HunYuanChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; + + public static final String MODEL_DEFAULT = "hunyuan-turbo"; + + public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com"; + + public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java index 55091c78d..fe784d86b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java @@ -8,9 +8,9 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.openai.api.ApiUtils; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -50,7 +50,10 @@ public class MidjourneyApi { public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) + .defaultHeaders(httpHeaders -> { + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + httpHeaders.setBearerAuth(apiKey); + }) .build(); this.notifyUrl = notifyUrl; } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java new file mode 100644 index 000000000..cada37987 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.framework.ai.core.model.siliconflow; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * 硅基流动 {@link ChatModel} 实现类 + * + * 1. API 文档:API 文档 + * + * @author fansili + */ +@Slf4j +@RequiredArgsConstructor +public class SiliconFlowChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.siliconflow.cn"; + + public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java index 501d916db..330d102a0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java @@ -1,163 +1,45 @@ package cn.iocoder.yudao.framework.ai.core.model.xinghuo; -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.lang.Assert; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.retry.support.RetryTemplate; +import org.springframework.ai.openai.OpenAiChatModel; import reactor.core.publisher.Flux; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT; - /** * 讯飞星火 {@link ChatModel} 实现类 * * @author fansili */ @Slf4j +@RequiredArgsConstructor public class XingHuoChatModel implements ChatModel { - private static final String BASE_URL = "https://spark-api-open.xf-yun.com"; + public static final String BASE_URL = "https://spark-api-open.xf-yun.com"; - private final XingHuoChatOptions defaultOptions; - private final RetryTemplate retryTemplate; + public static final String MODEL_DEFAULT = "generalv3.5"; /** - * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 - * - * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + * 兼容 OpenAI 接口,进行复用 */ - private final OpenAiApi openAiApi; - - public XingHuoChatModel(String apiKey, String secretKey) { - this(apiKey, secretKey, - XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); - } - - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options) { - this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); - } - - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) { - Assert.notEmpty(apiKey, "apiKey 不能为空"); - Assert.notEmpty(secretKey, "secretKey 不能为空"); - Assert.notNull(options, "options 不能为空"); - Assert.notNull(retryTemplate, "retryTemplate 不能为空"); - this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey); - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - } + private final OpenAiChatModel openAiChatModel; @Override public ChatResponse call(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { - // 1.1 发起调用 - ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); - // 1.2 校验结果 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); - if (chatCompletion == null) { - log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - List choices = chatCompletion.choices(); - if (choices == null) { - log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - - // 2. 转换 ChatResponse 返回 - List generations = choices.stream().map(choice -> { - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); - if (choice.finishReason() != null) { - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations, - OpenAiChatResponseMetadata.from(completionEntity.getBody())); - }); - } - - private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { - Map map = new HashMap<>(); - OpenAiApi.ChatCompletionMessage message = choice.message(); - if (message.role() != null) { - map.put("role", message.role().name()); - } - if (choice.finishReason() != null) { - map.put("finishReason", choice.finishReason().name()); - } - map.put("id", id); - return map; + return openAiChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); - return this.retryTemplate.execute(ctx -> { - // 1. 发起调用 - Flux response = this.openAiApi.chatCompletionStream(request); - return response.map(chatCompletion -> { - String id = chatCompletion.id(); - // 2. 转换 ChatResponse 返回 - List generations = chatCompletion.choices().stream().map(choice -> { - String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); - Generation generation = new Generation(choice.delta().content(), - Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish)); - if (choice.finishReason() != null) { - generation = generation.withGenerationMetadata( - ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations); - }); - }); - } - - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - // 1. 构建 ChatCompletionMessage 对象 - List chatCompletionMessages = prompt.getInstructions().stream().map(m -> - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); - - // 2.1 补充 prompt 内置的 options - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, OpenAiChatOptions.class); - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); - } else { - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " - + prompt.getOptions().getClass().getSimpleName()); - } - } - // 2.2 补充默认 options - if (this.defaultOptions != null) { - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); - } - return request; + return openAiChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return XingHuoChatOptions.fromOptions(defaultOptions); + return openAiChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java deleted file mode 100644 index e3287b613..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.chat.prompt.ChatOptions; - -/** - * 讯飞星火 {@link ChatOptions} 实现类 - * - * 参考文档:HTTP 调用 - * - * @author fansili - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class XingHuoChatOptions implements ChatOptions { - - public static final String MODEL_DEFAULT = "generalv3.5"; - - /** - * 模型 - */ - private String model; - /** - * 温度 - */ - private Float temperature; - /** - * 最大 Token - */ - private Integer maxTokens; - /** - * K 个候选 - */ - private Integer topK; - - @Override - public Float getTopP() { - return null; - } - - public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) { - return XingHuoChatOptions.builder() - .model(fromOptions.getModel()) - .temperature(fromOptions.getTemperature()) - .maxTokens(fromOptions.getMaxTokens()) - .topK(fromOptions.getTopK()) - .build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index e18f10015..5f307cd64 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -1,18 +1,22 @@ package cn.iocoder.yudao.framework.ai.core.util; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.minimax.MiniMaxChatOptions; +import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import java.util.Collections; +import java.util.Set; + /** * Spring AI 工具类 * @@ -21,26 +25,43 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { - Float temperatureF = temperature != null ? temperature.floatValue() : null; - //noinspection EnhancedSwitchMigration + return buildChatOptions(platform, model, temperature, maxTokens, null); + } + + public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, + Set toolNames) { + toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) + .withFunctions(toolNames).build(); case YI_YAN: - return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case DEEP_SEEK: - return DeepSeekChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: - return ZhiPuAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case XING_HUO: - return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); + case MINI_MAX: + return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); + case MOONSHOT: + return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case OPENAI: - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + case DEEP_SEEK: // 复用 OpenAI 客户端 + case DOU_BAO: // 复用 OpenAI 客户端 + case HUN_YUAN: // 复用 OpenAI 客户端 + case XING_HUO: // 复用 OpenAI 客户端 + case SILICON_FLOW: // 复用 OpenAI 客户端 + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .toolNames(toolNames).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! - return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) + .toolNames(toolNames).build(); case OLLAMA: - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) + .toolNames(toolNames).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -56,8 +77,8 @@ public class AiUtils { if (MessageType.SYSTEM.getValue().equals(type)) { return new SystemMessage(content); } - if (MessageType.FUNCTION.getValue().equals(type)) { - return new FunctionMessage(content); + if (MessageType.TOOL.getValue().equals(type)) { + throw new UnsupportedOperationException("暂不支持 tool 消息:" + content); } throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java index b02d1b3ce..e4655cd0b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java @@ -4,7 +4,10 @@ * models 包路径: * 1. xinghuo 包:【讯飞】星火,自己实现 * 2. deepseek 包:【深度求索】DeepSeek,自己实现 - * 3. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现 - * 4. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现 + * 3. doubao 包:【字节豆包】DouBao,自己实现 + * 4. hunyuan 包:【腾讯混元】HunYuan,自己实现 + * 5. siliconflow 包:【硅基硅流】SiliconFlow,自己实现 + * 6. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现 + * 7. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现 */ package cn.iocoder.yudao.framework.ai; \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java deleted file mode 100644 index 1add717c3..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi; - -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechModel; -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechProperties; -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionModel; -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionProperties; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; -import com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingModel; -import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingProperties; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.audio.asr.transcription.Transcription; -import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.utils.ApiKey; -import com.alibaba.dashscope.utils.Constants; -import org.springframework.ai.model.function.FunctionCallbackContext; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Scope; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@AutoConfiguration -@ConditionalOnClass({ - MessageManager.class, - TongYiChatModel.class, - TongYiImagesModel.class, - TongYiAudioSpeechModel.class, - TongYiTextEmbeddingModel.class, - TongYiAudioTranscriptionModel.class -}) -@EnableConfigurationProperties({ - TongYiChatProperties.class, - TongYiImagesProperties.class, - TongYiAudioSpeechProperties.class, - TongYiConnectionProperties.class, - TongYiTextEmbeddingProperties.class, - TongYiAudioTranscriptionProperties.class -}) -public class TongYiAutoConfiguration { - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public Generation generation() { - - return new Generation(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public MessageManager msgManager() { - - return new MessageManager(10); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public ImageSynthesis imageSynthesis() { - - return new ImageSynthesis(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer() { - - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription() { - - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - public TextEmbedding textEmbedding() { - - return new TextEmbedding(); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) { - - FunctionCallbackContext manager = new FunctionCallbackContext(); - manager.setApplicationContext(context); - - return manager; - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiChatProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiChatModel tongYiChatClient(Generation generation, - TongYiChatProperties chatOptions, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiChatModel(generation, chatOptions.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiImagesProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiImagesModel tongYiImagesClient( - ImageSynthesis imageSynthesis, - TongYiImagesProperties imagesOptions, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiImagesModel(imageSynthesis, imagesOptions.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiAudioSpeechProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiAudioSpeechModel tongYiAudioSpeechClient( - SpeechSynthesizer speechSynthesizer, - TongYiAudioSpeechProperties speechProperties, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiAudioSpeechModel(speechSynthesizer, speechProperties.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiAudioTranscriptionProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiAudioTranscriptionModel tongYiAudioTranscriptionClient( - Transcription transcription, - TongYiAudioTranscriptionProperties transcriptionProperties, - TongYiConnectionProperties connectionProperties) { - - settingApiKey(connectionProperties); - - return new TongYiAudioTranscriptionModel( - transcriptionProperties.getOptions(), - transcription - ); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiTextEmbeddingProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiTextEmbeddingModel tongYiTextEmbeddingClient( - TextEmbedding textEmbedding, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - return new TongYiTextEmbeddingModel(textEmbedding); - } - - /** - * Setting the API key. - * @param connectionProperties {@link TongYiConnectionProperties} - */ - private void settingApiKey(TongYiConnectionProperties connectionProperties) { - - String apiKey; - - try { - // It is recommended to set the key by defining the api-key in an environment variable. - var envKey = System.getenv(TongYiConstants.SCA_AI_TONGYI_API_KEY); - if (Objects.nonNull(envKey)) { - Constants.apiKey = envKey; - return; - } - if (Objects.nonNull(connectionProperties.getApiKey())) { - apiKey = connectionProperties.getApiKey(); - } - else { - apiKey = ApiKey.getApiKey(null); - } - - Constants.apiKey = apiKey; - } - catch (NoApiKeyException e) { - - throw new TongYiException(e.getMessage()); - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java deleted file mode 100644 index 74141bd22..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * Spring Cloud Alibaba AI TongYi LLM connection properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiConnectionProperties.CONFIG_PREFIX) -public class TongYiConnectionProperties { - - /** - * Spring Cloud Alibaba AI connection configuration Prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "tongyi"; - - /** - * TongYi LLM API key. - */ - private String apiKey; - - public String getApiKey() { - return apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java deleted file mode 100644 index 2c8bd707b..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio; - -/** - * More models see: https://help.aliyun.com/zh/dashscope/model-list?spm=a2c4g.11186623.0.i5 - * Support all models in list. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public final class AudioSpeechModels { - - private AudioSpeechModels() { - } - - /** - * Male Voice of the Tongue(舌尖男声). - * zh & en. - * Default sample rate: 48 Hz. - */ - public static final String SAMBERT_ZHICHU_V1 = "sambert-zhichu-v1"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java deleted file mode 100644 index 4b8435629..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public final class AudioTranscriptionModels { - - private AudioTranscriptionModels() { - } - - /** - * Paraformer Chinese and English speech recognition model supports audio or video speech recognition with a sampling rate of 16kHz or above. - */ - public static final String Paraformer_V1 = "paraformer-v1"; - /** - * Paraformer Chinese speech recognition model, support 8kHz telephone speech recognition. - */ - public static final String Paraformer_8K_V1 = "paraformer-8k-v1"; - /** - * The Paraformer multilingual speech recognition model supports audio or video speech recognition with a sample rate of 16kHz or above. - */ - public static final String Paraformer_MTL_V1 = "paraformer-mtl-v1"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java deleted file mode 100644 index f450042da..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.cloud.ai.tongyi.audio.speech.api.*; -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisParam; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult; -import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; -import com.alibaba.dashscope.common.ResultCallback; -import io.reactivex.Flowable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; - -import java.nio.ByteBuffer; - -/** - * TongYiAudioSpeechClient is a client for TongYi audio speech service for Spring Cloud Alibaba AI. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechModel implements SpeechModel, SpeechStreamModel { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * Default speed rate. - */ - private static final float SPEED_RATE = 1.0f; - - /** - * TongYi models api. - */ - private final SpeechSynthesizer speechSynthesizer; - - /** - * TongYi models options. - */ - private final TongYiAudioSpeechOptions defaultOptions; - - /** - * TongYiAudioSpeechClient constructor. - * @param speechSynthesizer the speech synthesizer - */ - public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer) { - - this(speechSynthesizer, null); - } - - /** - * TongYiAudioSpeechClient constructor. - * @param speechSynthesizer the speech synthesizer - * @param tongYiAudioOptions the tongYi audio options - */ - public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer, TongYiAudioSpeechOptions tongYiAudioOptions) { - - Assert.notNull(speechSynthesizer, "speechSynthesizer must not be null"); - Assert.notNull(tongYiAudioOptions, "tongYiAudioOptions must not be null"); - - this.speechSynthesizer = speechSynthesizer; - this.defaultOptions = tongYiAudioOptions; - } - - /** - * Call the TongYi audio speech service. - * @param text the text message to be converted to audio. - * @return the audio byte buffer. - */ - @Override - public ByteBuffer call(String text) { - - var speechRequest = new SpeechPrompt(text); - - return call(speechRequest).getResult().getOutput(); - } - - - /** - * Call the TongYi audio speech service. - * @param prompt the speech prompt. - * @return the speech response. - */ - @Override - public SpeechResponse call(SpeechPrompt prompt) { - - var SCASpeechParam = merge(prompt.getOptions()); - var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam); - speechSynthesisParams.setText(prompt.getInstructions().getText()); - logger.info(speechSynthesisParams.toString()); - - var res = speechSynthesizer.call(speechSynthesisParams); - - return convert(res, null); - } - - /** - * Call the TongYi audio speech service. - * @param prompt the speech prompt. - * @param callback the result callback. - * {@link SpeechSynthesizer#call(SpeechSynthesisParam, ResultCallback)} - */ - public void call(SpeechPrompt prompt, ResultCallback callback) { - - var SCASpeechParam = merge(prompt.getOptions()); - var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam); - speechSynthesisParams.setText(prompt.getInstructions().getText()); - - speechSynthesizer.call(speechSynthesisParams, callback); - } - - /** - * Stream the TongYi audio speech service. - * @param prompt the speech prompt. - * @return the speech response. - * {@link SpeechSynthesizer#streamCall(SpeechSynthesisParam)} - */ - @Override - public Flux stream(SpeechPrompt prompt) { - - var SCASpeechParam = merge(prompt.getOptions()); - - Flowable resultFlowable = speechSynthesizer - .streamCall(toSpeechSynthesisParams(SCASpeechParam)); - - return Flux.from(resultFlowable) - .flatMap( - res -> Flux.just(res.getAudioFrame()) - .map(audio -> { - var speech = new Speech(audio); - var respMetadata = TongYiAudioSpeechResponseMetadata.from(res); - return new SpeechResponse(speech, respMetadata); - }) - ).publishOn(Schedulers.parallel()); - } - - public TongYiAudioSpeechOptions merge(TongYiAudioSpeechOptions target) { - - var mergeBuilder = TongYiAudioSpeechOptions.builder(); - - mergeBuilder.withModel(defaultOptions.getModel() != null ? defaultOptions.getModel() : target.getModel()); - mergeBuilder.withPitch(defaultOptions.getPitch() != null ? defaultOptions.getPitch() : target.getPitch()); - mergeBuilder.withRate(defaultOptions.getRate() != null ? defaultOptions.getRate() : target.getRate()); - mergeBuilder.withFormat(defaultOptions.getFormat() != null ? defaultOptions.getFormat() : target.getFormat()); - mergeBuilder.withSampleRate(defaultOptions.getSampleRate() != null ? defaultOptions.getSampleRate() : target.getSampleRate()); - mergeBuilder.withTextType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : target.getTextType()); - mergeBuilder.withVolume(defaultOptions.getVolume() != null ? defaultOptions.getVolume() : target.getVolume()); - mergeBuilder.withEnablePhonemeTimestamp(defaultOptions.isEnablePhonemeTimestamp() != null ? defaultOptions.isEnablePhonemeTimestamp() : target.isEnablePhonemeTimestamp()); - mergeBuilder.withEnableWordTimestamp(defaultOptions.isEnableWordTimestamp() != null ? defaultOptions.isEnableWordTimestamp() : target.isEnableWordTimestamp()); - - return mergeBuilder.build(); - } - - public SpeechSynthesisParam toSpeechSynthesisParams(TongYiAudioSpeechOptions source) { - - var mergeBuilder = SpeechSynthesisParam.builder(); - - mergeBuilder.model(source.getModel() != null ? source.getModel() : AudioSpeechModels.SAMBERT_ZHICHU_V1); - mergeBuilder.text(source.getText() != null ? source.getText() : ""); - - if (source.getFormat() != null) { - mergeBuilder.format(source.getFormat()); - } - if (source.getRate() != null) { - mergeBuilder.rate(source.getRate()); - } - if (source.getPitch() != null) { - mergeBuilder.pitch(source.getPitch()); - } - if (source.getTextType() != null) { - mergeBuilder.textType(source.getTextType()); - } - if (source.getSampleRate() != null) { - mergeBuilder.sampleRate(source.getSampleRate()); - } - if (source.isEnablePhonemeTimestamp() != null) { - mergeBuilder.enablePhonemeTimestamp(source.isEnablePhonemeTimestamp()); - } - if (source.isEnableWordTimestamp() != null) { - mergeBuilder.enableWordTimestamp(source.isEnableWordTimestamp()); - } - if (source.getVolume() != null) { - mergeBuilder.volume(source.getVolume()); - } - - return mergeBuilder.build(); - } - - /** - * Convert the TongYi audio speech service result to the speech response. - * @param result the audio byte buffer. - * @param synthesisResult the synthesis result. - * @return the speech response. - */ - private SpeechResponse convert(ByteBuffer result, SpeechSynthesisResult synthesisResult) { - - if (synthesisResult == null) { - - return new SpeechResponse(new Speech(result)); - } - - var responseMetadata = TongYiAudioSpeechResponseMetadata.from(synthesisResult); - var speech = new Speech(synthesisResult.getAudioFrame()); - - return new SpeechResponse(speech, responseMetadata); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java deleted file mode 100644 index 09f188f8e..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisTextType; -import org.springframework.ai.model.ModelOptions; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechOptions implements ModelOptions { - - - /** - * Audio Speech models. - */ - private String model = AudioSpeechModels.SAMBERT_ZHICHU_V1; - - /** - * Text content. - */ - private String text; - - /** - * Input text type. - */ - private SpeechSynthesisTextType textType = SpeechSynthesisTextType.PLAIN_TEXT; - - /** - * synthesis audio format. - */ - private SpeechSynthesisAudioFormat format = SpeechSynthesisAudioFormat.WAV; - - /** - * synthesis audio sample rate. - */ - private Integer sampleRate = 16000; - - /** - * synthesis audio volume. - */ - private Integer volume = 50; - - /** - * synthesis audio speed. - */ - private Float rate = 1.0f; - - /** - * synthesis audio pitch. - */ - private Float pitch = 1.0f; - - /** - * enable word level timestamp. - */ - private Boolean enableWordTimestamp = false; - - /** - * enable phoneme level timestamp. - */ - private Boolean enablePhonemeTimestamp = false; - - public static Builder builder() { - return new Builder(); - } - - public String getModel() { - - return model; - } - - public void setModel(String model) { - - this.model = model; - } - - public String getText() { - - return text; - } - - public void setText(String text) { - - this.text = text; - } - - public SpeechSynthesisTextType getTextType() { - - return textType; - } - - public void setTextType(SpeechSynthesisTextType textType) { - - this.textType = textType; - } - - public SpeechSynthesisAudioFormat getFormat() { - - return format; - } - - public void setFormat(SpeechSynthesisAudioFormat format) { - - this.format = format; - } - - public Integer getSampleRate() { - - return sampleRate; - } - - public void setSampleRate(Integer sampleRate) { - - this.sampleRate = sampleRate; - } - - public Integer getVolume() { - - return volume; - } - - public void setVolume(Integer volume) { - - this.volume = volume; - } - - public Float getRate() { - - return rate; - } - - public void setRate(Float rate) { - - this.rate = rate; - } - - public Float getPitch() { - - return pitch; - } - - public void setPitch(Float pitch) { - - this.pitch = pitch; - } - - public Boolean isEnableWordTimestamp() { - - return enableWordTimestamp; - } - - public void setEnableWordTimestamp(Boolean enableWordTimestamp) { - - this.enableWordTimestamp = enableWordTimestamp; - } - - public Boolean isEnablePhonemeTimestamp() { - - return enablePhonemeTimestamp; - } - - public void setEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) { - - this.enablePhonemeTimestamp = enablePhonemeTimestamp; - } - - /** - * Build a options instances. - */ - public static class Builder { - - private final TongYiAudioSpeechOptions options = new TongYiAudioSpeechOptions(); - - public Builder withModel(String model) { - - options.model = model; - return this; - } - - public Builder withText(String text) { - - options.text = text; - return this; - } - - public Builder withTextType(SpeechSynthesisTextType textType) { - - options.textType = textType; - return this; - } - - public Builder withFormat(SpeechSynthesisAudioFormat format) { - - options.format = format; - return this; - } - - public Builder withSampleRate(Integer sampleRate) { - - options.sampleRate = sampleRate; - return this; - } - - public Builder withVolume(Integer volume) { - - options.volume = volume; - return this; - } - - public Builder withRate(Float rate) { - - options.rate = rate; - return this; - } - - public Builder withPitch(Float pitch) { - - options.pitch = pitch; - return this; - } - - public Builder withEnableWordTimestamp(Boolean enableWordTimestamp) { - - options.enableWordTimestamp = enableWordTimestamp; - return this; - } - - public Builder withEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) { - - options.enablePhonemeTimestamp = enablePhonemeTimestamp; - return this; - } - - public TongYiAudioSpeechOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java deleted file mode 100644 index a3b435770..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * TongYi audio speech configuration properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiAudioSpeechProperties.CONFIG_PREFIX) -public class TongYiAudioSpeechProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.speech"; - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_AUDIO_MODEL_NAME = AudioSpeechModels.SAMBERT_ZHICHU_V1; - - /** - * Enable TongYiQWEN ai audio client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiAudioSpeechOptions options = TongYiAudioSpeechOptions.builder() - .withModel(DEFAULT_AUDIO_MODEL_NAME) - .withFormat(SpeechSynthesisAudioFormat.WAV) - .build(); - - public TongYiAudioSpeechOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiAudioSpeechOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java deleted file mode 100644 index adeef4e79..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.ModelResult; -import org.springframework.lang.Nullable; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class Speech implements ModelResult { - - private final ByteBuffer audio; - - private SpeechMetadata speechMetadata; - - public Speech(ByteBuffer audio) { - this.audio = audio; - } - - @Override - public ByteBuffer getOutput() { - return this.audio; - } - - @Override - public SpeechMetadata getMetadata() { - - return speechMetadata != null ? speechMetadata : SpeechMetadata.NULL; - } - - public Speech withSpeechMetadata(@Nullable SpeechMetadata speechMetadata) { - - this.speechMetadata = speechMetadata; - return this; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof Speech that)) { - - return false; - } - - return Arrays.equals(audio.array(), that.audio.array()) - && Objects.equals(speechMetadata, that.speechMetadata); - } - - @Override - public int hashCode() { - - return Objects.hash(Arrays.hashCode(audio.array()), speechMetadata); - } - - @Override - public String toString() { - - return "Speech{" + "text=" + audio + ", speechMetadata=" + speechMetadata + '}'; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java deleted file mode 100644 index 9748bcf50..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import java.util.Objects; - -/** - * The {@link SpeechMessage} class represents a single text message to - * be converted to speech by the TongYi LLM TTS. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechMessage { - - private String text; - - /** - * Constructs a new {@link SpeechMessage} object with the given text. - * @param text the text to be converted to speech - */ - public SpeechMessage(String text) { - this.text = text; - } - - /** - * Returns the text of this speech message. - * @return the text of this speech message - */ - public String getText() { - return text; - } - - /** - * Sets the text of this speech message. - * @param text the new text for this speech message - */ - public void setText(String text) { - this.text = text; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - - if (!(o instanceof SpeechMessage that)) { - - return false; - } - - return Objects.equals(text, that.text); - } - - @Override - public int hashCode() { - - return Objects.hash(text); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java deleted file mode 100644 index 513a2839c..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.ResultMetadata; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public interface SpeechMetadata extends ResultMetadata { - - /** - * Null Object. - */ - SpeechMetadata NULL = SpeechMetadata.create(); - - /** - * Factory method used to construct a new {@link SpeechMetadata}. - * @return a new {@link SpeechMetadata} - */ - static SpeechMetadata create() { - return new SpeechMetadata() { - }; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java deleted file mode 100644 index 601dce72c..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.Model; - -import java.nio.ByteBuffer; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.0.0-RC1 - */ - -@FunctionalInterface -public interface SpeechModel extends Model { - - /** - * Generates spoken audio from the provided text message. - * @param message the text message to be converted to audio. - * @return the resulting audio bytes. - */ - default ByteBuffer call(String message) { - - SpeechPrompt prompt = new SpeechPrompt(message); - - return call(prompt).getResult().getOutput(); - } - - /** - * Sends a speech request to the TongYi TTS API and returns the resulting speech response. - * @param request the speech prompt containing the input text and other parameters. - * @return the speech response containing the generated audio. - */ - SpeechResponse call(SpeechPrompt request); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java deleted file mode 100644 index dc89ff31a..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechOptions; -import org.springframework.ai.model.ModelRequest; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechPrompt implements ModelRequest { - - private TongYiAudioSpeechOptions speechOptions; - - private final SpeechMessage message; - - public SpeechPrompt(String instructions) { - - this(new SpeechMessage(instructions), TongYiAudioSpeechOptions.builder().build()); - } - - public SpeechPrompt(String instructions, TongYiAudioSpeechOptions speechOptions) { - - this(new SpeechMessage(instructions), speechOptions); - } - - public SpeechPrompt(SpeechMessage speechMessage) { - this(speechMessage, TongYiAudioSpeechOptions.builder().build()); - } - - public SpeechPrompt(SpeechMessage speechMessage, TongYiAudioSpeechOptions speechOptions) { - - this.message = speechMessage; - this.speechOptions = speechOptions; - } - - @Override - public SpeechMessage getInstructions() { - return this.message; - } - - @Override - public TongYiAudioSpeechOptions getOptions() { - - return speechOptions; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof SpeechPrompt that)) { - - return false; - } - - return Objects.equals(speechOptions, that.speechOptions) && Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - - return Objects.hash(speechOptions, message); - } - - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java deleted file mode 100644 index b8d7484cf..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata; -import org.springframework.ai.model.ModelResponse; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechResponse implements ModelResponse { - - private final Speech speech; - - private final TongYiAudioSpeechResponseMetadata speechResponseMetadata; - - /** - * Creates a new instance of SpeechResponse with the given speech result. - * @param speech the speech result to be set in the SpeechResponse - * @see Speech - */ - public SpeechResponse(Speech speech) { - this(speech, TongYiAudioSpeechResponseMetadata.NULL); - } - - /** - * Creates a new instance of SpeechResponse with the given speech result and speech - * response metadata. - * @param speech the speech result to be set in the SpeechResponse - * @param speechResponseMetadata the speech response metadata to be set in the - * SpeechResponse - * @see Speech - * @see TongYiAudioSpeechResponseMetadata - */ - public SpeechResponse(Speech speech, TongYiAudioSpeechResponseMetadata speechResponseMetadata) { - - this.speech = speech; - this.speechResponseMetadata = speechResponseMetadata; - } - - @Override - public Speech getResult() { - return speech; - } - - @Override - public List getResults() { - return Collections.singletonList(speech); - } - - @Override - public TongYiAudioSpeechResponseMetadata getMetadata() { - return speechResponseMetadata; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof SpeechResponse that)) { - - return false; - } - - return Objects.equals(speech, that.speech) - && Objects.equals(speechResponseMetadata, that.speechResponseMetadata); - } - - @Override - public int hashCode() { - - return Objects.hash(speech, speechResponseMetadata); - } - - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java deleted file mode 100644 index d39c79ab2..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.StreamingModel; -import reactor.core.publisher.Flux; - -import java.nio.ByteBuffer; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@FunctionalInterface -public interface SpeechStreamModel extends StreamingModel { - - /** - * Generates a stream of audio bytes from the provided text message. - * - * @param message the text message to be converted to audio - * @return a Flux of audio bytes representing the generated speech - */ - default Flux stream(String message) { - - SpeechPrompt prompt = new SpeechPrompt(message); - return stream(prompt).map(SpeechResponse::getResult).map(Speech::getOutput); - } - - /** - * Sends a speech request to the TongYi TTS API and returns a stream of the resulting - * speech responses. - * @param prompt the speech prompt containing the input text and other parameters. - * @return a Flux of speech responses, each containing a portion of the generated audio. - */ - @Override - Flux stream(SpeechPrompt prompt); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java deleted file mode 100644 index 0f0dca9c0..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResult; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata; -import com.alibaba.dashscope.audio.asr.transcription.*; -import org.springframework.ai.model.Model; -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * TongYiAudioTranscriptionModel is a client for TongYi audio transcription service for - * Spring Cloud Alibaba AI. - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionModel - implements Model { - - /** - * TongYi models options. - */ - private final TongYiAudioTranscriptionOptions defaultOptions; - - /** - * TongYi models api. - */ - private final Transcription transcription; - - public TongYiAudioTranscriptionModel(Transcription transcription) { - this(null, transcription); - } - - public TongYiAudioTranscriptionModel(TongYiAudioTranscriptionOptions defaultOptions, - Transcription transcription) { - Assert.notNull(transcription, "transcription must not be null"); - Assert.notNull(defaultOptions, "defaultOptions must not be null"); - - this.defaultOptions = defaultOptions; - this.transcription = transcription; - } - - @Override - public AudioTranscriptionResponse call(AudioTranscriptionPrompt prompt) { - - TranscriptionParam transcriptionParam; - - if (prompt.getOptions() != null) { - var param = merge(prompt.getOptions()); - transcriptionParam = toTranscriptionParam(param); - transcriptionParam.setFileUrls(prompt.getOptions().getFileUrls()); - } - else { - Resource instructions = prompt.getInstructions(); - try { - transcriptionParam = TranscriptionParam.builder() - .model(AudioTranscriptionModels.Paraformer_V1) - .fileUrls(ListUtil.of(String.valueOf(instructions.getURL()))) - .build(); - } - catch (IOException e) { - throw new TongYiException("Failed to create resource", e); - } - } - - List taskResultList; - try { - // Submit a transcription request - TranscriptionResult result = transcription.asyncCall(transcriptionParam); - // Wait for the transcription to complete - result = transcription.wait(TranscriptionQueryParam - .FromTranscriptionParam(transcriptionParam, result.getTaskId())); - // Get the transcription results - System.out.println(result.getOutput().getAsJsonObject()); - taskResultList = result.getResults(); - System.out.println(Arrays.toString(taskResultList.toArray())); - - return new AudioTranscriptionResponse( - taskResultList.stream().map(taskResult -> - new AudioTranscriptionResult(taskResult.getTranscriptionUrl()) - ).collect(Collectors.toList()), - TongYiAudioTranscriptionResponseMetadata.from(result) - ); - } - catch (Exception e) { - throw new TongYiException("Failed to call audio transcription", e); - } - - } - - public TongYiAudioTranscriptionOptions merge(TongYiAudioTranscriptionOptions target) { - var mergeBuilder = TongYiAudioTranscriptionOptions.builder(); - - mergeBuilder - .withModel(defaultOptions.getModel() != null ? defaultOptions.getModel() - : target.getModel()); - mergeBuilder.withChannelId( - defaultOptions.getChannelId() != null ? defaultOptions.getChannelId() - : target.getChannelId()); - mergeBuilder.withDiarizationEnabled(defaultOptions.getDiarizationEnabled() != null - ? defaultOptions.getDiarizationEnabled() - : target.getDiarizationEnabled()); - mergeBuilder.withDisfluencyRemovalEnabled( - defaultOptions.getDisfluencyRemovalEnabled() != null - ? defaultOptions.getDisfluencyRemovalEnabled() - : target.getDisfluencyRemovalEnabled()); - mergeBuilder.withTimestampAlignmentEnabled( - defaultOptions.getTimestampAlignmentEnabled() != null - ? defaultOptions.getTimestampAlignmentEnabled() - : target.getTimestampAlignmentEnabled()); - mergeBuilder.withSpecialWordFilter(defaultOptions.getSpecialWordFilter() != null - ? defaultOptions.getSpecialWordFilter() - : target.getSpecialWordFilter()); - mergeBuilder.withAudioEventDetectionEnabled( - defaultOptions.getAudioEventDetectionEnabled() != null - ? defaultOptions.getAudioEventDetectionEnabled() - : target.getAudioEventDetectionEnabled()); - - return mergeBuilder.build(); - } - - public TranscriptionParam toTranscriptionParam( - TongYiAudioTranscriptionOptions source) { - var mergeBuilder = TranscriptionParam.builder(); - - mergeBuilder.model(source.getModel() != null ? source.getModel() - : AudioTranscriptionModels.Paraformer_V1); - mergeBuilder.fileUrls( - source.getFileUrls() != null ? source.getFileUrls() : new ArrayList<>()); - if (source.getPhraseId() != null) { - mergeBuilder.phraseId(source.getPhraseId()); - } - if (source.getChannelId() != null) { - mergeBuilder.channelId(source.getChannelId()); - } - if (source.getDiarizationEnabled() != null) { - mergeBuilder.diarizationEnabled(source.getDiarizationEnabled()); - } - if (source.getSpeakerCount() != null) { - mergeBuilder.speakerCount(source.getSpeakerCount()); - } - if (source.getDisfluencyRemovalEnabled() != null) { - mergeBuilder.disfluencyRemovalEnabled(source.getDisfluencyRemovalEnabled()); - } - if (source.getTimestampAlignmentEnabled() != null) { - mergeBuilder.timestampAlignmentEnabled(source.getTimestampAlignmentEnabled()); - } - if (source.getSpecialWordFilter() != null) { - mergeBuilder.specialWordFilter(source.getSpecialWordFilter()); - } - if (source.getAudioEventDetectionEnabled() != null) { - mergeBuilder - .audioEventDetectionEnabled(source.getAudioEventDetectionEnabled()); - } - - return mergeBuilder.build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java deleted file mode 100644 index 3fec6375c..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription; - -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import org.springframework.ai.model.ModelOptions; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionOptions implements ModelOptions { - - private String model = AudioTranscriptionModels.Paraformer_V1; - - private List fileUrls = new ArrayList<>(); - - private String phraseId = null; - - private List channelId = Collections.singletonList(0); - - private Boolean diarizationEnabled = false; - - private Integer speakerCount = null; - - private Boolean disfluencyRemovalEnabled = false; - - private Boolean timestampAlignmentEnabled = false; - - private String specialWordFilter = ""; - - private Boolean audioEventDetectionEnabled = false; - - public static Builder builder() { - - return new Builder(); - } - - public String getModel() { - return model; - } - - public void setModel(String model) { - this.model = model; - } - - public List getFileUrls() { - return fileUrls; - } - - public void setFileUrls(List fileUrls) { - this.fileUrls = fileUrls; - } - - public String getPhraseId() { - return phraseId; - } - - public void setPhraseId(String phraseId) { - this.phraseId = phraseId; - } - - public List getChannelId() { - return channelId; - } - - public void setChannelId(List channelId) { - this.channelId = channelId; - } - - public Boolean getDiarizationEnabled() { - return diarizationEnabled; - } - - public void setDiarizationEnabled(Boolean diarizationEnabled) { - this.diarizationEnabled = diarizationEnabled; - } - - public Integer getSpeakerCount() { - return speakerCount; - } - - public void setSpeakerCount(Integer speakerCount) { - this.speakerCount = speakerCount; - } - - public Boolean getDisfluencyRemovalEnabled() { - return disfluencyRemovalEnabled; - } - - public void setDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) { - this.disfluencyRemovalEnabled = disfluencyRemovalEnabled; - } - - public Boolean getTimestampAlignmentEnabled() { - return timestampAlignmentEnabled; - } - - public void setTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) { - this.timestampAlignmentEnabled = timestampAlignmentEnabled; - } - - public String getSpecialWordFilter() { - return specialWordFilter; - } - - public void setSpecialWordFilter(String specialWordFilter) { - this.specialWordFilter = specialWordFilter; - } - - public Boolean getAudioEventDetectionEnabled() { - return audioEventDetectionEnabled; - } - - public void setAudioEventDetectionEnabled(Boolean audioEventDetectionEnabled) { - this.audioEventDetectionEnabled = audioEventDetectionEnabled; - } - - /** - * Builder class for constructing TongYiAudioTranscriptionOptions instances. - */ - public static class Builder { - - private final TongYiAudioTranscriptionOptions options = new TongYiAudioTranscriptionOptions(); - - public Builder withModel(String model) { - options.model = model; - return this; - } - - public Builder withFileUrls(List fileUrls) { - options.fileUrls = fileUrls; - return this; - } - - public Builder withPhraseId(String phraseId) { - options.phraseId = phraseId; - return this; - } - - public Builder withChannelId(List channelId) { - options.channelId = channelId; - return this; - } - - public Builder withDiarizationEnabled(Boolean diarizationEnabled) { - options.diarizationEnabled = diarizationEnabled; - return this; - } - - public Builder withSpeakerCount(Integer speakerCount) { - options.speakerCount = speakerCount; - return this; - } - - public Builder withDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) { - options.disfluencyRemovalEnabled = disfluencyRemovalEnabled; - return this; - } - - public Builder withTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) { - options.timestampAlignmentEnabled = timestampAlignmentEnabled; - return this; - } - - public Builder withSpecialWordFilter(String specialWordFilter) { - options.specialWordFilter = specialWordFilter; - return this; - } - - public Builder withAudioEventDetectionEnabled( - Boolean audioEventDetectionEnabled) { - options.audioEventDetectionEnabled = audioEventDetectionEnabled; - return this; - } - - public TongYiAudioTranscriptionOptions build() { - // Perform any necessary validation here before returning the built object - return options; - } - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java deleted file mode 100644 index fe86d0d72..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription; - -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiAudioTranscriptionProperties.CONFIG_PREFIX) -public class TongYiAudioTranscriptionProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.transcription"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_AUDIO_MODEL_NAME = AudioTranscriptionModels.Paraformer_V1; - - /** - * Enable TongYiQWEN ai audio client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiAudioTranscriptionOptions options = TongYiAudioTranscriptionOptions - .builder().withModel(DEFAULT_AUDIO_MODEL_NAME).build(); - - public TongYiAudioTranscriptionOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiAudioTranscriptionOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java deleted file mode 100644 index f4c288245..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionOptions; -import org.springframework.ai.model.ModelRequest; -import org.springframework.core.io.Resource; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class AudioTranscriptionPrompt implements ModelRequest { - - private Resource audioResource; - - private TongYiAudioTranscriptionOptions transcriptionOptions; - - public AudioTranscriptionPrompt(Resource resource) { - this.audioResource = resource; - } - - public AudioTranscriptionPrompt(Resource resource, TongYiAudioTranscriptionOptions transcriptionOptions) { - this.audioResource = resource; - this.transcriptionOptions = transcriptionOptions; - } - - @Override - public Resource getInstructions() { - - return audioResource; - } - - @Override - public TongYiAudioTranscriptionOptions getOptions() { - - return transcriptionOptions; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java deleted file mode 100644 index 2e740158b..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata; -import org.springframework.ai.model.ModelResponse; -import org.springframework.ai.model.ResponseMetadata; - -import java.util.List; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class AudioTranscriptionResponse implements ModelResponse { - - private List resultList; - - private TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata; - - public AudioTranscriptionResponse(List result) { - - this(result, TongYiAudioTranscriptionResponseMetadata.NULL); - } - - public AudioTranscriptionResponse(List result, - TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata) { - - this.resultList = List.copyOf(result); - this.transcriptionResponseMetadata = transcriptionResponseMetadata; - } - - @Override - public AudioTranscriptionResult getResult() { - - return this.resultList.get(0); - } - - @Override - public List getResults() { - - return this.resultList; - } - - @Override - public ResponseMetadata getMetadata() { - - return this.transcriptionResponseMetadata; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java deleted file mode 100644 index 651855caf..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionMetadata; -import org.springframework.ai.model.ModelResult; -import org.springframework.ai.model.ResultMetadata; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ -public class AudioTranscriptionResult implements ModelResult { - - private String text; - - private TongYiAudioTranscriptionMetadata transcriptionMetadata; - - public AudioTranscriptionResult(String text) { - this.text = text; - } - - @Override - public String getOutput() { - - return this.text; - } - - @Override - public ResultMetadata getMetadata() { - - return transcriptionMetadata != null ? transcriptionMetadata : TongYiAudioTranscriptionMetadata.NULL; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AudioTranscriptionResult that = (AudioTranscriptionResult) o; - return Objects.equals(text, that.text) && Objects.equals(transcriptionMetadata, that.transcriptionMetadata); - } - - @Override - public int hashCode() { - return Objects.hash(text, transcriptionMetadata); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java deleted file mode 100644 index 11328a02e..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java +++ /dev/null @@ -1,483 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.chat; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.dashscope.aigc.conversation.ConversationParam; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationOutput; -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.common.Role; -import com.alibaba.dashscope.exception.InputRequiredException; -import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.tools.FunctionDefinition; -import com.alibaba.dashscope.tools.ToolCallBase; -import com.alibaba.dashscope.tools.ToolCallFunction; -import com.alibaba.dashscope.utils.ApiKeywords; -import com.alibaba.dashscope.utils.JsonUtils; -import io.reactivex.Flowable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.StreamingChatModel; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.model.function.AbstractFunctionCallSupport; -import org.springframework.ai.model.function.FunctionCallbackContext; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.CollectionUtils; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; - -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - - -/** - * {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal Alibaba DashScope} - * backed by {@link Generation}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - * @see ChatModel - * @see com.alibaba.dashscope.aigc.generation - */ - -public class TongYiChatModel extends - AbstractFunctionCallSupport< - com.alibaba.dashscope.common.Message, - ConversationParam, - GenerationResult> - implements ChatModel, StreamingChatModel { - - private static final Logger logger = LoggerFactory.getLogger(TongYiChatModel.class); - - /** - * DashScope generation client. - */ - private final Generation generation; - - /** - * The TongYi models default chat completion api. - */ - private TongYiChatOptions defaultOptions; - - /** - * User role message manager. - */ - @Autowired - private MessageManager msgManager; - - /** - * Initializes an instance of the TongYiChatClient. - * @param generation DashScope generation client. - */ - public TongYiChatModel(Generation generation) { - - this(generation, - TongYiChatOptions.builder() - .withTopP(0.8) - .withEnableSearch(true) - .withResultFormat(ConversationParam.ResultFormat.MESSAGE) - .build(), - null - ); - } - - /** - * Initializes an instance of the TongYiChatClient. - * @param generation DashScope generation client. - * @param options TongYi model params. - */ - public TongYiChatModel(Generation generation, TongYiChatOptions options) { - - this(generation, options, null); - } - - /** - * Create a TongYi models client. - * @param generation DashScope model generation client. - * @param options TongYi default chat completion api. - */ - public TongYiChatModel(Generation generation, TongYiChatOptions options, - FunctionCallbackContext functionCallbackContext) { - - super(functionCallbackContext); - this.generation = generation; - this.defaultOptions = options; - } - - /** - * Get default sca chat options. - * - * @return TongYiChatOptions default object. - */ - public TongYiChatOptions getDefaultOptions() { - - return this.defaultOptions; - } - - @Override - public ChatResponse call(Prompt prompt) { - - ConversationParam params = toTongYiChatParams(prompt); - - // TongYi models context loader. - com.alibaba.dashscope.common.Message message = new com.alibaba.dashscope.common.Message(); - message.setRole(Role.USER.getValue()); - message.setContent(prompt.getContents()); - msgManager.add(message); - params.setMessages(msgManager.get()); - - logger.trace("TongYi ConversationOptions: {}", params); - GenerationResult chatCompletions = this.callWithFunctionSupport(params); - logger.trace("TongYi ConversationOptions: {}", params); - - msgManager.add(chatCompletions); - - List generations = - chatCompletions - .getOutput() - .getChoices() - .stream() - .map(choice -> - new org.springframework.ai.chat.model.Generation( - choice - .getMessage() - .getContent() - ).withGenerationMetadata(generateChoiceMetadata(choice) - )) - .toList(); - - return new ChatResponse(generations); - - } - - @Override - public Flux stream(Prompt prompt) { - - Flowable genRes; - ConversationParam tongYiChatParams = toTongYiChatParams(prompt); - - // See https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.0.655fc11aRR0jj7#b9ad0a10cfhpe - // tongYiChatParams.setIncrementalOutput(true); - - try { - genRes = generation.streamCall(tongYiChatParams); - } - catch (NoApiKeyException | InputRequiredException e) { - logger.warn("TongYi chat client: " + e.getMessage()); - throw new TongYiException(e.getMessage()); - } - - return Flux.from(genRes) - .flatMap( - message -> Flux.just( - message.getOutput() - .getChoices() - .get(0) - .getMessage() - .getContent()) - .map(content -> { - var gen = new org.springframework.ai.chat.model.Generation(content) - .withGenerationMetadata(generateChoiceMetadata( - message.getOutput() - .getChoices() - .get(0) - )); - return new ChatResponse(ListUtil.of(gen)); - }) - ) - .publishOn(Schedulers.parallel()); - - } - - /** - * Configuration properties to Qwen model params. - * Test access. - * - * @param prompt {@link Prompt} - * @return Qwen models params {@link ConversationParam} - */ - public ConversationParam toTongYiChatParams(Prompt prompt) { - - Set functionsForThisRequest = new HashSet<>(); - - List tongYiMessage = prompt.getInstructions().stream() - .map(this::fromSpringAIMessage) - .toList(); - - ConversationParam chatParams = ConversationParam.builder() - .messages(tongYiMessage) - // models setting - // {@link HalfDuplexServiceParam#models} - .model(Generation.Models.QWEN_TURBO) - // {@link GenerationOutput} - .resultFormat(ConversationParam.ResultFormat.MESSAGE) - .incrementalOutput(true) - - .build(); - - if (this.defaultOptions != null) { - - chatParams = merge(chatParams, this.defaultOptions); - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, !IS_RUNTIME_CALL); - functionsForThisRequest.addAll(defaultEnabledFunctions); - } - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - TongYiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, TongYiChatOptions.class); - - chatParams = merge(updatedRuntimeOptions, chatParams); - - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); - - } - else { - throw new IllegalArgumentException("Prompt options are not of type ConversationParam:" - + prompt.getOptions().getClass().getSimpleName()); - } - } - - // Add the enabled functions definitions to the request's tools parameter. - - if (!CollectionUtils.isEmpty(functionsForThisRequest)) { - List tools = this.getFunctionTools(functionsForThisRequest); - - // todo chatParams.setTools(tools) - } - - return chatParams; - } - - private ChatGenerationMetadata generateChoiceMetadata(GenerationOutput.Choice choice) { - - return ChatGenerationMetadata.from( - String.valueOf(choice.getFinishReason()), - choice.getMessage().getContent() - ); - } - - private List getFunctionTools(Set functionNames) { - return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> { - - FunctionDefinition functionDefinition = FunctionDefinition.builder() - .name(functionCallback.getName()) - .description(functionCallback.getDescription()) - .parameters(JsonUtils.parametersToJsonObject( - ModelOptionsUtils.jsonToMap(functionCallback.getInputTypeSchema()) - )) - .build(); - - return functionDefinition; - }).toList(); - } - - - private ConversationParam merge(ConversationParam tongYiParams, TongYiChatOptions scaChatParams) { - - if (scaChatParams == null) { - - return tongYiParams; - } - - return ConversationParam.builder() - .messages(tongYiParams.getMessages()) - .maxTokens((tongYiParams.getMaxTokens() != null) ? tongYiParams.getMaxTokens() : scaChatParams.getMaxTokens()) - // When merge options. Because ConversationParams is must not null. So is setting. - .model(scaChatParams.getModel()) - .resultFormat((tongYiParams.getResultFormat() != null) ? tongYiParams.getResultFormat() : scaChatParams.getResultFormat()) - .enableSearch((tongYiParams.getEnableSearch() != null) ? tongYiParams.getEnableSearch() : scaChatParams.getEnableSearch()) - .topK((tongYiParams.getTopK() != null) ? tongYiParams.getTopK() : scaChatParams.getTopK()) - .topP((tongYiParams.getTopP() != null) ? tongYiParams.getTopP() : scaChatParams.getTopP()) - .incrementalOutput((tongYiParams.getIncrementalOutput() != null) ? tongYiParams.getIncrementalOutput() : scaChatParams.getIncrementalOutput()) - .temperature((tongYiParams.getTemperature() != null) ? tongYiParams.getTemperature() : scaChatParams.getTemperature()) - .repetitionPenalty((tongYiParams.getRepetitionPenalty() != null) ? tongYiParams.getRepetitionPenalty() : scaChatParams.getRepetitionPenalty()) - .seed((tongYiParams.getSeed() != null) ? tongYiParams.getSeed() : scaChatParams.getSeed()) - .build(); - - } - - private ConversationParam merge(TongYiChatOptions scaChatParams, ConversationParam tongYiParams) { - - if (scaChatParams == null) { - - return tongYiParams; - } - - ConversationParam mergedTongYiParams = ConversationParam.builder() - .model(Generation.Models.QWEN_TURBO) - .messages(tongYiParams.getMessages()) - .build(); - mergedTongYiParams = merge(tongYiParams, scaChatParams); - - if (scaChatParams.getMaxTokens() != null) { - mergedTongYiParams.setMaxTokens(scaChatParams.getMaxTokens()); - } - - if (scaChatParams.getStop() != null) { - mergedTongYiParams.setStopStrings(scaChatParams.getStop()); - } - - if (scaChatParams.getTemperature() != null) { - mergedTongYiParams.setTemperature(scaChatParams.getTemperature()); - } - - if (scaChatParams.getTopK() != null) { - mergedTongYiParams.setTopK(scaChatParams.getTopK()); - } - - if (scaChatParams.getTopK() != null) { - mergedTongYiParams.setTopK(scaChatParams.getTopK()); - } - - return mergedTongYiParams; - } - - private com.alibaba.dashscope.common.Message fromSpringAIMessage(Message message) { - - return switch (message.getMessageType()) { - case USER -> com.alibaba.dashscope.common.Message.builder() - .role(Role.USER.getValue()) - .content(message.getContent()) - .build(); - case SYSTEM -> com.alibaba.dashscope.common.Message.builder() - .role(Role.SYSTEM.getValue()) - .content(message.getContent()) - .build(); - case ASSISTANT -> com.alibaba.dashscope.common.Message.builder() - .role(Role.ASSISTANT.getValue()) - .content(message.getContent()) - .build(); - default -> throw new IllegalArgumentException("Unknown message type " + message.getMessageType()); - }; - - } - - @Override - protected ConversationParam doCreateToolResponseRequest( - ConversationParam previousRequest, - com.alibaba.dashscope.common.Message responseMessage, - List conversationHistory - ) { - for (ToolCallBase toolCall : responseMessage.getToolCalls()) { - if (toolCall instanceof ToolCallFunction toolCallFunction) { - if (toolCallFunction.getFunction() != null) { - var functionName = toolCallFunction.getFunction().getName(); - var functionArguments = toolCallFunction.getFunction().getArguments(); - - if (!this.functionCallbackRegister.containsKey(functionName)) { - throw new IllegalStateException("No function callback found for function name: " + functionName); - } - - String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments); - - // Add the function response to the conversation. - conversationHistory - .add(com.alibaba.dashscope.common.Message.builder() - .content(functionResponse) - .role(Role.BOT.getValue()) - .toolCallId(toolCall.getId()) - .build() - ); - } - } - - } - - ConversationParam newRequest = ConversationParam.builder().messages(conversationHistory).build(); - - // todo: No @JsonProperty fields. - newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ConversationParam.class); - - return newRequest; - - } - - @Override - protected List doGetUserMessages(ConversationParam request) { - - return request.getMessages(); - } - - @Override - protected com.alibaba.dashscope.common.Message doGetToolResponseMessage(GenerationResult response) { - - var message = response.getOutput().getChoices().get(0).getMessage(); - var assistantMessage = com.alibaba.dashscope.common.Message.builder().role(Role.ASSISTANT.getValue()) - .content("").build(); - assistantMessage.setToolCalls(message.getToolCalls()); - - return assistantMessage; - } - - @Override - protected GenerationResult doChatCompletion(ConversationParam request) { - - GenerationResult result; - try { - result = generation.call(request); - } - catch (NoApiKeyException | InputRequiredException e) { - throw new RuntimeException(e); - } - - return result; - } - - @Override - protected Flux doChatCompletionStream(ConversationParam request) { - final Flowable genRes; - try { - genRes = generation.streamCall(request); - } - catch (NoApiKeyException | InputRequiredException e) { - logger.warn("TongYi chat client: " + e.getMessage()); - throw new TongYiException(e.getMessage()); - } - return Flux.from(genRes); - - } - - @Override - protected boolean isToolFunctionCall(GenerationResult response) { - - if (response == null || CollectionUtils.isEmpty(response.getOutput().getChoices())) { - - return false; - } - var choice = response.getOutput().getChoices().get(0); - if (choice == null || choice.getFinishReason() == null) { - - return false; - } - - return Objects.equals(choice.getFinishReason(), ApiKeywords.TOOL_CALLS); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java deleted file mode 100644 index 20005fd99..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.chat; - -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationParam; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallingOptions; -import org.springframework.util.Assert; - -import java.util.*; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions { - - /** - * TongYi Models. - * {@link Generation.Models} - */ - private String model = Generation.Models.QWEN_TURBO; - - /** - * The random number seed used in generation, the user controls the randomness of the content generated by the model. - * seed supports unsigned 64-bit integers, with a default value of 1234. - * when using seed, the model will generate the same or similar results as much as possible, but there is currently no guarantee that the results will be exactly the same each time. - */ - private Integer seed = 1234; - - /** - * Used to specify the maximum number of tokens that the model can generate when generating content, - * it defines the upper limit of generation but does not guarantee that this number will be generated every time. - * For qwen-turbo the maximum and default values are 1500 tokens. - * The qwen-max, qwen-max-1201, qwen-max-longcontext, and qwen-plus models have a maximum and default value of 2000 tokens. - */ - private Integer maxTokens = 1500; - - /** - * The generation process kernel sampling method probability threshold, - * for example, takes the value of 0.8, only retains the smallest set of the most probable tokens with probabilities that add up to greater than or equal to 0.8 as the candidate set. - * The range of values is (0,1.0), the larger the value, the higher the randomness of generation; the lower the value, the higher the certainty of generation. - */ - private Double topP = 0.8; - - /** - * The size of the sampling candidate set at the time of generation. - * For example, with a value of 50, only the 50 highest scoring tokens in a single generation will form a randomly sampled candidate set. - * The larger the value, the higher the randomness of the generation; the smaller the value, the higher the certainty of the generation. - * This parameter is not passed by default, and a value of None or when top_k is greater than 100 indicates that the top_k policy is not enabled, - * at which time, only the top_p policy is in effect. - */ - private Integer topK; - - /** - * Used to control the repeatability of model generation. - * Increasing repetition_penalty reduces the repetition of model generation. 1.0 means no penalty. - */ - private Double repetitionPenalty = 1.1; - - /** - * is used to control the degree of randomness and diversity. - * Specifically, the temperature value controls the extent to which the probability distribution of each candidate word is smoothed when generating text. - * Higher values of temperature reduce the peak of the probability distribution, allowing more low-probability words to be selected and generating more diverse results, - * while lower values of temperature enhance the peak of the probability distribution, making it easier for high-probability words to be selected and generating more certain results. - * Range: [0, 2), 0 is not recommended, meaningless. - * java version >= 2.5.1 - */ - private Double temperature = 0.85; - - /** - * The stop parameter is used to realize precise control of the content generation process, automatically stopping when the generated content is about to contain the specified string or token_ids, - * and the generated content does not contain the specified content. - * For example, if stop is specified as "Hello", it means stop when "Hello" will be generated; if stop is specified as [37763, 367], it means stop when "Observation" will be generated. - * The stop parameter can be passed as a list of arrays of strings or token_ids to support the scenario of using multiple stops. - * Explanation: Do not mix strings and token_ids in list mode, the element types should be the same in list mode. - */ - private List stop; - - /** - * Whether or not to use stream output. When outputting the result in stream mode, the interface returns the result as generator, - * you need to iterate to get the result, the default output is the whole sequence of the current generation for each output, - * the last output is the final result of all the generation, you can change the output mode to non-incremental output by the parameter incremental_output to False. - */ - private Boolean stream = false; - - /** - * The model has a built-in Internet search service. - * This parameter controls whether the model refers to the use of Internet search results when generating text. The values are as follows: - * True: enable internet search, the model will use the search result as the reference information in the text generation process, but the model will "judge by itself" whether to use the internet search result based on its internal logic. - * False (default): Internet search is disabled. - */ - private Boolean enableSearch = false; - - /** - * [text|message], defaults to text, when it is message, - * the output refers to the message result example. - * It is recommended to prioritize the use of message format. - */ - private String resultFormat = GenerationParam.ResultFormat.MESSAGE; - - /** - * Control the streaming output mode, that is, the content will contain the content has been output; - * set to True, will open the incremental output mode, the output will not contain the content has been output, - * you need to splice the whole output, refer to the streaming output sample code. - */ - private Boolean incrementalOutput = false; - - /** - * A list of tools that the model can optionally call. - * Currently only functions are supported, and even if multiple functions are entered, the model will only select one to generate the result. - */ - private List tools; - - @Override - public Float getTemperature() { - - return this.temperature.floatValue(); - } - - public void setTemperature(Float temperature) { - - this.temperature = temperature.doubleValue(); - } - - @Override - public Float getTopP() { - - return this.topP.floatValue(); - } - - public void setTopP(Float topP) { - - this.topP = topP.doubleValue(); - } - - @Override - public Integer getTopK() { - - return this.topK; - } - - public void setTopK(Integer topK) { - - this.topK = topK; - } - - public String getModel() { - - return model; - } - - public void setModel(String model) { - - this.model = model; - } - - public Integer getSeed() { - - return seed; - } - - public String getResultFormat() { - - return resultFormat; - } - - public void setResultFormat(String resultFormat) { - - this.resultFormat = resultFormat; - } - - public void setSeed(Integer seed) { - - this.seed = seed; - } - - public Integer getMaxTokens() { - - return maxTokens; - } - - public void setMaxTokens(Integer maxTokens) { - - this.maxTokens = maxTokens; - } - - public Float getRepetitionPenalty() { - - return repetitionPenalty.floatValue(); - } - - public void setRepetitionPenalty(Float repetitionPenalty) { - - this.repetitionPenalty = repetitionPenalty.doubleValue(); - } - - public List getStop() { - - return stop; - } - - public void setStop(List stop) { - - this.stop = stop; - } - - public Boolean getStream() { - - return stream; - } - - public void setStream(Boolean stream) { - - this.stream = stream; - } - - public Boolean getEnableSearch() { - - return enableSearch; - } - - public void setEnableSearch(Boolean enableSearch) { - - this.enableSearch = enableSearch; - } - - public Boolean getIncrementalOutput() { - - return incrementalOutput; - } - - public void setIncrementalOutput(Boolean incrementalOutput) { - - this.incrementalOutput = incrementalOutput; - } - - public List getTools() { - - return tools; - } - - public void setTools(List tools) { - - this.tools = tools; - } - - private List functionCallbacks = new ArrayList<>(); - - private Set functions = new HashSet<>(); - - @Override - public List getFunctionCallbacks() { - - return this.functionCallbacks; - } - - @Override - public void setFunctionCallbacks(List functionCallbacks) { - - this.functionCallbacks = functionCallbacks; - } - - @Override - public Set getFunctions() { - - return this.functions; - } - - @Override - public void setFunctions(Set functions) { - - this.functions = functions; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - TongYiChatOptions that = (TongYiChatOptions) o; - - return Objects.equals(model, that.model) - && Objects.equals(seed, that.seed) - && Objects.equals(maxTokens, that.maxTokens) - && Objects.equals(topP, that.topP) - && Objects.equals(topK, that.topK) - && Objects.equals(repetitionPenalty, that.repetitionPenalty) - && Objects.equals(temperature, that.temperature) - && Objects.equals(stop, that.stop) - && Objects.equals(stream, that.stream) - && Objects.equals(enableSearch, that.enableSearch) - && Objects.equals(resultFormat, that.resultFormat) - && Objects.equals(incrementalOutput, that.incrementalOutput) - && Objects.equals(tools, that.tools) - && Objects.equals(functionCallbacks, that.functionCallbacks) - && Objects.equals(functions, that.functions); - } - - @Override - public int hashCode() { - - return Objects.hash( - model, - seed, - maxTokens, - topP, - topK, - repetitionPenalty, - temperature, - stop, - stream, - enableSearch, - resultFormat, - incrementalOutput, - tools, - functionCallbacks, - functions - ); - } - - @Override - public String toString() { - - final StringBuilder sb = new StringBuilder("TongYiChatOptions{"); - - sb.append(", model='").append(model).append('\''); - sb.append(", seed=").append(seed); - sb.append(", maxTokens=").append(maxTokens); - sb.append(", topP=").append(topP); - sb.append(", topK=").append(topK); - sb.append(", repetitionPenalty=").append(repetitionPenalty); - sb.append(", temperature=").append(temperature); - sb.append(", stop=").append(stop); - sb.append(", stream=").append(stream); - sb.append(", enableSearch=").append(enableSearch); - sb.append(", resultFormat='").append(resultFormat).append('\''); - sb.append(", incrementalOutput=").append(incrementalOutput); - sb.append(", tools=").append(tools); - sb.append(", functionCallbacks=").append(functionCallbacks); - sb.append(", functions=").append(functions); - sb.append('}'); - - return sb.toString(); - } - - public static Builder builder() { - - return new Builder(); - } - - public static class Builder { - - protected TongYiChatOptions options; - - public Builder() { - - this.options = new TongYiChatOptions(); - } - - public Builder(TongYiChatOptions options) { - - this.options = options; - } - - public Builder withModel(String model) { - this.options.model = model; - return this; - } - - public Builder withMaxTokens(Integer maxTokens) { - this.options.maxTokens = maxTokens; - return this; - } - - public Builder withResultFormat(String rf) { - this.options.resultFormat = rf; - return this; - } - - public Builder withEnableSearch(Boolean enableSearch) { - this.options.enableSearch = enableSearch; - return this; - } - - public Builder withFunctionCallbacks(List functionCallbacks) { - this.options.functionCallbacks = functionCallbacks; - return this; - } - - public Builder withFunctions(Set functionNames) { - Assert.notNull(functionNames, "Function names must not be null"); - this.options.functions = functionNames; - return this; - } - - public Builder withFunction(String functionName) { - Assert.hasText(functionName, "Function name must not be empty"); - this.options.functions.add(functionName); - return this; - } - - public Builder withSeed(Integer seed) { - this.options.seed = seed; - return this; - } - - public Builder withStop(List stop) { - this.options.stop = stop; - return this; - } - - public Builder withTemperature(Double temperature) { - this.options.temperature = temperature; - return this; - } - - public Builder withTopP(Double topP) { - this.options.topP = topP; - return this; - } - - public Builder withTopK(Integer topK) { - this.options.topK = topK; - return this; - } - - public Builder withRepetitionPenalty(Double repetitionPenalty) { - this.options.repetitionPenalty = repetitionPenalty; - return this; - } - - public TongYiChatOptions build() { - - return this.options; - } - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java deleted file mode 100644 index 9a7e85afb..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.chat; - -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationParam; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiChatProperties.CONFIG_PREFIX) -public class TongYiChatProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "chat"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_DEPLOYMENT_NAME = Generation.Models.QWEN_TURBO; - - /** - * Default temperature speed. - */ - private static final Double DEFAULT_TEMPERATURE = 0.8; - - /** - * Enable TongYiQWEN ai chat client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiChatOptions options = TongYiChatOptions.builder() - .withModel(DEFAULT_DEPLOYMENT_NAME) - .withTemperature(DEFAULT_TEMPERATURE) - .withEnableSearch(true) - .withResultFormat(GenerationParam.ResultFormat.MESSAGE) - .build(); - - public TongYiChatOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiChatOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java deleted file mode 100644 index 6fe77bd30..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.common.constants; - -/** - * @author yuluo - * @author yuluo - */ - -public final class TongYiConstants { - - private TongYiConstants() { - } - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String SCA_AI_CONFIGURATION = "spring.cloud.ai.tongyi."; - - /** - * Spring Cloud Alibaba AI constants prefix. - */ - public static final String SCA_AI = "SPRING_CLOUD_ALIBABA_"; - - /** - * TongYi AI apikey env name. - */ - public static final String SCA_AI_TONGYI_API_KEY = SCA_AI + "TONGYI_API_KEY"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java deleted file mode 100644 index 18e14ad76..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.common.exception; - -/** - * TongYi models runtime exception. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiException extends RuntimeException { - - public TongYiException(String message) { - - super(message); - } - - public TongYiException(String message, Throwable cause) { - - super(message, cause); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java deleted file mode 100644 index 5e2e577e9..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.common.exception; - -/** - * TongYi models images exception. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesException extends TongYiException { - - public TongYiImagesException(String message) { - - super(message); - } - - public TongYiImagesException(String message, Throwable cause) { - - super(message, cause); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java deleted file mode 100644 index 2ad81258f..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.embedding; - -import com.alibaba.dashscope.embeddings.TextEmbeddingParam; -import org.springframework.ai.embedding.EmbeddingOptions; - -import java.util.List; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public final class TongYiEmbeddingOptions implements EmbeddingOptions { - - private List texts; - - private TextEmbeddingParam.TextType textType; - - public List getTexts() { - return texts; - } - - public void setTexts(List texts) { - this.texts = texts; - } - - public TextEmbeddingParam.TextType getTextType() { - return textType; - } - - public void setTextType(TextEmbeddingParam.TextType textType) { - this.textType = textType; - } - - public static Builder builder() { - return new Builder(); - } - - public final static class Builder { - - private final TongYiEmbeddingOptions options; - - private Builder() { - this.options = new TongYiEmbeddingOptions(); - } - - public Builder withtexts(List texts) { - - options.setTexts(texts); - return this; - } - - public Builder withtextType(TextEmbeddingParam.TextType textType) { - - options.setTextType(textType); - return this; - } - - public TongYiEmbeddingOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java deleted file mode 100644 index 99a356fe8..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.embedding; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.alibaba.dashscope.embeddings.TextEmbeddingParam; -import com.alibaba.dashscope.embeddings.TextEmbeddingResult; -import com.alibaba.dashscope.embeddings.TextEmbeddingResultItem; -import com.alibaba.dashscope.exception.InputRequiredException; -import com.alibaba.dashscope.exception.NoApiKeyException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.AbstractEmbeddingModel; -import org.springframework.ai.embedding.Embedding; -import org.springframework.ai.embedding.EmbeddingRequest; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.util.Assert; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * {@link TongYiTextEmbeddingModel} implementation for {@literal Alibaba DashScope}. - * - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel { - - private final Logger logger = LoggerFactory.getLogger(TongYiTextEmbeddingModel.class); - - /** - * TongYi Text Embedding client. - */ - private final TextEmbedding textEmbedding; - - /** - * {@link MetadataMode}. - */ - private final MetadataMode metadataMode; - - private final TongYiEmbeddingOptions defaultOptions; - - public TongYiTextEmbeddingModel(TextEmbedding textEmbedding) { - - this(textEmbedding, MetadataMode.EMBED); - } - - public TongYiTextEmbeddingModel(TextEmbedding textEmbedding, MetadataMode metadataMode) { - - this(textEmbedding, metadataMode, - TongYiEmbeddingOptions.builder() - .withtextType(TextEmbeddingParam.TextType.DOCUMENT) - .build() - ); - } - - public TongYiTextEmbeddingModel( - TextEmbedding textEmbedding, - MetadataMode metadataMode, - TongYiEmbeddingOptions options - ) { - Assert.notNull(textEmbedding, "textEmbedding must not be null"); - Assert.notNull(metadataMode, "Metadata mode must not be null"); - Assert.notNull(options, "TongYiEmbeddingOptions must not be null"); - - this.metadataMode = metadataMode; - this.textEmbedding = textEmbedding; - this.defaultOptions = options; - } - - public TongYiEmbeddingOptions getDefaultOptions() { - - return this.defaultOptions; - } - - @Override - public List embed(Document document) { - - return this.call( - new EmbeddingRequest( - ListUtil.of(document.getFormattedContent(this.metadataMode)), - null) - ).getResults().stream() - .map(Embedding::getOutput) - .flatMap(List::stream) - .toList(); - } - - @Override - public EmbeddingResponse call(EmbeddingRequest request) { - - TextEmbeddingParam embeddingParams = toEmbeddingParams(request); - logger.debug("Embedding request: {}", embeddingParams); - TextEmbeddingResult resp; - - try { - resp = textEmbedding.call(embeddingParams); - } - catch (NoApiKeyException e) { - throw new TongYiException(e.getMessage()); - } - - return genEmbeddingResp(resp); - } - - private EmbeddingResponse genEmbeddingResp(TextEmbeddingResult result) { - - return new EmbeddingResponse( - genEmbeddingList(result.getOutput().getEmbeddings()), - TongYiTextEmbeddingResponseMetadata.from(result.getUsage()) - ); - } - - private List genEmbeddingList(List embeddings) { - - return embeddings.stream() - .map(embedding -> - new Embedding( - embedding.getEmbedding(), - embedding.getTextIndex() - )) - .collect(Collectors.toList()); - } - - /** - * We recommend setting the model parameters by passing the embedding parameters through the code; - * yml configuration compatibility is not considered here. - * It is not recommended that users set parameters from yml, - * as this reduces the flexibility of the configuration. - * Because the model name keeps changing, strings are used here and constants are undefined: - * Model list: Text Embedding Model List - * @param requestOptions Client params. {@link EmbeddingRequest} - * @return {@link TextEmbeddingParam} - */ - private TextEmbeddingParam toEmbeddingParams(EmbeddingRequest requestOptions) { - - TextEmbeddingParam tongYiEmbeddingParams = TextEmbeddingParam.builder() - .texts(requestOptions.getInstructions()) - .textType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : TextEmbeddingParam.TextType.DOCUMENT) - .model("text-embedding-v1") - .build(); - - try { - tongYiEmbeddingParams.validate(); - } - catch (InputRequiredException e) { - throw new TongYiException(e.getMessage()); - } - - return tongYiEmbeddingParams; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java deleted file mode 100644 index 02196f41d..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.embedding; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiTextEmbeddingProperties.CONFIG_PREFIX) -public class TongYiTextEmbeddingProperties { - - /** - * Prefix of TongYi Text Embedding properties. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "embedding"; - - private boolean enabled = true; - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java deleted file mode 100644 index 55dbb339d..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; -import com.alibaba.dashscope.exception.NoApiKeyException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.image.*; -import org.springframework.util.Assert; - -import java.io.ByteArrayOutputStream; -import java.net.URL; -import java.util.Base64; -import java.util.stream.Collectors; - -import static com.alibaba.cloud.ai.tongyi.metadata.TongYiImagesResponseMetadata.from; - -/** - * TongYiImagesClient is a class that implements the ImageClient interface. It provides a - * client for calling the TongYi AI image generation API. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesModel implements ImageModel { - - private final Logger logger = LoggerFactory.getLogger(TongYiImagesModel.class); - - /** - * Gen Images API. - */ - private final ImageSynthesis imageSynthesis; - - /** - * TongYi Gen images properties. - */ - private TongYiImagesOptions defaultOptions; - - /** - * Adapt TongYi images api size properties. - */ - private final String sizeConnection = "*"; - - /** - * Get default images options. - * - * @return Default TongYiImagesOptions. - */ - public TongYiImagesOptions getDefaultOptions() { - - return this.defaultOptions; - } - - /** - * TongYiImagesClient constructor. - * @param imageSynthesis the image synthesis - * {@link ImageSynthesis} - */ - public TongYiImagesModel(ImageSynthesis imageSynthesis) { - - this(imageSynthesis, TongYiImagesOptions. - builder() - .withModel(ImageSynthesis.Models.WANX_V1) - .withN(1) - .build() - ); - } - - /** - * TongYiImagesClient constructor. - * @param imageSynthesis {@link ImageSynthesis} - * @param imagesOptions {@link TongYiImagesOptions} - */ - public TongYiImagesModel(ImageSynthesis imageSynthesis, TongYiImagesOptions imagesOptions) { - - Assert.notNull(imageSynthesis, "ImageSynthesis must not be null"); - Assert.notNull(imagesOptions, "TongYiImagesOptions must not be null"); - - this.imageSynthesis = imageSynthesis; - this.defaultOptions = imagesOptions; - } - - /** - * Call the TongYi images service. - * @param imagePrompt the image prompt. - * @return the image response. - * {@link ImageSynthesis#call(ImageSynthesisParam)} - */ - @Override - public ImageResponse call(ImagePrompt imagePrompt) { - - ImageSynthesisResult result; - String prompt = imagePrompt.getInstructions().get(0).getText(); - var imgParams = ImageSynthesisParam.builder() - .prompt("") - .model(ImageSynthesis.Models.WANX_V1) - .build(); - - if (this.defaultOptions != null) { - - imgParams = merge(this.defaultOptions); - } - - if (imagePrompt.getOptions() != null) { - - imgParams = merge(toTingYiImageOptions(imagePrompt.getOptions())); - } - imgParams.setPrompt(prompt); - - try { - result = imageSynthesis.call(imgParams); - } - catch (NoApiKeyException e) { - - logger.error("TongYi models gen images failed: {}.", e.getMessage()); - throw new TongYiImagesException(e.getMessage()); - } - - return convert(result); - } - - public ImageSynthesisParam merge(TongYiImagesOptions target) { - - var builder = ImageSynthesisParam.builder(); - - builder.model(this.defaultOptions.getModel() != null ? this.defaultOptions.getModel() : target.getModel()); - builder.n(this.defaultOptions.getN() != null ? this.defaultOptions.getN() : target.getN()); - builder.size((this.defaultOptions.getHeight() != null && this.defaultOptions.getWidth() != null) - ? this.defaultOptions.getHeight() + "*" + this.defaultOptions.getWidth() - : target.getHeight() + "*" + target.getWidth() - ); - - // prompt is marked non-null but is null. - builder.prompt(""); - - return builder.build(); - } - - private ImageResponse convert(ImageSynthesisResult result) { - - return new ImageResponse( - result.getOutput().getResults().stream() - .flatMap(value -> value.entrySet().stream()) - .map(entry -> { - String key = entry.getKey(); - String value = entry.getValue(); - try { - String base64Image = convertImageToBase64(value); - Image image = new Image(value, base64Image); - return new ImageGeneration(image); - } - catch (Exception e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()), - from(result) - ); - } - - public TongYiImagesOptions toTingYiImageOptions(ImageOptions runtimeImageOptions) { - - var builder = TongYiImagesOptions.builder(); - - if (runtimeImageOptions != null) { - if (runtimeImageOptions.getN() != null) { - - builder.withN(runtimeImageOptions.getN()); - } - if (runtimeImageOptions.getModel() != null) { - - builder.withModel(runtimeImageOptions.getModel()); - } - if (runtimeImageOptions.getHeight() != null) { - - builder.withHeight(runtimeImageOptions.getHeight()); - } - if (runtimeImageOptions.getWidth() != null) { - - builder.withWidth(runtimeImageOptions.getWidth()); - } - - // todo ImagesParams. - } - - return builder.build(); - } - - /** - * Convert image to base64. - * @param imageUrl the image url. - * @return the base64 image. - * @throws Exception the exception. - */ - public String convertImageToBase64(String imageUrl) throws Exception { - - var url = new URL(imageUrl); - var inputStream = url.openStream(); - var outputStream = new ByteArrayOutputStream(); - var buffer = new byte[4096]; - int bytesRead; - - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - - var imageBytes = outputStream.toByteArray(); - - String base64Image = Base64.getEncoder().encodeToString(imageBytes); - - inputStream.close(); - outputStream.close(); - - return base64Image; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java deleted file mode 100644 index 412b07be0..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import org.springframework.ai.image.ImageOptions; - -import java.util.Objects; - -/** - * TongYi Image API options. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesOptions implements ImageOptions { - - /** - * Specify the model name, currently only wanx-v1 is supported. - */ - private String model = ImageSynthesis.Models.WANX_V1; - - /** - * Gen images number. - */ - private Integer n; - - /** - * The width of the generated images. - */ - private Integer width = 1024; - - /** - * The height of the generated images. - */ - private Integer height = 1024; - - @Override - public Integer getN() { - - return this.n; - } - - @Override - public String getModel() { - - return this.model; - } - - @Override - public Integer getWidth() { - - return this.width; - } - - @Override - public Integer getHeight() { - - return this.height; - } - - @Override - public String getResponseFormat() { - - throw new TongYiImagesException("unimplemented!"); - } - - public void setModel(String model) { - - this.model = model; - } - - public void setN(Integer n) { - - this.n = n; - } - - public void setWidth(Integer width) { - - this.width = width; - } - - public void setHeight(Integer height) { - - this.height = height; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - - return true; - } - if (o == null || getClass() != o.getClass()) { - - return false; - } - - TongYiImagesOptions that = (TongYiImagesOptions) o; - - return Objects.equals(model, that.model) - && Objects.equals(n, that.n) - && Objects.equals(width, that.width) - && Objects.equals(height, that.height); - } - - @Override - public int hashCode() { - - return Objects.hash(model, n, width, height); - } - - @Override - public String toString() { - - final StringBuilder sb = new StringBuilder("TongYiImagesOptions{"); - - sb.append("model='").append(model).append('\''); - sb.append(", n=").append(n); - sb.append(", width=").append(width); - sb.append(", height=").append(height); - sb.append('}'); - - return sb.toString(); - } - - public static Builder builder() { - return new Builder(); - } - - public final static class Builder { - - private final TongYiImagesOptions options; - - private Builder() { - this.options = new TongYiImagesOptions(); - } - - public Builder withN(Integer n) { - - options.setN(n); - return this; - } - - public Builder withModel(String model) { - - options.setModel(model); - return this; - } - - public Builder withWidth(Integer width) { - - options.setWidth(width); - return this; - } - - public Builder withHeight(Integer height) { - - options.setHeight(height); - return this; - } - - public TongYiImagesOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java deleted file mode 100644 index 23f4d7f33..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * TongYi Image API properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiImagesProperties.CONFIG_PREFIX) -public class TongYiImagesProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "images"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_IMAGES_MODEL_NAME = ImageSynthesis.Models.WANX_V1; - - /** - * Enable TongYiQWEN ai images client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiImagesOptions options = TongYiImagesOptions.builder() - .withModel(DEFAULT_IMAGES_MODEL_NAME) - .withN(1) - .build(); - - public TongYiImagesOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiImagesOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java deleted file mode 100644 index 3918313c0..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import org.springframework.ai.chat.metadata.ChatResponseMetadata; -import org.springframework.ai.chat.metadata.PromptMetadata; -import org.springframework.ai.chat.metadata.Usage; -import org.springframework.util.Assert; - -import java.util.HashMap; - - - -/** - * {@link ChatResponseMetadata} implementation for {@literal Alibaba DashScope}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAiChatResponseMetadata extends HashMap implements ChatResponseMetadata { - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }"; - - @SuppressWarnings("all") - public static TongYiAiChatResponseMetadata from(GenerationResult chatCompletions, - PromptMetadata promptFilterMetadata) { - - Assert.notNull(chatCompletions, "Alibaba ai ChatCompletions must not be null"); - String id = chatCompletions.getRequestId(); - TongYiAiUsage usage = TongYiAiUsage.from(chatCompletions); - - return new TongYiAiChatResponseMetadata( - id, - usage, - promptFilterMetadata - ); - } - - private final String id; - - private final Usage usage; - - private final PromptMetadata promptMetadata; - - protected TongYiAiChatResponseMetadata(String id, TongYiAiUsage usage, PromptMetadata promptMetadata) { - - this.id = id; - this.usage = usage; - this.promptMetadata = promptMetadata; - } - - public String getId() { - return this.id; - } - - @Override - public Usage getUsage() { - return this.usage; - } - - @Override - public PromptMetadata getPromptMetadata() { - return this.promptMetadata; - } - - @Override - public String toString() { - - return AI_METADATA_STRING.formatted(getClass().getTypeName(), getId(), getUsage(), getRateLimit()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java deleted file mode 100644 index da086e502..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import com.alibaba.dashscope.aigc.generation.GenerationUsage; -import org.springframework.ai.chat.metadata.Usage; -import org.springframework.util.Assert; - -/** - * {@link Usage} implementation for {@literal Alibaba DashScope}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAiUsage implements Usage { - - private final GenerationUsage usage; - - public TongYiAiUsage(GenerationUsage usage) { - - Assert.notNull(usage, "GenerationUsage must not be null"); - this.usage = usage; - } - - public static TongYiAiUsage from(GenerationResult chatCompletions) { - - Assert.notNull(chatCompletions, "ChatCompletions must not be null"); - return from(chatCompletions.getUsage()); - } - - public static TongYiAiUsage from(GenerationUsage usage) { - - return new TongYiAiUsage(usage); - } - - protected GenerationUsage getUsage() { - - return this.usage; - } - - @Override - public Long getPromptTokens() { - - throw new UnsupportedOperationException("Unimplemented method 'getPromptTokens'"); - } - - @Override - public Long getGenerationTokens() { - - return this.getUsage().getOutputTokens().longValue(); - } - - @Override - public Long getTotalTokens() { - - return this.getUsage().getTotalTokens().longValue(); - } - - @Override - public String toString() { - - return this.getUsage().toString(); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java deleted file mode 100644 index 11b38f6eb..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisTaskMetrics; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisUsage; -import org.springframework.ai.image.ImageResponseMetadata; -import org.springframework.util.Assert; - -import java.util.HashMap; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesResponseMetadata extends HashMap implements ImageResponseMetadata { - - private final Long created; - - private String taskId; - - private ImageSynthesisTaskMetrics metrics; - - private ImageSynthesisUsage usage; - - public static TongYiImagesResponseMetadata from(ImageSynthesisResult synthesisResult) { - - Assert.notNull(synthesisResult, "TongYiAiImageResponse must not be null"); - - return new TongYiImagesResponseMetadata( - System.currentTimeMillis(), - synthesisResult.getOutput().getTaskMetrics(), - synthesisResult.getOutput().getTaskId(), - synthesisResult.getUsage() - ); - } - - protected TongYiImagesResponseMetadata( - Long created, - ImageSynthesisTaskMetrics metrics, - String taskId, - ImageSynthesisUsage usage - ) { - - this.taskId = taskId; - this.metrics = metrics; - this.created = created; - this.usage = usage; - } - - public ImageSynthesisUsage getUsage() { - return usage; - } - - public void setUsage(ImageSynthesisUsage usage) { - this.usage = usage; - } - - @Override - public Long getCreated() { - return created; - } - - public String getTaskId() { - return taskId; - } - - public void setTaskId(String taskId) { - this.taskId = taskId; - } - - public ImageSynthesisTaskMetrics getMetrics() { - return metrics; - } - - void setMetrics(ImageSynthesisTaskMetrics metrics) { - this.metrics = metrics; - } - - - public Long created() { - return this.created; - } - - @Override - public String toString() { - return "TongYiImagesResponseMetadata {" + "created=" + created + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - - return true; - } - if (o == null || getClass() != o.getClass()) { - - return false; - } - - TongYiImagesResponseMetadata that = (TongYiImagesResponseMetadata) o; - - return Objects.equals(created, that.created) - && Objects.equals(taskId, that.taskId) - && Objects.equals(metrics, that.metrics); - } - - @Override - public int hashCode() { - return Objects.hash(created, taskId, metrics); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java deleted file mode 100644 index 414154bf8..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.embeddings.TextEmbeddingUsage; -import org.springframework.ai.embedding.EmbeddingResponseMetadata; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public class TongYiTextEmbeddingResponseMetadata extends EmbeddingResponseMetadata { - - private Integer totalTokens; - - protected TongYiTextEmbeddingResponseMetadata(Integer totalTokens) { - - this.totalTokens = totalTokens; - } - - public static TongYiTextEmbeddingResponseMetadata from(TextEmbeddingUsage usage) { - - return new TongYiTextEmbeddingResponseMetadata(usage.getTotalTokens()); - } - - public Integer getTotalTokens() { - - return totalTokens; - } - - public void setTotalTokens(Integer totalTokens) { - - this.totalTokens = totalTokens; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java deleted file mode 100644 index 8647e3f0b..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata.audio; - -import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisUsage; -import com.alibaba.dashscope.audio.tts.timestamp.Sentence; -import org.springframework.ai.chat.metadata.EmptyRateLimit; -import org.springframework.ai.chat.metadata.RateLimit; -import org.springframework.ai.model.ResponseMetadata; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -import java.util.HashMap; - - - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechResponseMetadata extends HashMap implements ResponseMetadata { - - private SpeechSynthesisUsage usage; - - private String requestId; - - private Sentence time; - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, requestsLimit: %2$s }"; - - /** - * NULL objects. - */ - public static final TongYiAudioSpeechResponseMetadata NULL = new TongYiAudioSpeechResponseMetadata() { - }; - - public static TongYiAudioSpeechResponseMetadata from(SpeechSynthesisResult result) { - - Assert.notNull(result, "TongYi AI speech must not be null"); - TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata(); - - - - return speechResponseMetadata; - } - - public static TongYiAudioSpeechResponseMetadata from(String result) { - - Assert.notNull(result, "TongYi AI speech must not be null"); - TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata(); - - return speechResponseMetadata; - } - - @Nullable - private RateLimit rateLimit; - - public TongYiAudioSpeechResponseMetadata() { - - this(null); - } - - public TongYiAudioSpeechResponseMetadata(@Nullable RateLimit rateLimit) { - - this.rateLimit = rateLimit; - } - - @Nullable - public RateLimit getRateLimit() { - - RateLimit rateLimit = this.rateLimit; - return rateLimit != null ? rateLimit : new EmptyRateLimit(); - } - - public TongYiAudioSpeechResponseMetadata withRateLimit(RateLimit rateLimit) { - - this.rateLimit = rateLimit; - return this; - } - - public TongYiAudioSpeechResponseMetadata withUsage(SpeechSynthesisUsage usage) { - - this.usage = usage; - return this; - } - - public TongYiAudioSpeechResponseMetadata withRequestId(String id) { - - this.requestId = id; - return this; - } - - public TongYiAudioSpeechResponseMetadata withSentence(Sentence sentence) { - - this.time = sentence; - return this; - } - - public SpeechSynthesisUsage getUsage() { - return usage; - } - - public String getRequestId() { - return requestId; - } - - public Sentence getTime() { - return time; - } - - @Override - public String toString() { - return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java deleted file mode 100644 index 32d5d63c2..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata.audio; - -import org.springframework.ai.model.ResultMetadata; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public interface TongYiAudioTranscriptionMetadata extends ResultMetadata { - - /** - * A constant instance of {@link TongYiAudioTranscriptionMetadata} that represents a null or empty metadata. - */ - TongYiAudioTranscriptionMetadata NULL = TongYiAudioTranscriptionMetadata.create(); - - /** - * Factory method for creating a new instance of {@link TongYiAudioTranscriptionMetadata}. - * @return a new instance of {@link TongYiAudioTranscriptionMetadata} - */ - static TongYiAudioTranscriptionMetadata create() { - return new TongYiAudioTranscriptionMetadata() { - }; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java deleted file mode 100644 index 36525d5f5..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2023-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.cloud.ai.tongyi.metadata.audio; - -import com.alibaba.dashscope.audio.asr.transcription.TranscriptionResult; -import com.google.gson.JsonObject; -import org.springframework.ai.chat.metadata.EmptyRateLimit; -import org.springframework.ai.chat.metadata.RateLimit; -import org.springframework.ai.model.ResponseMetadata; -import org.springframework.util.Assert; - -import javax.annotation.Nullable; -import java.util.HashMap; - - - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionResponseMetadata extends HashMap implements ResponseMetadata { - - @Nullable - private RateLimit rateLimit; - - private JsonObject usage; - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, rateLimit: %4$s }"; - - /** - * NULL objects. - */ - public static final TongYiAudioTranscriptionResponseMetadata NULL = new TongYiAudioTranscriptionResponseMetadata() { - }; - - protected TongYiAudioTranscriptionResponseMetadata() { - - this(null, new JsonObject()); - } - - protected TongYiAudioTranscriptionResponseMetadata(JsonObject usage) { - - this(null, usage); - } - - protected TongYiAudioTranscriptionResponseMetadata(@Nullable RateLimit rateLimit, JsonObject usage) { - - this.rateLimit = rateLimit; - this.usage = usage; - } - - public static TongYiAudioTranscriptionResponseMetadata from(TranscriptionResult result) { - - Assert.notNull(result, "TongYi Transcription must not be null"); - return new TongYiAudioTranscriptionResponseMetadata(result.getUsage()); - } - - @Nullable - public RateLimit getRateLimit() { - - return this.rateLimit != null ? this.rateLimit : new EmptyRateLimit(); - } - - public void setRateLimit(@Nullable RateLimit rateLimit) { - this.rateLimit = rateLimit; - } - - public JsonObject getUsage() { - return usage; - } - - public void setUsage(JsonObject usage) { - this.usage = usage; - } - - @Override - public String toString() { - - return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java deleted file mode 100644 index a72d50c4a..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 - 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.autoconfigure.vectorstore.redis; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.RedisVectorStore; -import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import redis.clients.jedis.JedisPooled; - -/** - * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 - * - * TODO 这个官方,有说啥时候 fix 哇? - * TODO 看着是列在1.0.0-M2版本 - * - * @author Christian Tzolov - * @author Eddú Meléndez - */ -@AutoConfiguration(after = RedisAutoConfiguration.class) -@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class}) -@ConditionalOnBean(JedisConnectionFactory.class) -@EnableConfigurationProperties(RedisVectorStoreProperties.class) -public class RedisVectorStoreAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, - JedisConnectionFactory jedisConnectionFactory) { - - var config = RedisVectorStoreConfig.builder() - .withIndexName(properties.getIndex()) - .withPrefix(properties.getPrefix()) - .build(); - - return new RedisVectorStore(config, embeddingModel, - new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), - properties.isInitializeSchema()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java deleted file mode 100644 index de80401ed..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright 2023 - 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.vectorstore; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.Pipeline; -import redis.clients.jedis.json.Path2; -import redis.clients.jedis.search.*; -import redis.clients.jedis.search.Schema.FieldType; -import redis.clients.jedis.search.schemafields.*; -import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; - -import java.text.MessageFormat; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * The RedisVectorStore is for managing and querying vector data in a Redis database. It - * offers functionalities like adding, deleting, and performing similarity searches on - * documents. - * - * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and - * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for - * efficient similarity searches. Additionally, it allows for custom metadata fields in - * the documents to be stored alongside the vector and content data. - * - * This class requires a RedisVectorStoreConfig configuration object for initialization, - * which includes settings like Redis URI, index name, field names, and vector algorithms. - * It also requires an EmbeddingModel to convert documents into embeddings before storing - * them. - * - * @author Julien Ruaux - * @author Christian Tzolov - * @author Eddú Meléndez - * @see VectorStore - * @see RedisVectorStoreConfig - * @see EmbeddingModel - */ -public class RedisVectorStore implements VectorStore, InitializingBean { - - public enum Algorithm { - - FLAT, HSNW - - } - - public record MetadataField(String name, FieldType fieldType) { - - public static MetadataField text(String name) { - return new MetadataField(name, FieldType.TEXT); - } - - public static MetadataField numeric(String name) { - return new MetadataField(name, FieldType.NUMERIC); - } - - public static MetadataField tag(String name) { - return new MetadataField(name, FieldType.TAG); - } - - } - - /** - * Configuration for the Redis vector store. - */ - public static final class RedisVectorStoreConfig { - - private final String indexName; - - private final String prefix; - - private final String contentFieldName; - - private final String embeddingFieldName; - - private final Algorithm vectorAlgorithm; - - private final List metadataFields; - - private RedisVectorStoreConfig() { - this(builder()); - } - - private RedisVectorStoreConfig(Builder builder) { - this.indexName = builder.indexName; - this.prefix = builder.prefix; - this.contentFieldName = builder.contentFieldName; - this.embeddingFieldName = builder.embeddingFieldName; - this.vectorAlgorithm = builder.vectorAlgorithm; - this.metadataFields = builder.metadataFields; - } - - /** - * Start building a new configuration. - * @return The entry point for creating a new configuration. - */ - public static Builder builder() { - - return new Builder(); - } - - /** - * {@return the default config} - */ - public static RedisVectorStoreConfig defaultConfig() { - - return builder().build(); - } - - public static class Builder { - - private String indexName = DEFAULT_INDEX_NAME; - - private String prefix = DEFAULT_PREFIX; - - private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME; - - private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME; - - private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM; - - private List metadataFields = new ArrayList<>(); - - private Builder() { - } - - /** - * Configures the Redis index name to use. - * @param name the index name to use - * @return this builder - */ - public Builder withIndexName(String name) { - this.indexName = name; - return this; - } - - /** - * Configures the Redis key prefix to use (default: "embedding:"). - * @param prefix the prefix to use - * @return this builder - */ - public Builder withPrefix(String prefix) { - this.prefix = prefix; - return this; - } - - /** - * Configures the Redis content field name to use. - * @param name the content field name to use - * @return this builder - */ - public Builder withContentFieldName(String name) { - this.contentFieldName = name; - return this; - } - - /** - * Configures the Redis embedding field name to use. - * @param name the embedding field name to use - * @return this builder - */ - public Builder withEmbeddingFieldName(String name) { - this.embeddingFieldName = name; - return this; - } - - /** - * Configures the Redis vector algorithmto use. - * @param algorithm the vector algorithm to use - * @return this builder - */ - public Builder withVectorAlgorithm(Algorithm algorithm) { - this.vectorAlgorithm = algorithm; - return this; - } - - public Builder withMetadataFields(MetadataField... fields) { - return withMetadataFields(Arrays.asList(fields)); - } - - public Builder withMetadataFields(List fields) { - this.metadataFields = fields; - return this; - } - - /** - * {@return the immutable configuration} - */ - public RedisVectorStoreConfig build() { - - return new RedisVectorStoreConfig(this); - } - - } - - } - - private final boolean initializeSchema; - - public static final String DEFAULT_INDEX_NAME = "spring-ai-index"; - - public static final String DEFAULT_CONTENT_FIELD_NAME = "content"; - - public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding"; - - public static final String DEFAULT_PREFIX = "embedding:"; - - public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW; - - private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]"; - - private static final Path2 JSON_SET_PATH = Path2.of("$"); - - private static final String JSON_PATH_PREFIX = "$."; - - private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class); - - private static final Predicate RESPONSE_OK = Predicate.isEqual("OK"); - - private static final Predicate RESPONSE_DEL_OK = Predicate.isEqual(1l); - - private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32"; - - private static final String EMBEDDING_PARAM_NAME = "BLOB"; - - public static final String DISTANCE_FIELD_NAME = "vector_score"; - - private static final String DEFAULT_DISTANCE_METRIC = "COSINE"; - - private final JedisPooled jedis; - - private final EmbeddingModel embeddingModel; - - private final RedisVectorStoreConfig config; - - private FilterExpressionConverter filterExpressionConverter; - - public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis, - boolean initializeSchema) { - - Assert.notNull(config, "Config must not be null"); - Assert.notNull(embeddingModel, "Embedding model must not be null"); - this.initializeSchema = initializeSchema; - - this.jedis = jedis; - this.embeddingModel = embeddingModel; - this.config = config; - this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields); - } - - public JedisPooled getJedis() { - return this.jedis; - } - - @Override - public void add(List documents) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (Document document : documents) { - var embedding = this.embeddingModel.embed(document); - document.setEmbedding(embedding); - - var fields = new HashMap(); - fields.put(this.config.embeddingFieldName, embedding); - fields.put(this.config.contentFieldName, document.getContent()); - fields.putAll(document.getMetadata()); - pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny(); - if (errResponse.isPresent()) { - String message = MessageFormat.format("Could not add document: {0}", errResponse.get()); - if (logger.isErrorEnabled()) { - logger.error(message); - } - throw new RuntimeException(message); - } - } - } - - private String key(String id) { - return this.config.prefix + id; - } - - @Override - public Optional delete(List idList) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (String id : idList) { - pipeline.jsonDel(key(id)); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny(); - if (errResponse.isPresent()) { - if (logger.isErrorEnabled()) { - logger.error("Could not delete document: {}", errResponse.get()); - } - return Optional.of(false); - } - return Optional.of(true); - } - } - - @Override - public List similaritySearch(SearchRequest request) { - - Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero"); - Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1, - "The similarity score is bounded between 0 and 1; least to most similar respectively."); - - String filter = nativeExpressionFilter(request); - - String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName, - EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME); - - List returnFields = new ArrayList<>(); - this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add); - returnFields.add(this.config.embeddingFieldName); - returnFields.add(this.config.contentFieldName); - returnFields.add(DISTANCE_FIELD_NAME); - var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery())); - Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding)) - .returnFields(returnFields.toArray(new String[0])) - .setSortBy(DISTANCE_FIELD_NAME, true) - .dialect(2); - - SearchResult result = this.jedis.ftSearch(this.config.indexName, query); - return result.getDocuments() - .stream() - .filter(d -> similarityScore(d) >= request.getSimilarityThreshold()) - .map(this::toDocument) - .toList(); - } - - private Document toDocument(redis.clients.jedis.search.Document doc) { - var id = doc.getId().substring(this.config.prefix.length()); - var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName) - : null; - Map metadata = this.config.metadataFields.stream() - .map(MetadataField::name) - .filter(doc::hasProperty) - .collect(Collectors.toMap(Function.identity(), doc::getString)); - metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc)); - return new Document(id, content, metadata); - } - - private float similarityScore(redis.clients.jedis.search.Document doc) { - return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2; - } - - private String nativeExpressionFilter(SearchRequest request) { - if (request.getFilterExpression() == null) { - return "*"; - } - return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")"; - } - - @Override - public void afterPropertiesSet() { - - if (!this.initializeSchema) { - return; - } - - // If index already exists don't do anything - if (this.jedis.ftList().contains(this.config.indexName)) { - return; - } - - String response = this.jedis.ftCreate(this.config.indexName, - FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields()); - if (!RESPONSE_OK.test(response)) { - String message = MessageFormat.format("Could not create index: {0}", response); - throw new RuntimeException(message); - } - } - - private Iterable schemaFields() { - Map vectorAttrs = new HashMap<>(); - vectorAttrs.put("DIM", this.embeddingModel.dimensions()); - vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC); - vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32); - List fields = new ArrayList<>(); - fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0)); - fields.add(VectorField.builder() - .fieldName(jsonPath(this.config.embeddingFieldName)) - .algorithm(vectorAlgorithm()) - .attributes(vectorAttrs) - .as(this.config.embeddingFieldName) - .build()); - - if (!CollectionUtils.isEmpty(this.config.metadataFields)) { - for (MetadataField field : this.config.metadataFields) { - fields.add(schemaField(field)); - } - } - return fields; - } - - private SchemaField schemaField(MetadataField field) { - String fieldName = jsonPath(field.name); - switch (field.fieldType) { - case NUMERIC: - return NumericField.of(fieldName).as(field.name); - case TAG: - return TagField.of(fieldName).as(field.name); - case TEXT: - return TextField.of(fieldName).as(field.name); - default: - throw new IllegalArgumentException( - MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType)); - } - } - - private VectorAlgorithm vectorAlgorithm() { - if (config.vectorAlgorithm == Algorithm.HSNW) { - return VectorAlgorithm.HNSW; - } - return VectorAlgorithm.FLAT; - } - - private String jsonPath(String field) { - return JSON_PATH_PREFIX + field; - } - - private static float[] toFloatArray(List embeddingDouble) { - float[] embeddingFloat = new float[embeddingDouble.size()]; - int i = 0; - for (Double d : embeddingDouble) { - embeddingFloat[i++] = d.floatValue(); - } - return embeddingFloat; - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java index c85958779..7713ec468 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.ai.chat; -import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.util.ClientOptions; @@ -27,13 +26,13 @@ import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatP */ public class AzureOpenAIChatModelTests { - private final OpenAIClient openAiApi = (new OpenAIClientBuilder()) + // TODO @芋艿:晚点在调整 + private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")) - .buildClient(); + .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java index f66c54817..bc6a367ec 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -20,7 +23,18 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private final DeepSeekChatModel chatModel = new DeepSeekChatModel("sk-e94db327cc7d457d99a8de8810fc6b12"); + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DeepSeekChatModel.BASE_URL) + .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("deepseek-chat") // 模型 + .temperature(0.7) + .build()) + .build(); + + private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java new file mode 100644 index 000000000..8b02346bb --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 {@link OpenAiChatModel} 集成 Dify 测试 + * + * @author 芋道源码 + */ +public class DifyChatModelTests { + + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("http://127.0.0.1:3000") + .apiKey("app-4hy2d7fJauSbrKbzTKX1afuP") // apiKey + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java new file mode 100644 index 000000000..fc5dc3a27 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DouBaoChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class DouBaoChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DouBaoChatModel.BASE_URL) + .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("doubao-1-5-lite-32k-250115") // 模型(doubao) +// .model("deepseek-r1-250120") // 模型(deepseek) + .temperature(0.7) + .build()) + .build(); + + private final DouBaoChatModel chatModel = new DouBaoChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + // TODO @芋艿:因为使用的是 v1 api,导致 deepseek-r1-250120 不返回 think 过程,后续需要优化 + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java new file mode 100644 index 000000000..b58807b79 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 {@link OpenAiChatModel} 集成 FastGPT 测试 + * + * @author 芋道源码 + */ +public class FastGPTChatModelTests { + + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("https://cloud.fastgpt.cn/api") + .apiKey("fastgpt-aqcc61kFtF8CeaglnGAfQOCIDWwjGdJVJHv6hIlMo28otFlva2aZNK") // apiKey + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java new file mode 100644 index 000000000..e083e6be2 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java @@ -0,0 +1,110 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link HunYuanChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class HunYuanChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(HunYuanChatModel.BASE_URL) + .apiKey("sk-bcd") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 + .temperature(0.7) + .build()) + .build(); + + private final HunYuanChatModel chatModel = new HunYuanChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + + private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) + .apiKey("sk-abc") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() +// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") + .model("deepseek-r1") // 模型("deepseek-r1") + .temperature(0.7) + .build()) + .build(); + + private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel); + + @Test + @Disabled + public void testCall_deepseek() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = deepSeekChatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream_deekseek() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = deepSeekChatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java index c6b99f287..497a6fe9a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java @@ -23,10 +23,12 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaApi ollamaApi = new OllamaApi( - "http://127.0.0.1:11434"); - private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, - OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); + private final OllamaChatModel chatModel = OllamaChatModel.builder() + .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .defaultOptions(OllamaOptions.builder() + .model(OllamaModel.LLAMA3.getName()) // 模型 + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java new file mode 100644 index 000000000..80b60aea9 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.MiniMaxChatOptions; +import org.springframework.ai.minimax.api.MiniMaxApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link MiniMaxChatModel} 的集成测试 + * + * @author 芋道源码 + */ +public class MiniMaxChatModelTests { + + private final MiniMaxChatModel chatModel = new MiniMaxChatModel( + new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥 + MiniMaxChatOptions.builder() + .model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型 + .build()); + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java new file mode 100644 index 000000000..e3f644a6f --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.moonshot.MoonshotChatModel; +import org.springframework.ai.moonshot.MoonshotChatOptions; +import org.springframework.ai.moonshot.api.MoonshotApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link org.springframework.ai.moonshot.MoonshotChatModel} 的集成测试 + * + * @author 芋道源码 + */ +public class MoonshotChatModelTests { + + private final MoonshotChatModel chatModel = new MoonshotChatModel( + new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 + MoonshotChatOptions.builder() + .model("moonshot-v1-8k") // 模型 + .build()); + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java new file mode 100644 index 000000000..6bb08f701 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaOptions; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link OllamaChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class OllamaChatModelTests { + + private final OllamaChatModel chatModel = OllamaChatModel.builder() + .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .defaultOptions(OllamaOptions.builder() +// .model("qwen") // 模型(https://ollama.com/library/qwen) + .model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1) + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index 676832546..735f0a941 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -22,11 +22,16 @@ import java.util.List; */ public class OpenAIChatModelTests { - private final OpenAiApi openAiApi = new OpenAiApi( - "https://api.holdai.top", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf"); - private final OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, - OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O).build()); + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("https://api.holdai.top") + .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(OpenAiApi.ChatModel.GPT_4_O) // 模型 + .temperature(0.7) + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java new file mode 100644 index 000000000..880795fe9 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link SiliconFlowChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class SiliconFlowChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(SiliconFlowChatModel.BASE_URL) + .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(SiliconFlowChatModel.MODEL_DEFAULT) // 模型 +// .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费 +// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费 + .temperature(0.7) + .build()) + .build(); + + private final SiliconFlowChatModel chatModel = new SiliconFlowChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java index 00bc2b900..b51d556a3 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java @@ -1,12 +1,8 @@ package cn.iocoder.yudao.framework.ai.chat; -import cn.hutool.core.util.ReflectUtil; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.utils.Constants; -import org.junit.jupiter.api.BeforeEach; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -20,25 +16,20 @@ import java.util.ArrayList; import java.util.List; /** - * {@link TongYiChatModel} 集成测试类 + * {@link DashScopeChatModel} 集成测试类 * * @author fansili */ public class TongYiChatModelTests { - private final Generation generation = new Generation(); - private final TongYiChatModel chatModel = new TongYiChatModel(generation, - TongYiChatOptions.builder().withModel("qwen1.5-72b-chat").build()); - - static { - Constants.apiKey = "sk-Zsd81gZYg7"; - } - - @BeforeEach - public void before() { - // 防止 TongYiChatModel 调用空指针 - ReflectUtil.setFieldValue(chatModel, "msgManager", new MessageManager()); - } + private final DashScopeChatModel chatModel = new DashScopeChatModel( + new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), + DashScopeChatOptions.builder() + .withModel("qwen1.5-72b-chat") // 模型 +// .withModel("deepseek-r1") // 模型(deepseek-r1) +// .withModel("deepseek-v3") // 模型(deepseek-v3) +// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) + .build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java index 63f76b96d..791e75688 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -20,9 +23,18 @@ import java.util.List; */ public class XingHuoChatModelTests { - private final XingHuoChatModel chatModel = new XingHuoChatModel( - "cb6415c19d6162cda07b47316fcb0416", - "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"); + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(XingHuoChatModel.BASE_URL) + .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("generalv3.5") // 模型 + .temperature(0.7) + .build()) + .build(); + + private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java index d10a04677..baa36d86e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java @@ -14,6 +14,7 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 /** * {@link QianFanChatModel} 的集成测试 * @@ -21,11 +22,11 @@ import java.util.List; */ public class YiYanChatModelTests { - private final QianFanApi qianFanApi = new QianFanApi( - "qS8k8dYr2nXunagK4SSU8Xjj", - "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); - private final QianFanChatModel chatModel = new QianFanChatModel(qianFanApi, - QianFanChatOptions.builder().withModel(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() + private final QianFanChatModel chatModel = new QianFanChatModel( + new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 + QianFanChatOptions.builder() + .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) + .build() ); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java index 20b73bd8e..4517482a0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java @@ -22,9 +22,12 @@ import java.util.List; */ public class ZhiPuAiChatModelTests { - private final ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"); - private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, - ZhiPuAiChatOptions.builder().withModel(ZhiPuAiApi.ChatModel.GLM_4.getModelName()).build()); + private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( + new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥 + ZhiPuAiChatOptions.builder() + .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 + .build() + ); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java index c03cebb2b..383b1e5c4 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java @@ -15,8 +15,8 @@ import java.util.List; public class MidjourneyApiTests { private final MidjourneyApi midjourneyApi = new MidjourneyApi( - "https://api.holdai.top/mj", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf", + "https://api.holdai.top/mj", // 链接 + "sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17", // 密钥 null); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java index c9b07d9ff..64e921a5a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java @@ -8,7 +8,6 @@ import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.web.client.RestClient; /** * {@link OpenAiImageModel} 集成测试类 @@ -17,11 +16,10 @@ import org.springframework.web.client.RestClient; */ public class OpenAiImageModelTests { - private final OpenAiImageApi imageApi = new OpenAiImageApi( - "https://api.holdai.top", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf", - RestClient.builder()); - private final OpenAiImageModel imageModel = new OpenAiImageModel(imageApi); + private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() + .baseUrl("https://api.holdai.top") // apiKey + .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") + .build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java index 22bf6614e..c284d8c76 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java @@ -10,14 +10,15 @@ import org.springframework.ai.qianfan.api.QianFanImageApi; import static cn.iocoder.yudao.framework.ai.image.StabilityAiImageModelTests.viewImage; +// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 + /** * {@link QianFanImageModel} 集成测试类 */ public class QianFanImageTests { - private final QianFanImageApi imageApi = new QianFanImageApi( - "qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); - private final QianFanImageModel imageModel = new QianFanImageModel(imageApi); + private final QianFanImageModel imageModel = new QianFanImageModel( + new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥 @Test @Disabled @@ -25,9 +26,9 @@ public class QianFanImageTests { // 准备参数 // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 QianFanImageOptions imageOptions = QianFanImageOptions.builder() - .withModel(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) - .withWidth(1024).withHeight(1024) - .withN(1) + .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) + .width(1024).height(1024) + .N(1) .build(); ImagePrompt prompt = new ImagePrompt("good", imageOptions); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java index 7ee7e6044..5c3aa1f41 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java @@ -22,9 +22,9 @@ import java.util.concurrent.TimeUnit; */ public class StabilityAiImageModelTests { - private final StabilityAiApi imageApi = new StabilityAiApi( - "sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx"); - private final StabilityAiImageModel imageModel = new StabilityAiImageModel(imageApi); + private final StabilityAiImageModel imageModel = new StabilityAiImageModel( + new StabilityAiApi("sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx") // 密钥 + ); @Test @Disabled @@ -32,7 +32,7 @@ public class StabilityAiImageModelTests { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() .withModel("stable-diffusion-v1-6") - .withHeight(256).withWidth(256) + .withHeight(320).withWidth(320) .build(); ImagePrompt prompt = new ImagePrompt("great wall", options); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java index 41d7859c4..ad4faaa46 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java @@ -1,35 +1,30 @@ package cn.iocoder.yudao.framework.ai.image; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.utils.Constants; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.openai.OpenAiImageOptions; /** - * {@link com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel} 集成测试类 + * {@link DashScopeImageModel} 集成测试类 * * @author fansili */ public class TongYiImagesModelTest { - private final ImageSynthesis imageApi = new ImageSynthesis(); - private final TongYiImagesModel imageModel = new TongYiImagesModel(imageApi); - - static { - Constants.apiKey = "sk-Zsd81gZYg7"; - } + private final DashScopeImageModel imageModel = new DashScopeImageModel( + new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); @Test @Disabled public void imageCallTest() { // 准备参数 - ImageOptions options = OpenAiImageOptions.builder() - .withModel(ImageSynthesis.Models.WANX_V1) + ImageOptions options = DashScopeImageOptions.builder() + .withModel("wanx-v1") .withHeight(256).withWidth(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java index f9338995f..cb0b4efb7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java @@ -13,16 +13,16 @@ import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; */ public class ZhiPuAiImageModelTests { - private final ZhiPuAiImageApi imageApi = new ZhiPuAiImageApi( - "78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy"); - private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel(imageApi); + private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel( + new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥 + ); @Test @Disabled public void testCall() { // 准备参数 ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() - .withModel(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) + .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) .build(); ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 2f293dd4f..efdb41951 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -981,7 +981,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskList.get(0).getProcessDefinitionId()); List activityIds = CollUtil.newArrayList(convertSet(taskList, Task::getTaskDefinitionKey)); EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); - Assert.notNull(endEvent, "结束节点不能未空"); + Assert.notNull(endEvent, "结束节点不能为空"); runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId()) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java index 0ddaba476..48e2393b0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -17,6 +17,7 @@ import org.springframework.web.client.RestTemplate; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; +// TODO @芋艿:可能会想换个包地址 /** * BPM 用户任务通用监听器 * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java index 8ebd9735e..dfbaa63ec 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task.trigger; import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +// TODO @芋艿:可能会想换个包地址 /** * BPM 触发器接口 *

diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java index 7c2dae223..33e8d84ab 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java @@ -127,8 +127,8 @@ public class CrmBusinessController { } @GetMapping("/simple-all-list") - @Operation(summary = "获得联系人的精简列表") - @PreAuthorize("@ss.hasPermission('crm:contact:query')") + @Operation(summary = "获得商机的精简列表") + @PreAuthorize("@ss.hasPermission('crm:business:query')") public CommonResult> getSimpleContactList() { CrmBusinessPageReqVO reqVO = new CrmBusinessPageReqVO(); reqVO.setPageSize(PAGE_SIZE_NONE); // 不分页 diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http index 1ef2bc1a1..7abc26705 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/permission/CrmPermissionController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/crm/permission/create Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "userId": 1, @@ -15,7 +15,7 @@ tenant-id: {{adminTenentId}} PUT {{baseUrl}}/crm/permission/update Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "userId": 1, @@ -28,5 +28,5 @@ tenant-id: {{adminTenentId}} ### 请求 /delete DELETE {{baseUrl}}/crm/permission/delete?bizType=2&bizId=1&id=1 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http index 6b960512d..4f343165e 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsCustomerController.http @@ -2,64 +2,64 @@ ### 1.1 客户总量分析(按日期) GET {{baseUrl}}/crm/statistics-customer/get-customer-summary-by-date?deptId=100&interval=2×[0]=2024-01-01 00:00:00×[1]=2024-01-29 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 1.2 客户总量统计(按用户) GET {{baseUrl}}/crm/statistics-customer/get-customer-summary-by-user?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} # == 2. 客户跟进次数分析 == ### 2.1 客户跟进次数分析(按日期) GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-date?deptId=100&interval=2×[0]=2024-01-01 00:00:00×[1]=2024-01-29 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 2.2 客户总量统计(按用户) GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-user?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} # == 3. 客户跟进方式分析 == ### 3.1 客户跟进方式分析 GET {{baseUrl}}/crm/statistics-customer/get-follow-up-summary-by-type?deptId=100&interval=2×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} # == 4. 客户成交周期 == ### 4.1 合同摘要信息(客户转化率页面) GET {{baseUrl}}/crm/statistics-customer/get-contract-summary?deptId=100&interval=2×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} # == 5. 客户成交周期 == ### 5.1 获取客户公海分析(按日期) GET {{baseUrl}}/crm/statistics-customer/get-pool-summary-by-date?deptId=100&interval=2×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 5.2 获取客户公海分析(按用户) GET {{baseUrl}}/crm/statistics-customer/get-pool-summary-by-user?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} # == 6. 客户成交周期 == ### 6.1 客户成交周期(按日期) GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-date?deptId=100&interval=2×[0]=2024-01-01 00:00:00×[1]=2024-01-29 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 6.2 获取客户成交周期(按用户) GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-user?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 6.3 获取客户成交周期(按区域) GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-area?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 6.4 获取客户成交周期(按产品) GET {{baseUrl}}/crm/statistics-customer/get-customer-deal-cycle-by-product?deptId=100×[0]=2023-01-01 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.http b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.http index e878ba1a9..407562ddb 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.http +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/statistics/CrmStatisticsRankController.http @@ -1,9 +1,9 @@ ### 合同金额排行榜 GET {{baseUrl}}/crm/statistics-rank/get-contract-price-rank?deptId=100×[0]=2022-12-12 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 回款金额排行榜 GET {{baseUrl}}/crm/statistics-rank/get-receivable-price-rank?deptId=100×[0]=2022-12-12 00:00:00×[1]=2024-12-12 23:59:59 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} \ No newline at end of file +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java index 269cac6cc..c24614ad5 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/receivable/CrmReceivableDO.java @@ -66,7 +66,9 @@ public class CrmReceivableDO extends BaseDO { */ private LocalDateTime returnTime; /** - * 回款方式,关联枚举{@link CrmReceivableReturnTypeEnum} + * 回款方式 + * + * 枚举 {@link CrmReceivableReturnTypeEnum} */ private Integer returnType; /** diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java index 174db9b3a..9348c5e46 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java @@ -223,7 +223,7 @@ public class CrmContactServiceImpl implements CrmContactService { } @Override - @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}", + @LogRecord(type = CRM_CONTACT_TYPE, subType = CRM_CONTACT_FOLLOW_UP_SUB_TYPE, bizNo = "{{#id}}", success = CRM_CONTACT_FOLLOW_UP_SUCCESS) @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTACT, bizId = "#id", level = CrmPermissionLevelEnum.WRITE) public void updateContactFollowUp(Long id, LocalDateTime contactNextTime, String contactLastContent) { diff --git a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/statistics/ErpSaleStatisticsController.http b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/statistics/ErpSaleStatisticsController.http index 5f5cab109..0dca8935c 100644 --- a/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/statistics/ErpSaleStatisticsController.http +++ b/yudao-module-erp/yudao-module-erp-biz/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/statistics/ErpSaleStatisticsController.http @@ -1,11 +1,11 @@ ### 请求 /erp/sale-statistics/summary 接口 => 成功 GET {{baseUrl}}/erp/sale-statistics/summary Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} ### 请求 /erp/sale-statistics/time-summary 接口 => 成功 GET {{baseUrl}}/erp/sale-statistics/time-summary Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http index 499f64df7..14b6228c9 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileConfigController.http @@ -1,7 +1,7 @@ ### 请求 /infra/file-config/create 接口 => 成功 POST {{baseUrl}}/infra/file-config/create Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} { @@ -21,7 +21,7 @@ Authorization: Bearer {{token}} ### 请求 /infra/file-config/update 接口 => 成功 PUT {{baseUrl}}/infra/file-config/update Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} { @@ -41,5 +41,5 @@ Authorization: Bearer {{token}} ### 请求 /infra/file-config/test 接口 => 成功 GET {{baseUrl}}/infra/file-config/test?id=2 Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http index 62f8dd606..52e7394c5 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/JobController.http @@ -1,5 +1,5 @@ ### 请求 /infra/job/sync 接口 => 成功 POST {{baseUrl}}/infra/job/sync Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java index 53f52f02c..8029f1584 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -17,7 +17,7 @@ public class ApiErrorLogRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @ExcelProperty("编号") - private Integer id; + private Long id; @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") @ExcelProperty("链路追踪编号") @@ -25,7 +25,7 @@ public class ApiErrorLogRespVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") @ExcelProperty("用户编号") - private Integer userId; + private Long userId; @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @ExcelProperty(value = "用户类型", converter = DictConvert.class) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http index 8a0e70fd3..68b5487e2 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/redis/RedisController.http @@ -1,4 +1,4 @@ ### 请求 /infra/redis/get-monitor-info 接口 => 成功 GET {{baseUrl}}/infra/redis/get-monitor-info Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index 8970c004c..b25870fe8 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import com.alibaba.ttl.TransmittableThreadLocal; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; import org.apache.tika.Tika; import java.io.IOException; -import java.net.URLEncoder; /** * 文件类型 Utils @@ -60,7 +60,7 @@ public class FileTypeUtils { */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType - response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); String contentType = getMineType(content, filename); response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 diff --git a/yudao-module-iot/pom.xml b/yudao-module-iot/pom.xml index 069af1699..0422c5d6c 100644 --- a/yudao-module-iot/pom.xml +++ b/yudao-module-iot/pom.xml @@ -10,6 +10,7 @@ yudao-module-iot-api yudao-module-iot-biz + yudao-module-iot-plugins 4.0.0 diff --git a/yudao-module-iot/yudao-module-iot-api/pom.xml b/yudao-module-iot/yudao-module-iot-api/pom.xml index 8a75b09bf..4a31c9bf5 100644 --- a/yudao-module-iot/yudao-module-iot-api/pom.xml +++ b/yudao-module-iot/yudao-module-iot-api/pom.xml @@ -12,6 +12,7 @@ jar ${project.artifactId} + 物联网 模块 API,暴露给其它模块调用 @@ -21,6 +22,32 @@ cn.iocoder.boot yudao-common + + + + org.springframework + spring-web + provided + + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + org.pf4j + pf4j-spring + + + + + org.springframework.boot + spring-boot-starter-validation + true + diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java new file mode 100644 index 000000000..e88706ac5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.enums.ApiConstants; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * 设备数据 Upstream 上行 API + * + * 目的:设备 -> 插件 -> 服务端 + * + * @author haohao + */ +public interface IotDeviceUpstreamApi { + + String PREFIX = ApiConstants.PREFIX + "/device/upstream"; + + // ========== 设备相关 ========== + + /** + * 更新设备状态 + * + * @param updateReqDTO 更新设备状态 DTO + */ + @PostMapping(PREFIX + "/update-state") + CommonResult updateDeviceState(@Valid @RequestBody IotDeviceStateUpdateReqDTO updateReqDTO); + + /** + * 上报设备属性数据 + * + * @param reportReqDTO 上报设备属性数据 DTO + */ + @PostMapping(PREFIX + "/report-property") + CommonResult reportDeviceProperty(@Valid @RequestBody IotDevicePropertyReportReqDTO reportReqDTO); + + /** + * 上报设备事件数据 + * + * @param reportReqDTO 设备事件 + */ + @PostMapping(PREFIX + "/report-event") + CommonResult reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册设备 + * + * @param registerReqDTO 注册设备 DTO + */ + @PostMapping(PREFIX + "/register") + CommonResult registerDevice(@Valid @RequestBody IotDeviceRegisterReqDTO registerReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册子设备 + * + * @param registerReqDTO 注册子设备 DTO + */ + @PostMapping(PREFIX + "/register-sub") + CommonResult registerSubDevice(@Valid @RequestBody IotDeviceRegisterSubReqDTO registerReqDTO); + + // TODO @芋艿:这个需要 plugins 接入下 + /** + * 注册设备拓扑 + * + * @param addReqDTO 注册设备拓扑 DTO + */ + @PostMapping(PREFIX + "/add-topology") + CommonResult addDeviceTopology(@Valid @RequestBody IotDeviceTopologyAddReqDTO addReqDTO); + + // TODO @芋艿:考虑 http 认证 + /** + * 认证 Emqx 连接 + * + * @param authReqDTO 认证 Emqx 连接 DTO + */ + @PostMapping(PREFIX + "/authenticate-emqx-connection") + CommonResult authenticateEmqxConnection(@Valid @RequestBody IotDeviceEmqxAuthReqDTO authReqDTO); + + // ========== 插件相关 ========== + + /** + * 心跳插件实例 + * + * @param heartbeatReqDTO 心跳插件实例 DTO + */ + @PostMapping(PREFIX + "/heartbeat-plugin-instance") + CommonResult heartbeatPluginInstance(@Valid @RequestBody IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java new file mode 100644 index 000000000..9624b671e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceConfigSetReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【配置】设置 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceConfigSetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 配置 + */ + @NotNull(message = "配置不能为空") + private Map config; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java new file mode 100644 index 000000000..e78bea6fb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceDownstreamAbstractReqDTO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * IoT 设备下行的抽象 Request DTO + * + * @author 芋道源码 + */ +@Data +public abstract class IotDeviceDownstreamAbstractReqDTO { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java new file mode 100644 index 000000000..8eccec42e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceOtaUpgradeReqDTO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import cn.hutool.core.map.MapUtil; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【OTA】升级下发 Request DTO(更新固件消息) + * + * @author 芋道源码 + */ +@Data +public class IotDeviceOtaUpgradeReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + /** + * 固件版本 + */ + private String version; + + /** + * 签名方式 + * + * 例如说:MD5、SHA256 + */ + private String signMethod; + /** + * 固件文件签名 + */ + private String fileSign; + /** + * 固件文件大小 + */ + private Long fileSize; + /** + * 固件文件 URL + */ + private String fileUrl; + + /** + * 自定义信息,建议使用 JSON 格式 + */ + private String information; + + public static IotDeviceOtaUpgradeReqDTO build(Map map) { + return new IotDeviceOtaUpgradeReqDTO() + .setFirmwareId(MapUtil.getLong(map, "firmwareId")).setVersion((String) map.get("version")) + .setSignMethod((String) map.get("signMethod")).setFileSign((String) map.get("fileSign")) + .setFileSize(MapUtil.getLong(map, "fileSize")).setFileUrl((String) map.get("fileUrl")) + .setInformation((String) map.get("information")); + } + + public static Map build(IotDeviceOtaUpgradeReqDTO dto) { + return MapUtil.builder() + .put("firmwareId", dto.getFirmwareId()).put("version", dto.getVersion()) + .put("signMethod", dto.getSignMethod()).put("fileSign", dto.getFileSign()) + .put("fileSize", dto.getFileSize()).put("fileUrl", dto.getFileUrl()) + .put("information", dto.getInformation()) + .build(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java new file mode 100644 index 000000000..d9ae96321 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertyGetReqDTO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +// TODO @芋艿:从 server => plugin => device 是否有必要?从阿里云 iot 来看,没有这个功能?! +// TODO @芋艿:是不是改成 read 更好?在看看阿里云的 topic 设计 +/** + * IoT 设备【属性】获取 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertyGetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 属性标识数组 + */ + @NotEmpty(message = "属性标识数组不能为空") + private List identifiers; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java new file mode 100644 index 000000000..170fe80f6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDevicePropertySetReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【属性】设置 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertySetReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 属性参数 + */ + @NotEmpty(message = "属性参数不能为空") + private Map properties; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java new file mode 100644 index 000000000..0a2b3f0bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/downstream/IotDeviceServiceInvokeReqDTO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【服务】调用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceServiceInvokeReqDTO extends IotDeviceDownstreamAbstractReqDTO { + + /** + * 服务标识 + */ + @NotEmpty(message = "服务标识不能为空") + private String identifier; + /** + * 调用参数 + */ + private Map params; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java new file mode 100644 index 000000000..8762aae5b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEmqxAuthReqDTO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +// TODO @芋艿:要不要继承 IotDeviceUpstreamAbstractReqDTO +// TODO @芋艿:@haohao:后续其它认证的设计 +/** + * IoT 认证 Emqx 连接 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceEmqxAuthReqDTO { + + /** + * 客户端 ID + */ + @NotEmpty(message = "客户端 ID 不能为空") + private String clientId; + + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java new file mode 100644 index 000000000..34e6283d9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceEventReportReqDTO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【事件】上报 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceEventReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 事件标识 + */ + @NotEmpty(message = "事件标识不能为空") + private String identifier; + /** + * 事件参数 + */ + private Map params; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java new file mode 100644 index 000000000..a88a72e91 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaProgressReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import lombok.Data; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/progress +/** + * IoT 设备【OTA】升级进度 Request DTO(上报更新固件进度) + * + * @author 芋道源码 + */ +@Data +public class IotDeviceOtaProgressReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 升级状态 + * + * 枚举 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + private Integer status; + /** + * 升级进度,百分比 + */ + private Integer progress; + + /** + * 升级进度描述 + */ + private String description; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java new file mode 100644 index 000000000..6328704e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaPullReqDTO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/pull +/** + * IoT 设备【OTA】升级下拉 Request DTO(拉取固件更新) + * + * @author 芋道源码 + */ +public class IotDeviceOtaPullReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 固件版本 + */ + private String version; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java new file mode 100644 index 000000000..2b3b91c98 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceOtaReportReqDTO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/report +/** + * IoT 设备【OTA】上报 Request DTO(上报固件版本) + * + * @author 芋道源码 + */ +public class IotDeviceOtaReportReqDTO { + + /** + * 固件编号 + */ + private Long firmwareId; + + /** + * 固件版本 + */ + private String version; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java new file mode 100644 index 000000000..4a276bd22 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDevicePropertyReportReqDTO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Map; + +/** + * IoT 设备【属性】上报 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDevicePropertyReportReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 属性参数 + */ + @NotEmpty(message = "属性参数不能为空") + private Map properties; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java new file mode 100644 index 000000000..cab55e832 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterReqDTO.java @@ -0,0 +1,12 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import lombok.Data; + +/** + * IoT 设备【注册】自己 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceRegisterReqDTO extends IotDeviceUpstreamAbstractReqDTO { +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java new file mode 100644 index 000000000..0b826fbb1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceRegisterSubReqDTO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +/** + * IoT 设备【注册】子设备 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceRegisterSubReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + // TODO @芋艿:看看要不要优化命名 + /** + * 子设备数组 + */ + @NotEmpty(message = "子设备不能为空") + private List params; + + /** + * 设备信息 + */ + @Data + public static class Device { + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java new file mode 100644 index 000000000..38c479a57 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceStateUpdateReqDTO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 设备【状态】更新 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotDeviceStateUpdateReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + /** + * 设备状态 + */ + @NotNull(message = "设备状态不能为空") + @InEnum(IotDeviceStateEnum.class) // 只使用:在线、离线 + private Integer state; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java new file mode 100644 index 000000000..18efe7d48 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceTopologyAddReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.List; + +// TODO @芋艿:要写清楚,是来自设备网关,还是设备。 +/** + * IoT 设备【拓扑】添加 Request DTO + */ +@Data +public class IotDeviceTopologyAddReqDTO extends IotDeviceUpstreamAbstractReqDTO { + + // TODO @芋艿:看看要不要优化命名 + /** + * 子设备数组 + */ + @NotEmpty(message = "子设备不能为空") + private List params; + + /** + * 设备信息 + */ + @Data + public static class Device { + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + // TODO @芋艿:阿里云还有 sign 签名 + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java new file mode 100644 index 000000000..a0c8ce92a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotDeviceUpstreamAbstractReqDTO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import cn.iocoder.yudao.framework.common.util.json.databind.TimestampLocalDateTimeSerializer; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * IoT 设备上行的抽象 Request DTO + * + * @author 芋道源码 + */ +@Data +public abstract class IotDeviceUpstreamAbstractReqDTO { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 插件实例的进程编号 + */ + private String processId; + + /** + * 产品标识 + */ + @NotEmpty(message = "产品标识不能为空") + private String productKey; + /** + * 设备名称 + */ + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + /** + * 上报时间 + */ + @JsonSerialize(using = TimestampLocalDateTimeSerializer.class) // 解决 iot plugins 序列化 LocalDateTime 是数组,导致无法解析的问题 + private LocalDateTime reportTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java new file mode 100644 index 000000000..9125b5f24 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * IoT 插件实例心跳 Request DTO + * + * @author 芋道源码 + */ +@Data +public class IotPluginInstanceHeartbeatReqDTO { + + /** + * 请求编号 + */ + @NotEmpty(message = "请求编号不能为空") + private String processId; + + /** + * 插件包标识符 + */ + @NotEmpty(message = "插件包标识符不能为空") + private String pluginKey; + + /** + * 插件实例所在 IP + */ + @NotEmpty(message = "插件实例所在 IP 不能为空") + private String hostIp; + /** + * 插件实例的进程编号 + */ + @NotNull(message = "插件实例的进程编号不能为空") + private Integer downstreamPort; + + /** + * 是否在线 + */ + @NotNull(message = "是否在线不能为空") + private Boolean online; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java new file mode 100644 index 000000000..cb946cd89 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位 + */ +package cn.iocoder.yudao.module.iot.api.device.dto; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java new file mode 100644 index 000000000..2c4147be1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ApiConstants.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.iot.enums; + +import cn.iocoder.yudao.framework.common.enums.RpcConstants; + +/** + * API 相关的枚举 + * + * @author 芋道源码 + */ +public class ApiConstants { + + public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/iot"; + + public static final String VERSION = "1.0.0"; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java new file mode 100644 index 000000000..d8f0cc60d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/DictTypeConstants.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.enums; + +/** + * IoT 字典类型的枚举类 + * + * @author 芋道源码 + */ +public class DictTypeConstants { + + public static final String PRODUCT_STATUS = "iot_product_status"; + public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type"; + public static final String NET_TYPE = "iot_net_type"; + public static final String PROTOCOL_TYPE = "iot_protocol_type"; + public static final String DATA_FORMAT = "iot_data_format"; + public static final String VALIDATE_TYPE = "iot_validate_type"; + + public static final String DEVICE_STATE = "iot_device_state"; + + public static final String IOT_DATA_BRIDGE_DIRECTION_ENUM = "iot_data_bridge_direction_enum"; + public static final String IOT_DATA_BRIDGE_TYPE_ENUM = "iot_data_bridge_type_enum"; + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java index 31702e1e6..230baca3f 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java @@ -9,24 +9,67 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; */ public interface ErrorCodeConstants { - // ========== IoT 产品相关 1-050-001-000 ============ + // ========== 产品相关 1-050-001-000 ============ ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_050_001_000, "产品不存在"); - ErrorCode PRODUCT_IDENTIFICATION_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); + ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在"); ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除"); + ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型"); - // ========== IoT 产品物模型 1-050-002-000 ============ - ErrorCode THINK_MODEL_FUNCTION_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); - ErrorCode THINK_MODEL_FUNCTION_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在"); - ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。"); - ErrorCode THINK_MODEL_FUNCTION_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。"); - ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效"); + // ========== 产品物模型 1-050-002-000 ============ + ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在"); + ErrorCode THING_MODEL_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在"); + ErrorCode THING_MODEL_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。"); + ErrorCode THING_MODEL_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。"); + ErrorCode THING_MODEL_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效"); - // ========== IoT 设备 1-050-003-000 ============ + // ========== 设备 1-050-003-000 ============ ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在"); ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一"); ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除"); - ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改"); - ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改"); - ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态"); + ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在"); + ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在"); + ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备"); + ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); + ErrorCode DEVICE_DOWNSTREAM_FAILED = new ErrorCode(1_050_003_007, "执行失败,原因:{}"); -} + // ========== 产品分类 1-050-004-000 ========== + ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); + + // ========== 设备分组 1-050-005-000 ========== + ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在"); + ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除"); + + // ========== 插件配置 1-050-006-000 ========== + ErrorCode PLUGIN_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "插件配置不存在"); + ErrorCode PLUGIN_INSTALL_FAILED = new ErrorCode(1_050_006_001, "插件安装失败"); + ErrorCode PLUGIN_INSTALL_FAILED_FILE_NAME_NOT_MATCH = new ErrorCode(1_050_006_002, "插件安装失败,文件名与原插件id不匹配"); + ErrorCode PLUGIN_CONFIG_DELETE_FAILED_RUNNING = new ErrorCode(1_050_006_003, "请先停止插件"); + ErrorCode PLUGIN_STATUS_INVALID = new ErrorCode(1_050_006_004, "插件状态无效"); + ErrorCode PLUGIN_CONFIG_KEY_DUPLICATE = new ErrorCode(1_050_006_005, "插件标识已存在"); + ErrorCode PLUGIN_START_FAILED = new ErrorCode(1_050_006_006, "插件启动失败"); + ErrorCode PLUGIN_STOP_FAILED = new ErrorCode(1_050_006_007, "插件停止失败"); + + // ========== 插件实例 1-050-007-000 ========== + + // ========== 固件相关 1-050-008-000 ========== + + ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在"); + ErrorCode OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE = new ErrorCode(1_050_008_001, "产品版本号重复"); + + ErrorCode OTA_UPGRADE_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在"); + ErrorCode OTA_UPGRADE_TASK_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "升级任务名称重复"); + ErrorCode OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY = new ErrorCode(1_050_008_102, "设备编号列表不能为空"); + ErrorCode OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY = new ErrorCode(1_050_008_103, "设备列表不能为空"); + ErrorCode OTA_UPGRADE_TASK_CANNOT_CANCEL = new ErrorCode(1_050_008_104, "升级任务不能取消"); + + ErrorCode OTA_UPGRADE_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在"); + ErrorCode OTA_UPGRADE_RECORD_DUPLICATE = new ErrorCode(1_050_008_201, "升级记录重复"); + ErrorCode OTA_UPGRADE_RECORD_CANNOT_RETRY = new ErrorCode(1_050_008_202, "升级记录不能重试"); + + // ========== MQTT 通信相关 1-050-009-000 ========== + ErrorCode MQTT_TOPIC_ILLEGAL = new ErrorCode(1_050_009_000, "topic illegal"); + + // ========== IoT 数据桥梁 1-050-010-000 ========== + ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_010_000, "IoT 数据桥梁不存在"); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java new file mode 100644 index 000000000..6de9359ba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +// TODO @芋艿:需要添加对应的 DTO,以及上下行的链路,网关、网关服务、设备等 +/** + * IoT 设备消息标识符枚举 + */ +@Getter +@RequiredArgsConstructor +public enum IotDeviceMessageIdentifierEnum { + + PROPERTY_GET("get"), // 下行 TODO 芋艿:【讨论】貌似这个“上行”更合理?device 主动拉取配置。和 IotDevicePropertyGetReqDTO 一样的配置 + PROPERTY_SET("set"), // 下行 + PROPERTY_REPORT("report"), // 上行 + + STATE_ONLINE("online"), // 上行 + STATE_OFFLINE("offline"), // 上行 + + CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景 + CONFIG_SET("set"), // 下行 + + SERVICE_INVOKE("${identifier}"), // 下行 + SERVICE_REPLY_SUFFIX("_reply"), // 芋艿:TODO 芋艿:【讨论】上行 or 下行 + + OTA_UPGRADE("upgrade"), // 下行 + OTA_PULL("pull"), // 上行 + OTA_PROGRESS("progress"), // 上行 + OTA_REPORT("report"), // 上行 + + REGISTER_REGISTER("register"), // 上行 + REGISTER_REGISTER_SUB("register_sub"), // 上行 + REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行 + + TOPOLOGY_ADD("topology_add"), // 下行; + ; + + /** + * 标志符 + */ + private final String identifier; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java new file mode 100644 index 000000000..0354157ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 设备消息类型枚举 + */ +@Getter +@RequiredArgsConstructor +public enum IotDeviceMessageTypeEnum implements ArrayValuable { + + STATE("state"), // 设备状态 + PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务 + CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置 + OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级 + REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册 + TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new); + + /** + * 属性 + */ + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java new file mode 100644 index 000000000..6ce2677db --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStateEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.enums.device; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 设备状态枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotDeviceStateEnum implements ArrayValuable { + + INACTIVE(0, "未激活"), + ONLINE(1, "在线"), + OFFLINE(2, "离线"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDeviceStateEnum::getState).toArray(Integer[]::new); + + /** + * 状态 + */ + private final Integer state; + /** + * 状态名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static boolean isOnline(Integer state) { + return ONLINE.getState().equals(state); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java deleted file mode 100644 index 0ec2ac776..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceStatusEnum.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.device; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.Getter; - -import java.util.Arrays; - -/** - * IoT 设备状态枚举 - * - * @author haohao - */ -@Getter -public enum IotDeviceStatusEnum implements ArrayValuable { - - INACTIVE(0, "未激活"), - ONLINE(1, "在线"), - OFFLINE(2, "离线"), - DISABLED(3, "已禁用"); - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDeviceStatusEnum::getStatus).toArray(Integer[]::new); - - /** - * 状态 - */ - private final Integer status; - /** - * 状态名 - */ - private final String name; - - IotDeviceStatusEnum(Integer status, String name) { - this.status = status; - this.name = name; - } - - public static IotDeviceStatusEnum fromStatus(Integer status) { - for (IotDeviceStatusEnum value : values()) { - if (value.getStatus().equals(status)) { - return value; - } - } - return null; - } - - public static boolean isValidStatus(Integer status) { - return fromStatus(status) != null; - } - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java new file mode 100644 index 000000000..e809a7e5b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeRecordStatusEnum.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级记录的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeRecordStatusEnum implements ArrayValuable { + + PENDING(0), // 待推送 + PUSHED(10), // 已推送 + UPGRADING(20), // 升级中 + SUCCESS(30), // 升级成功 + FAILURE(40), // 升级失败 + CANCELED(50),; // 已取消 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeRecordStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer status; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java new file mode 100644 index 000000000..6dccbb041 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskScopeEnum.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级任务的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeTaskScopeEnum implements ArrayValuable { + + ALL(1), // 全部设备:只包括当前产品下的设备,不包括未来创建的设备 + SELECT(2); // 指定设备 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskScopeEnum::getScope).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer scope; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java new file mode 100644 index 000000000..78af16cb2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ota/IotOtaUpgradeTaskStatusEnum.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.enums.ota; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT OTA 升级任务的范围枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotOtaUpgradeTaskStatusEnum implements ArrayValuable { + + IN_PROGRESS(10), // 进行中:升级中 + COMPLETED(20), // 已完成:已结束,全部升级完成 + INCOMPLETE(21), // 未完成:已结束,部分升级完成 + CANCELED(30),; // 已取消:一般是主动取消任务 + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotOtaUpgradeTaskStatusEnum::getStatus).toArray(Integer[]::new); + + /** + * 范围 + */ + private final Integer status; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java new file mode 100644 index 000000000..b6ef4f0cc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.plugin; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 部署方式枚举 + * + * @author haohao + */ +@RequiredArgsConstructor +@Getter +public enum IotPluginDeployTypeEnum implements ArrayValuable { + + JAR(0, "JAR 部署"), + STANDALONE(1, "独立部署"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginDeployTypeEnum::getDeployType).toArray(Integer[]::new); + + /** + * 部署方式 + */ + private final Integer deployType; + /** + * 部署方式名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java similarity index 50% rename from yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java index 6ded3f6de..7e3fa657e 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginStatusEnum.java @@ -1,36 +1,34 @@ -package cn.iocoder.yudao.module.ai.enums.knowledge; +package cn.iocoder.yudao.module.iot.enums.plugin; import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; import java.util.Arrays; /** - * AI 知识库-文档状态的枚举 + * IoT 插件状态枚举 * - * @author xiaoxin + * @author haohao */ -@AllArgsConstructor +@RequiredArgsConstructor @Getter -public enum AiKnowledgeDocumentStatusEnum implements ArrayValuable { +public enum IotPluginStatusEnum implements ArrayValuable { - IN_PROGRESS(10, "索引中"), - SUCCESS(20, "可用"), - FAIL(30, "失败"); + STOPPED(0, "停止"), + RUNNING(1, "运行"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginStatusEnum::getStatus).toArray(Integer[]::new); /** * 状态 */ private final Integer status; - /** * 状态名 */ private final String name; - public static final Integer[] ARRAYS = Arrays.stream(values()).map(AiKnowledgeDocumentStatusEnum::getStatus).toArray(Integer[]::new); - @Override public Integer[] array() { return ARRAYS; diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java new file mode 100644 index 000000000..ec0b72f9f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.plugin; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 插件类型枚举 + * + * @author haohao + */ +@AllArgsConstructor +@Getter +public enum IotPluginTypeEnum implements ArrayValuable { + + NORMAL(0, "普通插件"), + DEVICE(1, "设备插件"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginTypeEnum::getType).toArray(Integer[]::new); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java deleted file mode 100644 index 64ece99ca..000000000 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotAccessModeEnum.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.enums.product; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * IOT 访问方式枚举类 - * - * @author ahh - */ -@AllArgsConstructor -@Getter -public enum IotAccessModeEnum { - - READ("r"), - WRITE("w"), - READ_WRITE("rw"); - - private final String mode; - -} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java index 561bc66f9..2a54e489f 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotNetTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 联网方式枚举类 + * IoT 联网方式枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java index 3d8ca10b3..7910f1b2d 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductDeviceTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品的设备类型 + * IoT 产品的设备类型 * * @author ahh */ @@ -16,7 +16,7 @@ import java.util.Arrays; public enum IotProductDeviceTypeEnum implements ArrayValuable { DIRECT(0, "直连设备"), - GATEWAY_CHILD(1, "网关子设备"), + GATEWAY_SUB(1, "网关子设备"), GATEWAY(2, "网关设备"); /** @@ -36,4 +36,24 @@ public enum IotProductDeviceTypeEnum implements ArrayValuable { return ARRAYS; } + /** + * 判断是否是网关 + * + * @param type 类型 + * @return 是否是网关 + */ + public static boolean isGateway(Integer type) { + return GATEWAY.getType().equals(type); + } + + /** + * 判断是否是网关子设备 + * + * @param type 类型 + * @return 是否是网关子设备 + */ + public static boolean isGatewaySub(Integer type) { + return GATEWAY_SUB.getType().equals(type); + } + } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java index 32daf1671..b9bbbeec7 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductStatusEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品的状态枚举类 + * IoT 产品的状态枚举类 * * @author ahh */ @@ -18,12 +18,12 @@ public enum IotProductStatusEnum implements ArrayValuable { UNPUBLISHED(0, "开发中"), PUBLISHED(1, "已发布"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductStatusEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductStatusEnum::getStatus).toArray(Integer[]::new); /** * 类型 */ - private final Integer type; + private final Integer status; /** * 描述 */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java index 9eb57044f..d24dea92e 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProtocolTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 接入网关协议枚举类 + * IoT 接入网关协议枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java index 11604b4dd..2a15d16a4 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotValidateTypeEnum.java @@ -7,7 +7,7 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 数据校验级别枚举类 + * IoT 数据校验级别枚举类 * * @author ahh */ diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java new file mode 100644 index 000000000..3fdd53234 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotAlertConfigReceiveTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 告警配置的接收方式枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotAlertConfigReceiveTypeEnum implements ArrayValuable { + + SMS(1), // 短信 + MAIL(2), // 邮箱 + NOTIFY(3); // 通知 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotAlertConfigReceiveTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java new file mode 100644 index 000000000..a9d445fd2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeDirectionEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 数据桥接的方向枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotDataBridgeDirectionEnum implements ArrayValuable { + + INPUT(1), // 输入 + OUTPUT(2); // 输出 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeDirectionEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java new file mode 100644 index 000000000..78fc8452e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataBridgeTypeEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 数据桥接的类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotDataBridgeTypeEnum implements ArrayValuable { + + HTTP(1, "HTTP"), + TCP(2, "TCP"), + WEBSOCKET(3, "WEBSOCKET"), + + MQTT(10, "MQTT"), + + DATABASE(20, "DATABASE"), + REDIS_STREAM(21, "REDIS_STREAM"), + + ROCKETMQ(30, "ROCKETMQ"), + RABBITMQ(31, "RABBITMQ"), + KAFKA(32, "KAFKA"); + + private final Integer type; + + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotDataBridgeTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java new file mode 100644 index 000000000..2bdf7d0ed --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneActionTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 规则场景的触发类型枚举 + * + * 设备触发,定时触发 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneActionTypeEnum implements ArrayValuable { + + DEVICE_CONTROL(1), // 设备执行 + ALERT(2), // 告警执行 + DATA_BRIDGE(3); // 桥接执行 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneActionTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java new file mode 100644 index 000000000..5ed90ccae --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 场景触发条件参数的操作符枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayValuable { + + EQUALS("=", "#source == #value"), + NOT_EQUALS("!=", "!(#source == #value)"), + + GREATER_THAN(">", "#source > #value"), + GREATER_THAN_OR_EQUALS(">=", "#source >= #value"), + + LESS_THAN("<", "#source < #value"), + LESS_THAN_OR_EQUALS("<=", "#source <= #value"), + + IN("in", "#values.contains(#source)"), + NOT_IN("not in", "!(#values.contains(#source))"), + + BETWEEN("between", "(#source >= #values.get(0)) && (#source <= #values.get(1))"), + NOT_BETWEEN("not between", "(#source < #values.get(0)) || (#source > #values.get(1))"), + + LIKE("like", "#source.contains(#value)"), // 字符串匹配 + NOT_NULL("not null", "#source != null && #source.length() > 0"); // 非空 + + private final String operator; + private final String springExpression; + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerConditionParameterOperatorEnum::getOperator).toArray(String[]::new); + + /** + * Spring 表达式 - 原始值 + */ + public static final String SPRING_EXPRESSION_SOURCE = "source"; + /** + * Spring 表达式 - 目标值 + */ + public static final String SPRING_EXPRESSION_VALUE = "value"; + /** + * Spring 表达式 - 目标值数组 + */ + public static final String SPRING_EXPRESSION_VALUE_List = "values"; + + public static IotRuleSceneTriggerConditionParameterOperatorEnum operatorOf(String operator) { + return ArrayUtil.firstMatch(item -> item.getOperator().equals(operator), values()); + } + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java new file mode 100644 index 000000000..a420a21d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.rule; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * IoT 场景流转的触发类型枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum IotRuleSceneTriggerTypeEnum implements ArrayValuable { + + DEVICE(1), // 设备触发 + TIMER(2); // 定时触发 + + private final Integer type; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java new file mode 100644 index 000000000..5524fdeb4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotDataSpecsDataTypeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 数据定义的数据类型枚举类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum IotDataSpecsDataTypeEnum implements ArrayValuable { + + INT("int"), + FLOAT("float"), + DOUBLE("double"), + ENUM("enum"), + BOOL("bool"), + TEXT("text"), + DATE("date"), + STRUCT("struct"), + ARRAY("array"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotDataSpecsDataTypeEnum::getDataType).toArray(String[]::new); + + private final String dataType; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java new file mode 100644 index 000000000..c0a2b329b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelAccessModeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型属性读取类型枚举 + * + * @author ahh + */ +@AllArgsConstructor +@Getter +public enum IotThingModelAccessModeEnum implements ArrayValuable { + + READ_ONLY("r"), + READ_WRITE("rw"); + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelAccessModeEnum::getMode).toArray(String[]::new); + + private final String mode; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java new file mode 100644 index 000000000..4f06cefce --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelParamDirectionEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + + +/** + * IoT 产品物模型参数是输入参数还是输出参数枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelParamDirectionEnum implements ArrayValuable { + + INPUT("input"), // 输入参数 + OUTPUT("output"); // 输出参数 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelParamDirectionEnum::getDirection).toArray(String[]::new); + + private final String direction; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java new file mode 100644 index 000000000..376db6b4a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceCallTypeEnum.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型服务调用方式枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelServiceCallTypeEnum implements ArrayValuable { + + ASYNC("async"), // 异步调用 + SYNC("sync"); // 同步调用 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelServiceCallTypeEnum::getType).toArray(String[]::new); + + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java new file mode 100644 index 000000000..c7c74092a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelServiceEventTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.enums.thingmodel; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * IoT 产品物模型事件类型枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum IotThingModelServiceEventTypeEnum implements ArrayValuable { + + INFO("info"), // 信息 + ALERT("alert"), // 告警 + ERROR("error"); // 故障 + + public static final String[] ARRAYS = Arrays.stream(values()).map(IotThingModelServiceEventTypeEnum::getType).toArray(String[]::new); + + private final String type; + + @Override + public String[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java similarity index 69% rename from yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java rename to yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java index 1e95c6b78..e0097cfe9 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotProductFunctionTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/thingmodel/IotThingModelTypeEnum.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.enums.product; +package cn.iocoder.yudao.module.iot.enums.thingmodel; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.AllArgsConstructor; @@ -7,19 +7,19 @@ import lombok.Getter; import java.util.Arrays; /** - * IOT 产品功能(物模型)类型枚举类 + * IoT 产品功能(物模型)类型枚举类 * * @author ahh */ @AllArgsConstructor @Getter -public enum IotProductFunctionTypeEnum implements ArrayValuable { +public enum IotThingModelTypeEnum implements ArrayValuable { PROPERTY(1, "属性"), SERVICE(2, "服务"), EVENT(3, "事件"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductFunctionTypeEnum::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotThingModelTypeEnum::getType).toArray(Integer[]::new); /** * 类型 diff --git a/yudao-module-iot/yudao-module-iot-biz/pom.xml b/yudao-module-iot/yudao-module-iot-biz/pom.xml index e3f93086a..8721e4de9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/pom.xml +++ b/yudao-module-iot/yudao-module-iot-biz/pom.xml @@ -25,6 +25,11 @@ ${revision} + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + cn.iocoder.boot @@ -37,11 +42,21 @@ + + com.taosdata.jdbc + taos-jdbcdriver + + cn.iocoder.boot yudao-spring-boot-starter-mybatis + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + cn.iocoder.boot @@ -54,11 +69,64 @@ yudao-spring-boot-starter-excel - + - org.eclipse.paho - org.eclipse.paho.client.mqttv3 + org.apache.rocketmq + rocketmq-spring-boot-starter + true + + org.springframework.kafka + spring-kafka + true + + + org.springframework.boot + spring-boot-starter-amqp + true + + + + org.pf4j + pf4j-spring + + + + + org.apache.groovy + groovy-all + 4.0.25 + pom + + + + + org.graalvm.js + js + 24.1.2 + pom + + + org.graalvm.js + js-scriptengine + 24.1.2 + + + + + + + + + + + + + + + + + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java new file mode 100644 index 000000000..9f54d60e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/ScriptTest.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot; + +import cn.hutool.script.ScriptUtil; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +/** + * TODO 芋艿:测试脚本的接入 + */ +public class ScriptTest { + + public static void main2(String[] args) { + // 创建一个 Groovy 脚本引擎 + ScriptEngine engine = ScriptUtil.createGroovyEngine(); + + // 创建绑定参数 + Bindings bindings = engine.createBindings(); + bindings.put("name", "Alice"); + bindings.put("age", 30); + + // 定义一个稍微复杂的 Groovy 脚本 + String script = "def greeting = 'Hello, ' + name + '!';\n" + + "def ageInFiveYears = age + 5;\n" + + "def message = greeting + ' In five years, you will be ' + ageInFiveYears + ' years old.';\n" + + "return message.toUpperCase();\n"; + + try { + // 执行脚本并获取结果 + Object result = engine.eval(script, bindings); + System.out.println(result); // 输出: HELLO, ALICE! IN FIVE YEARS, YOU WILL BE 35 YEARS OLD. + } catch (ScriptException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + // 创建一个 JavaScript 脚本引擎 + ScriptEngine jsEngine = ScriptUtil.createJsEngine(); + + // 创建绑定参数 + Bindings jsBindings = jsEngine.createBindings(); + jsBindings.put("name", "Bob"); + jsBindings.put("age", 25); + + // 定义一个简单的 JavaScript 脚本 + String jsScript = "var greeting = 'Hello, ' + name + '!';\n" + + "var ageInTenYears = age + 10;\n" + + "var message = greeting + ' In ten years, you will be ' + ageInTenYears + ' years old.';\n" + + "message.toUpperCase();\n"; + + try { + // 执行脚本并获取结果 + Object jsResult = jsEngine.eval(jsScript, jsBindings); + System.out.println(jsResult); // 输出: HELLO, BOB! IN TEN YEARS, YOU WILL BE 35 YEARS OLD. + } catch (ScriptException e) { + e.printStackTrace(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java new file mode 100644 index 000000000..25faa1a6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.api.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * * 设备数据 Upstream 上行 API 实现类 + */ +@RestController +@Validated +public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { + + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + // ========== 设备相关 ========== + + @Override + public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + deviceUpstreamService.updateDeviceState(updateReqDTO); + return success(true); + } + + @Override + public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + deviceUpstreamService.reportDeviceProperty(reportReqDTO); + return success(true); + } + + @Override + public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + deviceUpstreamService.reportDeviceEvent(reportReqDTO); + return success(true); + } + + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + deviceUpstreamService.registerDevice(registerReqDTO); + return success(true); + } + + @Override + public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + deviceUpstreamService.registerSubDevice(registerReqDTO); + return success(true); + } + + @Override + public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + deviceUpstreamService.addDeviceTopology(addReqDTO); + return success(true); + } + + @Override + public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + boolean result = deviceUpstreamService.authenticateEmqxConnection(authReqDTO); + return success(result); + } + + // ========== 插件相关 ========== + + @Override + public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + pluginInstanceService.heartbeatPluginInstance(heartbeatReqDTO); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java new file mode 100644 index 000000000..07852180d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/package-info.java @@ -0,0 +1,6 @@ +/** + * 占位 + * + * TODO 芋艿:后续删除 + */ +package cn.iocoder.yudao.module.iot.api; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http new file mode 100644 index 000000000..c1190cec1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http @@ -0,0 +1,75 @@ +### 请求 /iot/device/downstream 接口(服务调用) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "service", + "identifier": "temperature", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性设置) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "set", + "data": { + "xx": "yy" + } +} + +### 请求 /iot/device/downstream 接口(属性获取) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "property", + "identifier": "get", + "data": ["xx", "yy"] +} + +### 请求 /iot/device/downstream 接口(配置设置) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "config", + "identifier": "set" +} + +### 请求 /iot/device/downstream 接口(OTA 升级) => 成功 +POST {{baseUrl}}/iot/device/downstream +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 25, + "type": "ota", + "identifier": "upgrade", + "data": { + "firmwareId": 1, + "version": "1.0.0", + "signMethod": "MD5", + "fileSign": "d41d8cd98f00b204e9800998ecf8427e", + "fileSize": 1024, + "fileUrl": "http://example.com/firmware.bin", + "information": "{\"desc\":\"升级到最新版本\"}" + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java index 6d75f1cdd..08fc244b1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java @@ -1,24 +1,37 @@ package cn.iocoder.yudao.module.iot.controller.admin.device; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceStatusUpdateReqVO; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - IoT 设备") @RestController @@ -28,6 +41,10 @@ public class IotDeviceController { @Resource private IotDeviceService deviceService; + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + @Resource + private IotDeviceDownstreamService deviceDownstreamService; @PostMapping("/create") @Operation(summary = "创建设备") @@ -36,14 +53,6 @@ public class IotDeviceController { return success(deviceService.createDevice(createReqVO)); } - @PutMapping("/update-status") - @Operation(summary = "更新设备状态") - @PreAuthorize("@ss.hasPermission('iot:device:update')") - public CommonResult updateDeviceStatus(@Valid @RequestBody IotDeviceStatusUpdateReqVO updateReqVO) { - deviceService.updateDeviceStatus(updateReqVO); - return success(true); - } - @PutMapping("/update") @Operation(summary = "更新设备") @PreAuthorize("@ss.hasPermission('iot:device:update')") @@ -52,8 +61,18 @@ public class IotDeviceController { return success(true); } + // TODO @芋艿:参考阿里云:1)绑定网关;2)解绑网关 + + @PutMapping("/update-group") + @Operation(summary = "更新设备分组") + @PreAuthorize("@ss.hasPermission('iot:device:update')") + public CommonResult updateDeviceGroup(@Valid @RequestBody IotDeviceUpdateGroupReqVO updateReqVO) { + deviceService.updateDeviceGroup(updateReqVO); + return success(true); + } + @DeleteMapping("/delete") - @Operation(summary = "删除设备") + @Operation(summary = "删除单个设备") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('iot:device:delete')") public CommonResult deleteDevice(@RequestParam("id") Long id) { @@ -61,6 +80,15 @@ public class IotDeviceController { return success(true); } + @DeleteMapping("/delete-list") + @Operation(summary = "删除多个设备") + @Parameter(name = "ids", description = "编号数组", required = true) + @PreAuthorize("@ss.hasPermission('iot:device:delete')") + public CommonResult deleteDeviceList(@RequestParam("ids") Collection ids) { + deviceService.deleteDeviceList(ids); + return success(true); + } + @GetMapping("/get") @Operation(summary = "获得设备") @Parameter(name = "id", description = "编号", required = true, example = "1024") @@ -78,6 +106,19 @@ public class IotDeviceController { return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class)); } + @GetMapping("/export-excel") + @Operation(summary = "导出设备 Excel") + @PreAuthorize("@ss.hasPermission('iot:device:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDeviceExcel(@Valid IotDevicePageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + CommonResult> result = getDevicePage(exportReqVO); + // 导出 Excel + ExcelUtils.write(response, "设备.xls", "数据", IotDeviceRespVO.class, + result.getData().getList()); + } + @GetMapping("/count") @Operation(summary = "获得设备数量") @Parameter(name = "productId", description = "产品编号", example = "1") @@ -86,4 +127,62 @@ public class IotDeviceController { return success(deviceService.getDeviceCountByProductId(productId)); } + @GetMapping("/simple-list") + @Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项") + @Parameter(name = "deviceType", description = "设备类型", example = "1") + public CommonResult> getSimpleDeviceList( + @RequestParam(value = "deviceType", required = false) Integer deviceType) { + List list = deviceService.getDeviceListByDeviceType(deviceType); + return success(convertList(list, device -> // 只返回 id、name 字段 + new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName()))); + } + + @PostMapping("/import") + @Operation(summary = "导入设备") + @PreAuthorize("@ss.hasPermission('iot:device:import')") + public CommonResult importDevice( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) + throws Exception { + List list = ExcelUtils.read(file, IotDeviceImportExcelVO.class); + return success(deviceService.importDevice(list, updateSupport)); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得导入设备模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110") + .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(), + IotDeviceImportExcelVO.builder().deviceName("biubiu") + .productKey("YzvHxd4r67sT4s2B").groupNames("").build()); + // 输出 + ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list); + } + + @PostMapping("/upstream") + @Operation(summary = "设备上行", description = "可用于设备模拟") + @PreAuthorize("@ss.hasPermission('iot:device:upstream')") + public CommonResult upstreamDevice(@Valid @RequestBody IotDeviceUpstreamReqVO upstreamReqVO) { + deviceUpstreamService.upstreamDevice(upstreamReqVO); + return success(true); + } + + @PostMapping("/downstream") + @Operation(summary = "设备下行", description = "可用于设备模拟") + @PreAuthorize("@ss.hasPermission('iot:device:downstream')") + public CommonResult downstreamDevice(@Valid @RequestBody IotDeviceDownstreamReqVO downstreamReqVO) { + deviceDownstreamService.downstreamDevice(downstreamReqVO); + return success(true); + } + + // TODO @haohao:是不是默认详情接口,不返回 secret,然后这个接口,用于统一返回。然后接口名可以更通用一点。 + @GetMapping("/mqtt-connection-params") + @Operation(summary = "获取 MQTT 连接参数") + @PreAuthorize("@ss.hasPermission('iot:device:mqtt-connection-params')") + public CommonResult getMqttConnectionParams(@RequestParam("deviceId") Long deviceId) { + return success(deviceService.getMqttConnectionParams(deviceId)); + } + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java new file mode 100644 index 000000000..d19cf7fc9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceGroupController.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceGroupService; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 设备分组") +@RestController +@RequestMapping("/iot/device-group") +@Validated +public class IotDeviceGroupController { + + @Resource + private IotDeviceGroupService deviceGroupService; + @Resource + private IotDeviceService deviceService; + + @PostMapping("/create") + @Operation(summary = "创建设备分组") + @PreAuthorize("@ss.hasPermission('iot:device-group:create')") + public CommonResult createDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO createReqVO) { + return success(deviceGroupService.createDeviceGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新设备分组") + @PreAuthorize("@ss.hasPermission('iot:device-group:update')") + public CommonResult updateDeviceGroup(@Valid @RequestBody IotDeviceGroupSaveReqVO updateReqVO) { + deviceGroupService.updateDeviceGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除设备分组") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:device-group:delete')") + public CommonResult deleteDeviceGroup(@RequestParam("id") Long id) { + deviceGroupService.deleteDeviceGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得设备分组") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:device-group:query')") + public CommonResult getDeviceGroup(@RequestParam("id") Long id) { + IotDeviceGroupDO deviceGroup = deviceGroupService.getDeviceGroup(id); + return success(BeanUtils.toBean(deviceGroup, IotDeviceGroupRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得设备分组分页") + @PreAuthorize("@ss.hasPermission('iot:device-group:query')") + public CommonResult> getDeviceGroupPage(@Valid IotDeviceGroupPageReqVO pageReqVO) { + PageResult pageResult = deviceGroupService.getDeviceGroupPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceGroupRespVO.class, + group -> group.setDeviceCount(deviceService.getDeviceCountByGroupId(group.getId())))); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取设备分组的精简信息列表", description = "只包含被开启的分组,主要用于前端的下拉选项") + public CommonResult> getSimpleDeviceGroupList() { + List list = deviceGroupService.getDeviceGroupListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, group -> // 只返回 id、name 字段 + new IotDeviceGroupRespVO().setId(group.getId()).setName(group.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java new file mode 100644 index 000000000..81d1bff94 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceLogController.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 设备日志") +@RestController +@RequestMapping("/iot/device/log") +@Validated +public class IotDeviceLogController { + + @Resource + private IotDeviceLogService deviceLogService; + + @GetMapping("/page") + @Operation(summary = "获得设备日志分页") + @PreAuthorize("@ss.hasPermission('iot:device:log-query')") + public CommonResult> getDeviceLogPage(@Valid IotDeviceLogPageReqVO pageReqVO) { + PageResult pageResult = deviceLogService.getDeviceLogPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDeviceLogRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java new file mode 100644 index 000000000..47bf325dd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDevicePropertyController.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 设备属性") +@RestController +@RequestMapping("/iot/device/property") +@Validated +public class IotDevicePropertyController { + + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotDeviceService deviceService; + + @GetMapping("/latest") + @Operation(summary = "获取设备属性最新属性") + @Parameters({ + @Parameter(name = "deviceId", description = "设备编号", required = true), + @Parameter(name = "identifier", description = "标识符"), + @Parameter(name = "name", description = "名称") + }) + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getLatestDeviceProperties( + @RequestParam("deviceId") Long deviceId, + @RequestParam(value = "identifier", required = false) String identifier, + @RequestParam(value = "name", required = false) String name) { + Map properties = devicePropertyService.getLatestDeviceProperties(deviceId); + + // 拼接数据 + IotDeviceDO device = deviceService.getDevice(deviceId); + Assert.notNull(device, "设备不存在"); + List thingModels = thingModelService.getThingModelListByProductId(device.getProductId()); + return success(convertList(properties.entrySet(), entry -> { + IotThingModelDO thingModel = CollUtil.findOne(thingModels, + item -> item.getIdentifier().equals(entry.getKey())); + if (thingModel == null || thingModel.getProperty() == null) { + return null; + } + if (StrUtil.isNotEmpty(identifier) && !StrUtil.contains(thingModel.getIdentifier(), identifier)) { + return null; + } + if (StrUtil.isNotEmpty(name) && !StrUtil.contains(thingModel.getName(), name)) { + return null; + } + // 构建对象 + IotDevicePropertyDO property = entry.getValue(); + return new IotDevicePropertyRespVO().setProperty(thingModel.getProperty()) + .setValue(property.getValue()).setUpdateTime(LocalDateTimeUtil.toEpochMilli(property.getUpdateTime())); + })); + } + + @GetMapping("/history-page") + @Operation(summary = "获取设备属性历史数据") + @PreAuthorize("@ss.hasPermission('iot:device:property-query')") + public CommonResult> getHistoryDevicePropertyPage( + @Valid IotDevicePropertyHistoryPageReqVO pageReqVO) { + Assert.notEmpty(pageReqVO.getIdentifier(), "标识符不能为空"); + return success(devicePropertyService.getHistoryDevicePropertyPage(pageReqVO)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java deleted file mode 100644 index 26bdaca05..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDevicePageReqVO.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; -import org.springframework.format.annotation.DateTimeFormat; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; - -@Schema(description = "管理后台 - IoT 设备分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IotDevicePageReqVO extends PageParam { - - // TODO @芋艿:需要去掉一些多余的字段; - - @Schema(description = "设备唯一标识符", example = "24602") - private String deviceKey; - - @Schema(description = "设备名称", example = "王五") - private String deviceName; - - @Schema(description = "备注名称", example = "张三") - private String nickname; - - @Schema(description = "产品编号", example = "26202") - private Long productId; - - @Schema(description = "产品标识") - private String productKey; - - @Schema(description = "设备类型", example = "1") - @InEnum(IotProductDeviceTypeEnum.class) - private Integer deviceType; - - @Schema(description = "网关设备 ID", example = "16380") - private Long gatewayId; - - @Schema(description = "设备状态", example = "1") - @InEnum(IotDeviceStatusEnum.class) - private Integer status; - - @Schema(description = "设备状态最后更新时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] statusLastUpdateTime; - - @Schema(description = "最后上线时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] lastOnlineTime; - - @Schema(description = "最后离线时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] lastOfflineTime; - - @Schema(description = "设备激活时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] activeTime; - - @Schema(description = "设备密钥,用于设备认证,需安全存储") - private String deviceSecret; - - @Schema(description = "MQTT 客户端 ID", example = "24602") - private String mqttClientId; - - @Schema(description = "MQTT 用户名", example = "芋艿") - private String mqttUsername; - - @Schema(description = "MQTT 密码") - private String mqttPassword; - - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") - private String authType; - - @Schema(description = "创建时间") - @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) - private LocalDateTime[] createTime; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java deleted file mode 100644 index 620e5310f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceSaveReqVO.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") -@Data -public class IotDeviceSaveReqVO { - - @Schema(description = "设备编号", example = "177") - private Long id; - - @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") - private String deviceName; - - @Schema(description = "备注名称", example = "张三") - private String nickname; - - @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") - private Long productId; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java deleted file mode 100644 index a91a58690..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceStatusUpdateReqVO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; - -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 设备状态更新 Request VO") -@Data -public class IotDeviceStatusUpdateReqVO { - - @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "设备编号不能为空") - private Long id; - - @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "设备状态不能为空") - @InEnum(IotDeviceStatusEnum.class) - private Integer status; -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java new file mode 100644 index 000000000..eefaeffeb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceDownstreamReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备下行 Request VO") // 服务调用、属性设置、属性获取等 +@Data +public class IotDeviceDownstreamReqVO { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long id; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + @NotEmpty(message = "消息类型不能为空") + @InEnum(IotDeviceMessageTypeEnum.class) + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") + @NotEmpty(message = "标识符不能为空") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Object data; // 例如说:服务调用的 params、属性设置的 properties + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java new file mode 100644 index 000000000..778d75bba --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/control/IotDeviceUpstreamReqVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.control; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备上行 Request VO") // 属性上报、事件上报、状态变更等 +@Data +public class IotDeviceUpstreamReqVO { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long id; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + @NotEmpty(message = "消息类型不能为空") + @InEnum(IotDeviceMessageTypeEnum.class) + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "report") + @NotEmpty(message = "标识符不能为空") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举类 + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + private Object data; // 例如说:属性上报的 properties、事件上报的 params + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java new file mode 100644 index 000000000..fcf36994f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogPageReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备日志分页查询 Request VO") +@Data +public class IotDeviceLogPageReqVO extends PageParam { + + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") + @NotEmpty(message = "设备标识不能为空") + private String deviceKey; + + @Schema(description = "消息类型", example = "property") + private String type; // 参见 IotDeviceMessageTypeEnum 枚举,精准匹配 + + @Schema(description = "标识符", example = "temperature") + private String identifier; // 参见 IotDeviceMessageIdentifierEnum 枚举,模糊匹配 + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java new file mode 100644 index 000000000..6e6639ede --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDeviceLogRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备日志 Response VO") +@Data +public class IotDeviceLogRespVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123") + private String productKey; + + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123") + private String deviceKey; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property") + private String type; + + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") + private String identifier; + + @Schema(description = "日志内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String content; + + @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime reportTime; + + @Schema(description = "记录时间戳", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime ts; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java new file mode 100644 index 000000000..0de45e4a7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyHistoryPageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +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 = "管理后台 - IoT 设备属性历史分页 Request VO") +@Data +public class IotDevicePropertyHistoryPageReqVO extends PageParam { + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "177") + @NotNull(message = "设备编号不能为空") + private Long deviceId; + + @Schema(description = "设备 Key", hidden = true) + private String deviceKey; // 非前端传递,后端自己查询设置 + + @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "属性标识符不能为空") + private String identifier; + + @Schema(description = "时间范围", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Size(min = 2, max = 2, message = "请选择时间范围") + private LocalDateTime[] times; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java new file mode 100644 index 000000000..dd7a0d6ad --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/data/IotDevicePropertyRespVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.data; + +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备属性 Response VO") +@Data +public class IotDevicePropertyRespVO { + + @Schema(description = "属性定义", requiredMode = Schema.RequiredMode.REQUIRED) + private ThingModelProperty property; + + @Schema(description = "最新值", requiredMode = Schema.RequiredMode.REQUIRED) + private Object value; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Long updateTime; // 由于从 TDengine 查询出来的是 Long 类型,所以这里也使用 Long 类型 + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java new file mode 100644 index 000000000..710e74263 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 设备 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免设备导入有问题 +public class IotDeviceImportExcelVO { + + @ExcelProperty("设备名称") + @NotEmpty(message = "设备名称不能为空") + private String deviceName; + + @ExcelProperty("父设备名称") + @Schema(description = "父设备名称", example = "网关001") + private String parentDeviceName; + + @ExcelProperty("产品标识") + @NotEmpty(message = "产品标识不能为空") + private String productKey; + + @ExcelProperty("设备分组") + private String groupNames; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java new file mode 100644 index 000000000..bf52b123f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportRespVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - IoT 设备导入 Response VO") +@Data +@Builder +public class IotDeviceImportRespVO { + + @Schema(description = "创建成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createDeviceNames; + + @Schema(description = "更新成功的设备名称数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateDeviceNames; + + @Schema(description = "导入失败的设备集合,key为设备名称,value为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureDeviceNames; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java new file mode 100644 index 000000000..5ce68c0fe --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备 MQTT 连接参数 Response VO") +@Data +@ExcelIgnoreUnannotated +public class IotDeviceMqttConnectionParamsRespVO { + + @Schema(description = "MQTT 客户端 ID", example = "24602") + @ExcelProperty("MQTT 客户端 ID") + private String mqttClientId; + + @Schema(description = "MQTT 用户名", example = "芋艿") + @ExcelProperty("MQTT 用户名") + private String mqttUsername; + + @Schema(description = "MQTT 密码") + @ExcelProperty("MQTT 密码") + private String mqttPassword; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java new file mode 100644 index 000000000..686267732 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDevicePageReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备分页 Request VO") +@Data +public class IotDevicePageReqVO extends PageParam { + + @Schema(description = "设备名称", example = "王五") + private String deviceName; + + @Schema(description = "备注名称", example = "张三") + private String nickname; + + @Schema(description = "产品编号", example = "26202") + private Long productId; + + @Schema(description = "设备类型", example = "1") + @InEnum(IotProductDeviceTypeEnum.class) + private Integer deviceType; + + @Schema(description = "设备状态", example = "1") + @InEnum(IotDeviceStateEnum.class) + private Integer status; + + @Schema(description = "设备分组编号", example = "1024") + private Long groupId; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java similarity index 70% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java index 488f6b907..8404ca922 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/IotDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java @@ -1,12 +1,16 @@ -package cn.iocoder.yudao.module.iot.controller.admin.device.vo; +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Set; + +import static cn.iocoder.yudao.module.iot.enums.DictTypeConstants.DEVICE_STATE; @Schema(description = "管理后台 - IoT 设备 Response VO") @Data @@ -21,9 +25,24 @@ public class IotDeviceRespVO { private String deviceKey; @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") - @ExcelProperty("设备名称备") + @ExcelProperty("设备名称") private String deviceName; + @Schema(description = "设备备注名称", example = "张三") + @ExcelProperty("设备备注名称") + private String nickname; + + @Schema(description = "设备序列号", example = "1024") + @ExcelProperty("设备序列号") + private String serialNumber; + + @Schema(description = "设备图片", example = "我是一名码农") + @ExcelProperty("设备图片") + private String picUrl; + + @Schema(description = "设备分组编号数组", example = "1,2") + private Set groupIds; + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") @ExcelProperty("产品编号") private Long productId; @@ -36,28 +55,21 @@ public class IotDeviceRespVO { @ExcelProperty("设备类型") private Integer deviceType; - @Schema(description = "设备备注名称", example = "张三") - @ExcelProperty("设备备注名称") - private String nickname; - @Schema(description = "网关设备 ID", example = "16380") private Long gatewayId; @Schema(description = "设备状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("设备状态") - private Integer status; - - @Schema(description = "设备状态最后更新时间") - @ExcelProperty("设备状态最后更新时间") - private LocalDateTime statusLastUpdateTime; + @ExcelProperty(value = "设备状态", converter = DictConvert.class) + @DictFormat(DEVICE_STATE) + private Integer state; @Schema(description = "最后上线时间") @ExcelProperty("最后上线时间") - private LocalDateTime lastOnlineTime; + private LocalDateTime onlineTime; @Schema(description = "最后离线时间") @ExcelProperty("最后离线时间") - private LocalDateTime lastOfflineTime; + private LocalDateTime offlineTime; @Schema(description = "设备激活时间") @ExcelProperty("设备激活时间") @@ -67,22 +79,13 @@ public class IotDeviceRespVO { @ExcelProperty("设备密钥") private String deviceSecret; - @Schema(description = "MQTT 客户端 ID", example = "24602") - @ExcelProperty("MQTT 客户端 ID") - private String mqttClientId; - - @Schema(description = "MQTT 用户名", example = "芋艿") - @ExcelProperty("MQTT 用户名") - private String mqttUsername; - - @Schema(description = "MQTT 密码") - @ExcelProperty("MQTT 密码") - private String mqttPassword; - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2") @ExcelProperty("认证类型(如一机一密、动态注册)") private String authType; + @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") + private String config; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("创建时间") private LocalDateTime createTime; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java new file mode 100644 index 000000000..b9ea9b99f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备新增/修改 Request VO") +@Data +public class IotDeviceSaveReqVO { + + @Schema(description = "设备编号", example = "177") + private Long id; + + @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177") + @Size(max = 50, message = "设备编号长度不能超过 50 个字符") + private String deviceKey; + + @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.AUTO, example = "王五") + private String deviceName; + + @Schema(description = "备注名称", example = "张三") + private String nickname; + + @Schema(description = "设备序列号", example = "123456") + private String serialNumber; + + @Schema(description = "设备图片", example = "https://iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "设备分组编号数组", example = "1,2") + private Set groupIds; + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26202") + private Long productId; + + @Schema(description = "网关设备 ID", example = "16380") + private Long gatewayId; + + @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") + private String config; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java new file mode 100644 index 000000000..bf66fbf98 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUpdateGroupReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Set; + +@Schema(description = "管理后台 - IoT 设备更新分组 Request VO") +@Data +public class IotDeviceUpdateGroupReqVO { + + @Schema(description = "设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "设备编号列表不能为空") + private Set ids; + + @Schema(description = "分组编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotEmpty(message = "分组编号列表不能为空") + private Set groupIds; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java new file mode 100644 index 000000000..93b1a1ead --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +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 = "管理后台 - IoT 设备分组分页 Request VO") +@Data +public class IotDeviceGroupPageReqVO extends PageParam { + + @Schema(description = "分组名字", example = "李四") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java new file mode 100644 index 000000000..4fd541502 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupRespVO.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 设备分组 Response VO") +@Data +public class IotDeviceGroupRespVO { + + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583") + private Long id; + + @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "分组描述", example = "你说的对") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long deviceCount; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java new file mode 100644 index 000000000..491cd9366 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/group/IotDeviceGroupSaveReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 设备分组新增/修改 Request VO") +@Data +public class IotDeviceGroupSaveReqVO { + + @Schema(description = "分组 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "3583") + private Long id; + + @Schema(description = "分组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "分组名字不能为空") + private String name; + + @Schema(description = "分组状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分组状态不能为空") + private Integer status; + + @Schema(description = "分组描述", example = "你说的对") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java new file mode 100644 index 000000000..6cc3918e8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaFirmwareService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 固件") +@RestController +@RequestMapping("/iot/ota-firmware") +@Validated +public class IotOtaFirmwareController { + + @Resource + private IotOtaFirmwareService otaFirmwareService; + + @PostMapping("/create") + @Operation(summary = "创建 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:create')") + public CommonResult createOtaFirmware(@Valid @RequestBody IotOtaFirmwareCreateReqVO createReqVO) { + return success(otaFirmwareService.createOtaFirmware(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:update')") + public CommonResult updateOtaFirmware(@Valid @RequestBody IotOtaFirmwareUpdateReqVO updateReqVO) { + otaFirmwareService.updateOtaFirmware(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 OTA 固件") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") + public CommonResult getOtaFirmware(@RequestParam("id") Long id) { + IotOtaFirmwareDO otaFirmware = otaFirmwareService.getOtaFirmware(id); + return success(BeanUtils.toBean(otaFirmware, IotOtaFirmwareRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得 OTA 固件分页") + @PreAuthorize("@ss.hasPermission('iot:ota-firmware:query')") + public CommonResult> getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO) { + PageResult pageResult = otaFirmwareService.getOtaFirmwarePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaFirmwareRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java new file mode 100644 index 000000000..f6bc526ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 升级记录") +@RestController +@RequestMapping("/iot/ota-upgrade-record") +@Validated +public class IotOtaUpgradeRecordController { + + @Resource + private IotOtaUpgradeRecordService upgradeRecordService; + + @GetMapping("/get-statistics") + @Operation(summary = "固件升级设备统计") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + @Parameter(name = "firmwareId", description = "固件编号", required = true, example = "1024") + public CommonResult> getOtaUpgradeRecordStatistics(@RequestParam(value = "firmwareId") Long firmwareId) { + return success(upgradeRecordService.getOtaUpgradeRecordStatistics(firmwareId)); + } + + @GetMapping("/get-count") + @Operation(summary = "获得升级记录分页 tab 数量") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + public CommonResult> getOtaUpgradeRecordCount( + @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { + return success(upgradeRecordService.getOtaUpgradeRecordCount(pageReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得升级记录分页") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + public CommonResult> getUpgradeRecordPage( + @Valid IotOtaUpgradeRecordPageReqVO pageReqVO) { + PageResult pageResult = upgradeRecordService.getUpgradeRecordPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaUpgradeRecordRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:query')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult getUpgradeRecord(@RequestParam("id") Long id) { + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordService.getUpgradeRecord(id); + return success(BeanUtils.toBean(upgradeRecord, IotOtaUpgradeRecordRespVO.class)); + } + + @PutMapping("/retry") + @Operation(summary = "重试升级记录") + @PreAuthorize("@ss.hasPermission('iot:ota-upgrade-record:retry')") + @Parameter(name = "id", description = "升级记录编号", required = true, example = "1024") + public CommonResult retryUpgradeRecord(@RequestParam("id") Long id) { + upgradeRecordService.retryUpgradeRecord(id); + return success(true); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java new file mode 100644 index 000000000..e248e8027 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeTaskController.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.service.ota.IotOtaUpgradeTaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT OTA 升级任务") +@RestController +@RequestMapping("/iot/ota-upgrade-task") +@Validated +public class IotOtaUpgradeTaskController { + + @Resource + private IotOtaUpgradeTaskService upgradeTaskService; + + @PostMapping("/create") + @Operation(summary = "创建升级任务") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:create')") + public CommonResult createUpgradeTask(@Valid @RequestBody IotOtaUpgradeTaskSaveReqVO createReqVO) { + return success(upgradeTaskService.createUpgradeTask(createReqVO)); + } + + @PostMapping("/cancel") + @Operation(summary = "取消升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true) + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:cancel')") + public CommonResult cancelUpgradeTask(@RequestParam("id") Long id) { + upgradeTaskService.cancelUpgradeTask(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得升级任务分页") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") + public CommonResult> getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO) { + PageResult pageResult = upgradeTaskService.getUpgradeTaskPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotOtaUpgradeTaskRespVO.class)); + } + + @GetMapping("/get") + @Operation(summary = "获得升级任务") + @Parameter(name = "id", description = "升级任务编号", required = true, example = "1024") + @PreAuthorize(value = "@ss.hasPermission('iot:ota-upgrade-task:query')") + public CommonResult getUpgradeTask(@RequestParam("id") Long id) { + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(id); + return success(BeanUtils.toBean(upgradeTask, IotOtaUpgradeTaskRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java new file mode 100644 index 000000000..50c2ece15 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareCreateReqVO.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "管理后台 - IoT OTA 固件创建 Request VO") +@Data +public class IotOtaFirmwareCreateReqVO { + + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") + @NotEmpty(message = "固件名称不能为空") + private String name; + + @Schema(description = "固件描述", example = "某品牌型号固件,测试用") + private String description; + + @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") + @NotEmpty(message = "版本号不能为空") + private String version; + + @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private String productId; + + @Schema(description = "签名方式", example = "MD5") + // TODO @li:是不是必传哈 + private String signMethod; + + @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn/yudao-firmware.zip") + @NotEmpty(message = "固件文件 URL 不能为空") + private String fileUrl; + + @Schema(description = "自定义信息,建议使用 JSON 格式", example = "{\"key1\":\"value1\",\"key2\":\"value2\"}") + private String information; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java new file mode 100644 index 000000000..baa741029 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwarePageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "管理后台 - IoT OTA 固件分页 Request VO") +public class IotOtaFirmwarePageReqVO extends PageParam { + + /** + * 固件名称 + */ + @Schema(description = "固件名称", example = "智能开关固件") + private String name; + + /** + * 产品标识 + */ + @Schema(description = "产品标识", example = "1024") + private String productId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java new file mode 100644 index 000000000..735618781 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareRespVO.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 固件 Response VO") +public class IotOtaFirmwareRespVO implements VO { + + /** + * 固件编号 + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 固件名称 + */ + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "OTA固件") + private String name; + /** + * 固件描述 + */ + @Schema(description = "固件描述") + private String description; + /** + * 版本号 + */ + @Schema(description = "版本号", requiredMode = REQUIRED, example = "1.0.0") + private String version; + + /** + * 产品编号 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + @Schema(description = "产品编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotProductDO.class, fields = {"name"}, refs = {"productName"}) + private String productId; + /** + * 产品标识 + *

+ * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} + */ + @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot-product-key") + private String productKey; + /** + * 产品名称 + */ + @Schema(description = "产品名称", requiredMode = REQUIRED, example = "OTA产品") + private String productName; + /** + * 签名方式 + *

+ * 例如说:MD5、SHA256 + */ + @Schema(description = "签名方式", example = "MD5") + private String signMethod; + /** + * 固件文件签名 + */ + @Schema(description = "固件文件签名", example = "1024") + private String fileSign; + /** + * 固件文件大小 + */ + @Schema(description = "固件文件大小", requiredMode = REQUIRED, example = "1024") + private Long fileSize; + /** + * 固件文件 URL + */ + @Schema(description = "固件文件 URL", requiredMode = REQUIRED, example = "https://www.iocoder.cn") + private String fileUrl; + /** + * 自定义信息,建议使用 JSON 格式 + */ + @Schema(description = "自定义信息,建议使用 JSON 格式") + private String information; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java new file mode 100644 index 000000000..aa134bcee --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/firmware/IotOtaFirmwareUpdateReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "管理后台 - IoT OTA 固件更新 Request VO") +@Data +public class IotOtaFirmwareUpdateReqVO { + + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "固件编号不能为空") + private Long id; + + // TODO @li:name 是不是可以飞必传哈 + @Schema(description = "固件名称", requiredMode = REQUIRED, example = "智能开关固件") + @NotEmpty(message = "固件名称不能为空") + private String name; + + @Schema(description = "固件描述", example = "某品牌型号固件,测试用") + private String description; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java new file mode 100644 index 000000000..2b21b3079 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordPageReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级记录分页 Request VO") +public class IotOtaUpgradeRecordPageReqVO extends PageParam { + + // TODO @li:已经有注解,不用重复注释 + /** + * 升级任务编号字段。 + *

+ * 该字段用于标识升级任务的唯一编号,不能为空。 + */ + @Schema(description = "升级任务编号", requiredMode = REQUIRED, example = "1024") + @NotNull(message = "升级任务编号不能为空") + private Long taskId; + + /** + * 设备标识字段。 + *

+ * 该字段用于标识设备的名称,通常用于区分不同的设备。 + */ + @Schema(description = "设备标识", requiredMode = REQUIRED, example = "摄像头A1-1") + private String deviceName; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java new file mode 100644 index 000000000..db6737feb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/record/IotOtaUpgradeRecordRespVO.java @@ -0,0 +1,109 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级记录 Response VO") +public class IotOtaUpgradeRecordRespVO { + + /** + * 升级记录编号 + */ + @Schema(description = "升级记录编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"firmwareVersion"}) + private Long firmwareId; + /** + * 固件版本 + */ + @Schema(description = "固件版本", requiredMode = REQUIRED, example = "v1.0.0") + private String firmwareVersion; + /** + * 任务编号 + *

+ * 关联 {@link IotOtaUpgradeTaskDO#getId()} + */ + @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") + private Long taskId; + /** + * 产品标识 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + @Schema(description = "产品标识", requiredMode = REQUIRED, example = "iot") + private String productKey; + /** + * 设备名称 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + @Schema(description = "设备名称", requiredMode = REQUIRED, example = "iot") + private String deviceName; + /** + * 设备编号 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + @Schema(description = "设备编号", requiredMode = REQUIRED, example = "1024") + private String deviceId; + /** + * 来源的固件编号 + *

+ * 关联 {@link IotDeviceDO#getFirmwareId()} + */ + @Schema(description = "来源的固件编号", requiredMode = REQUIRED, example = "1024") + @Trans(type = TransType.SIMPLE, target = IotOtaFirmwareDO.class, fields = {"version"}, refs = {"fromFirmwareVersion"}) + private Long fromFirmwareId; + /** + * 来源的固件版本 + */ + @Schema(description = "来源的固件版本", requiredMode = REQUIRED, example = "v1.0.0") + private String fromFirmwareVersion; + /** + * 升级状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + @Schema(description = "升级状态", requiredMode = REQUIRED, allowableValues = {"0", "10", "20", "30", "40", "50"}) + private Integer status; + /** + * 升级进度,百分比 + */ + @Schema(description = "升级进度,百分比", requiredMode = REQUIRED, example = "10") + private Integer progress; + /** + * 升级进度描述 + *

+ * 注意,只记录设备最后一次的升级进度描述 + * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 + */ + @Schema(description = "升级进度描述", requiredMode = REQUIRED, example = "10") + private String description; + /** + * 升级开始时间 + */ + @Schema(description = "升级开始时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime startTime; + /** + * 升级结束时间 + */ + @Schema(description = "升级结束时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime endTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java new file mode 100644 index 000000000..d2b1926aa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskPageReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务分页 Request VO") +public class IotOtaUpgradeTaskPageReqVO extends PageParam { + + /** + * 任务名称字段,用于描述任务的名称 + */ + @Schema(description = "任务名称", example = "升级任务") + private String name; + + /** + * 固件编号字段,用于唯一标识固件,不能为空 + */ + @NotNull(message = "固件编号不能为空") + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java new file mode 100644 index 000000000..dbc29618f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskRespVO.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务 Response VO") +public class IotOtaUpgradeTaskRespVO implements VO { + + /** + * 任务编号 + */ + @Schema(description = "任务编号", requiredMode = REQUIRED, example = "1024") + private Long id; + /** + * 任务名称 + */ + @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") + private String name; + /** + * 任务描述 + */ + @Schema(description = "任务描述", example = "升级任务") + private String description; + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + /** + * 任务状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} + */ + @Schema(description = "任务状态", requiredMode = REQUIRED, allowableValues = {"10", "20", "21", "30"}) + private Integer status; + /** + * 任务状态名称 + */ + @Schema(description = "任务状态名称", requiredMode = REQUIRED, example = "进行中") + private String statusName; + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + @Schema(description = "升级范围", requiredMode = REQUIRED, allowableValues = {"1", "2"}) + private Integer scope; + /** + * 设备数量 + */ + @Schema(description = "设备数量", requiredMode = REQUIRED, example = "1024") + private Long deviceCount; + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @Schema(description = "选中的设备编号数组", example = "1024") + private List deviceIds; + /** + * 选中的设备名字数组 + *

+ * 关联 {@link IotDeviceDO#getDeviceName()} + */ + @Schema(description = "选中的设备名字数组", example = "1024") + private List deviceNames; + /** + * 创建时间 + */ + @Schema(description = "创建时间", requiredMode = REQUIRED, example = "2022-07-08 07:30:00") + private LocalDateTime createTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java new file mode 100644 index 000000000..0ace17a04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/vo/upgrade/task/IotOtaUpgradeTaskSaveReqVO.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Data +@Schema(description = "管理后台 - IoT OTA 升级任务创建/修改 Request VO") +public class IotOtaUpgradeTaskSaveReqVO { + + // TODO @li:已经有注解,不用重复注释 + // TODO @li: @Schema 写在参数校验前面。先有定义;其他的,也检查下; + + /** + * 任务名称 + */ + @NotEmpty(message = "任务名称不能为空") + @Schema(description = "任务名称", requiredMode = REQUIRED, example = "升级任务") + private String name; + + /** + * 任务描述 + */ + @Schema(description = "任务描述", example = "升级任务") + private String description; + + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + @NotNull(message = "固件编号不能为空") + @Schema(description = "固件编号", requiredMode = REQUIRED, example = "1024") + private Long firmwareId; + + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + @NotNull(message = "升级范围不能为空") + @InEnum(value = IotOtaUpgradeTaskScopeEnum.class) + @Schema(description = "升级范围", requiredMode = REQUIRED, example = "1") + private Integer scope; + + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @Schema(description = "选中的设备编号数组", requiredMode = REQUIRED, example = "[1,2,3,4]") + private List deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java new file mode 100644 index 000000000..e21b10241 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/PluginConfigController.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigImportReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigStatusReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 插件配置") +@RestController +@RequestMapping("/iot/plugin-config") +@Validated +public class PluginConfigController { + + @Resource + private IotPluginConfigService pluginConfigService; + + @PostMapping("/create") + @Operation(summary = "创建插件配置") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:create')") + public CommonResult createPluginConfig(@Valid @RequestBody PluginConfigSaveReqVO createReqVO) { + return success(pluginConfigService.createPluginConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新插件配置") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult updatePluginConfig(@Valid @RequestBody PluginConfigSaveReqVO updateReqVO) { + pluginConfigService.updatePluginConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除插件配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:plugin-config:delete')") + public CommonResult deletePluginConfig(@RequestParam("id") Long id) { + pluginConfigService.deletePluginConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得插件配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") + public CommonResult getPluginConfig(@RequestParam("id") Long id) { + IotPluginConfigDO pluginConfig = pluginConfigService.getPluginConfig(id); + return success(BeanUtils.toBean(pluginConfig, PluginConfigRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得插件配置分页") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:query')") + public CommonResult> getPluginConfigPage(@Valid PluginConfigPageReqVO pageReqVO) { + PageResult pageResult = pluginConfigService.getPluginConfigPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, PluginConfigRespVO.class)); + } + + @PostMapping("/upload-file") + @Operation(summary = "上传插件文件") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult uploadFile(@Valid PluginConfigImportReqVO reqVO) { + pluginConfigService.uploadFile(reqVO.getId(), reqVO.getFile()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改插件状态") + @PreAuthorize("@ss.hasPermission('iot:plugin-config:update')") + public CommonResult updatePluginConfigStatus(@Valid @RequestBody PluginConfigStatusReqVO reqVO) { + pluginConfigService.updatePluginStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java new file mode 100644 index 000000000..b9b277a54 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigImportReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +@Schema(description = "管理后台 - IoT 插件上传 Request VO") +@Data +public class PluginConfigImportReqVO { + + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "插件文件不能为空") + private MultipartFile file; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java new file mode 100644 index 000000000..1666d5d6b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置分页 Request VO") +@Data +public class PluginConfigPageReqVO extends PageParam { + + @Schema(description = "插件名称", example = "http") + private String name; + + @Schema(description = "状态", example = "1") + @InEnum(IotPluginStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java new file mode 100644 index 000000000..2b8c4dcde --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 插件配置 Response VO") +@Data +public class PluginConfigRespVO { + + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") + private String pluginKey; + + @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "描述", example = "你猜") + private String description; + + @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer deployType; + + @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileName; + + @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) + private String version; + + @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "设备插件协议类型") + private String protocol; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer status; + + @Schema(description = "插件配置项描述信息") + private String configSchema; + + @Schema(description = "插件配置信息") + private String config; + + @Schema(description = "插件脚本") + private String script; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java new file mode 100644 index 000000000..e48869d64 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigSaveReqVO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置新增/修改 Request VO") +@Data +public class PluginConfigSaveReqVO { + + // TODO @haohao:新增的字段有点多,每个都需要哇? + + // TODO @haohao:一些枚举字段,需要加枚举校验。例如说,deployType、status、type 等 + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627") + private String pluginKey; + + @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "描述", example = "你猜") + private String description; + + @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer deployType; + + @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileName; + + @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED) + private String version; + + @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer type; + + @Schema(description = "设备插件协议类型") + private String protocol; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @InEnum(IotPluginStatusEnum.class) + private Integer status; + + @Schema(description = "插件配置项描述信息") + private String configSchema; + + @Schema(description = "插件配置信息") + private String config; + + @Schema(description = "插件脚本") + private String script; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java new file mode 100644 index 000000000..eae4aa0a2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/config/PluginConfigStatusReqVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 插件配置状态 Request VO") +@Data +public class PluginConfigStatusReqVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @InEnum(IotPluginStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java new file mode 100644 index 000000000..e58b88856 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstancePageReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; + +import lombok.*; +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; + +// TODO @haohao:后续需要使用下 +@Schema(description = "管理后台 - IoT 插件实例分页 Request VO") +@Data +public class PluginInstancePageReqVO extends PageParam { + + @Schema(description = "插件主程序编号", example = "23738") + private String mainId; + + @Schema(description = "插件id", example = "26498") + private Long pluginId; + + @Schema(description = "插件主程序所在ip") + private String ip; + + @Schema(description = "插件主程序端口") + private Integer port; + + @Schema(description = "心跳时间,心路时间超过30秒需要剔除") + private Long heartbeatAt; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java new file mode 100644 index 000000000..cba59fdaf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/instance/PluginInstanceRespVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +// TODO @haohao:后续需要使用下 +@Schema(description = "管理后台 - IoT 插件实例 Response VO") +@Data +public class PluginInstanceRespVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23864") + private Long id; + + @Schema(description = "插件主程序id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738") + private String mainId; + + @Schema(description = "插件id", requiredMode = Schema.RequiredMode.REQUIRED, example = "26498") + private Long pluginId; + + @Schema(description = "插件主程序所在ip", requiredMode = Schema.RequiredMode.REQUIRED) + private String ip; + + @Schema(description = "插件主程序端口", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer port; + + @Schema(description = "心跳时间,心路时间超过30秒需要剔除", requiredMode = Schema.RequiredMode.REQUIRED) + private Long heartbeatAt; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java new file mode 100644 index 000000000..bc1c1fbf2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductCategoryController.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - IoT 产品分类") +@RestController +@RequestMapping("/iot/product-category") +@Validated +public class IotProductCategoryController { + + @Resource + private IotProductCategoryService productCategoryService; + + @PostMapping("/create") + @Operation(summary = "创建产品分类") + @PreAuthorize("@ss.hasPermission('iot:product-category:create')") + public CommonResult createProductCategory(@Valid @RequestBody IotProductCategorySaveReqVO createReqVO) { + return success(productCategoryService.createProductCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新产品分类") + @PreAuthorize("@ss.hasPermission('iot:product-category:update')") + public CommonResult updateProductCategory(@Valid @RequestBody IotProductCategorySaveReqVO updateReqVO) { + productCategoryService.updateProductCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除产品分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:product-category:delete')") + public CommonResult deleteProductCategory(@RequestParam("id") Long id) { + productCategoryService.deleteProductCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得产品分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult getProductCategory(@RequestParam("id") Long id) { + IotProductCategoryDO productCategory = productCategoryService.getProductCategory(id); + return success(BeanUtils.toBean(productCategory, IotProductCategoryRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得产品分类分页") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult> getProductCategoryPage(@Valid IotProductCategoryPageReqVO pageReqVO) { + PageResult pageResult = productCategoryService.getProductCategoryPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotProductCategoryRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得所有产品分类列表") + @PreAuthorize("@ss.hasPermission('iot:product-category:query')") + public CommonResult> getSimpleProductCategoryList() { + List list = productCategoryService.getProductCategoryListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, category -> + new IotProductCategoryRespVO().setId(category.getId()).setName(category.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java index 5b0ecb27a..2d8c85640 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java @@ -1,26 +1,36 @@ package cn.iocoder.yudao.module.iot.controller.admin.product; +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSimpleRespVO; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.io.IOException; import java.util.List; +import java.util.Map; +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - IoT 产品") @RestController @@ -30,6 +40,8 @@ public class IotProductController { @Resource private IotProductService productService; + @Resource + private IotProductCategoryService categoryService; @PostMapping("/create") @Operation(summary = "创建产品") @@ -72,7 +84,13 @@ public class IotProductController { @PreAuthorize("@ss.hasPermission('iot:product:query')") public CommonResult getProduct(@RequestParam("id") Long id) { IotProductDO product = productService.getProduct(id); - return success(BeanUtils.toBean(product, IotProductRespVO.class)); + // 拼接数据 + IotProductCategoryDO category = categoryService.getProductCategory(product.getCategoryId()); + return success(BeanUtils.toBean(product, IotProductRespVO.class, bean -> { + if (category != null) { + bean.setCategoryName(category.getName()); + } + })); } @GetMapping("/page") @@ -80,16 +98,35 @@ public class IotProductController { @PreAuthorize("@ss.hasPermission('iot:product:query')") public CommonResult> getProductPage(@Valid IotProductPageReqVO pageReqVO) { PageResult pageResult = productService.getProductPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotProductRespVO.class)); + // 拼接数据 + Map categoryMap = categoryService.getProductCategoryMap( + convertList(pageResult.getList(), IotProductDO::getCategoryId)); + return success(BeanUtils.toBean(pageResult, IotProductRespVO.class, bean -> { + MapUtils.findAndThen(categoryMap, bean.getCategoryId(), + category -> bean.setCategoryName(category.getName())); + })); } - // TODO @haohao:改成 simple-list 哈 - @GetMapping("/list-all-simple") - @Operation(summary = "获得所有产品列表") - @PreAuthorize("@ss.hasPermission('iot:product:query')") - public CommonResult> listAllSimpleProducts() { + @GetMapping("/export-excel") + @Operation(summary = "导出产品 Excel") + @PreAuthorize("@ss.hasPermission('iot:product:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportProductExcel(@Valid IotProductPageReqVO exportReqVO, + HttpServletResponse response) throws IOException { + exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + CommonResult> result = getProductPage(exportReqVO); + // 导出 Excel + ExcelUtils.write(response, "产品.xls", "数据", IotProductRespVO.class, + result.getData().getList()); + } + + @GetMapping("/simple-list") + @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleProductList() { List list = productService.getProductList(); - return success(BeanUtils.toBean(list, IotProductSimpleRespVO.class)); + return success(convertList(list, product -> // 只返回 id、name 字段 + new IotProductRespVO().setId(product.getId()).setName(product.getName()) + .setDeviceType(product.getDeviceType()))); } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java deleted file mode 100644 index 83855eaaf..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSimpleRespVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - IoT 产品 Response VO") -@Data -public class IotProductSimpleRespVO { - - @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26087") - private Long id; - - @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") - private String name; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java new file mode 100644 index 000000000..f1c12bf7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryPageReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +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 = "管理后台 - IoT 产品分类分页 Request VO") +@Data +public class IotProductCategoryPageReqVO extends PageParam { + + @Schema(description = "分类名字", example = "王五") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java new file mode 100644 index 000000000..d684b0215 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategoryRespVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 产品分类 Response VO") +@Data +public class IotProductCategoryRespVO { + + @Schema(description = "分类 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25284") + private Long id; + + @Schema(description = "分类名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "分类排序") + private Integer sort; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @Schema(description = "分类描述", example = "随便") + private String description; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java new file mode 100644 index 000000000..a7b2fe427 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/category/IotProductCategorySaveReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.category; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品分类新增/修改 Request VO") +@Data +public class IotProductCategorySaveReqVO { + + @Schema(description = "分类 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "25284") + private Long id; + + @Schema(description = "分类名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "分类名字不能为空") + private String name; + + @Schema(description = "分类排序") + private Integer sort; + + @Schema(description = "分类状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "分类状态不能为空") + private Integer status; + + @Schema(description = "分类描述", example = "随便") + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java index 3437f563f..18c69c4ce 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductPageReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductPageReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; @@ -8,8 +8,6 @@ import lombok.ToString; @Schema(description = "管理后台 - IoT 产品分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class IotProductPageReqVO extends PageParam { @Schema(description = "产品名称", example = "李四") diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java similarity index 56% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index 0958b3e84..f674651d5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -1,5 +1,8 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; +import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; @@ -20,48 +23,65 @@ public class IotProductRespVO { @ExcelProperty("产品名称") private String name; - @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) - @ExcelProperty("创建时间") - private LocalDateTime createTime; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) @ExcelProperty("产品标识") private String productKey; + @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long categoryId; + + @Schema(description = "产品分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("产品分类") + private String categoryName; + + @Schema(description = "产品图标", example = "https://iocoder.cn/1.svg") + @ExcelProperty("产品图标") + private String icon; + + @Schema(description = "产品图片", example = "https://iocoder.cn/1.png") + @ExcelProperty("产品图片") + private String picUrl; + + @Schema(description = "产品描述", example = "你猜") + @ExcelProperty("产品描述") + private String description; + + @Schema(description = "产品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "产品状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_STATUS) + private Integer status; + + @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @ExcelProperty(value = "设备类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_DEVICE_TYPE) + private Integer deviceType; + + @Schema(description = "联网方式", example = "2") + @ExcelProperty(value = "联网方式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.NET_TYPE) + private Integer netType; + @Schema(description = "接入网关协议", example = "2") - @ExcelProperty("接入网关协议") + @ExcelProperty(value = "接入网关协议", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PROTOCOL_TYPE) private Integer protocolType; @Schema(description = "协议编号(脚本解析 id)", requiredMode = Schema.RequiredMode.REQUIRED, example = "13177") @ExcelProperty("协议编号(脚本解析 id)") private Long protocolId; - @Schema(description = "产品所属品类标识符", example = "14237") - @ExcelProperty("产品所属品类标识符") - private Long categoryId; - - @Schema(description = "产品描述", example = "你猜") - @ExcelProperty("产品描述") - private String description; - - @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("数据校验级别") - private Integer validateType; - - @Schema(description = "产品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @ExcelProperty("产品状态") - private Integer status; - - @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") - @ExcelProperty("设备类型") - private Integer deviceType; - - @Schema(description = "联网方式", example = "2") - @ExcelProperty("联网方式") - private Integer netType; - @Schema(description = "数据格式") - @ExcelProperty("数据格式") + @ExcelProperty(value = "数据格式", converter = DictConvert.class) + @DictFormat(DictTypeConstants.DATA_FORMAT) private Integer dataFormat; + @Schema(description = "数据校验级别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty(value = "数据校验级别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.VALIDATE_TYPE) + private Integer validateType; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java similarity index 78% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java index 254b6b9da..268ab7c6f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/IotProductSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.iot.controller.admin.product.vo; +package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.iot.enums.product.*; @@ -14,13 +14,26 @@ public class IotProductSaveReqVO { @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.AUTO, example = "1") private Long id; - @Schema(description = "产品Key", requiredMode = Schema.RequiredMode.AUTO, example = "12345abc") - private String productKey; - @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温湿度") @NotEmpty(message = "产品名称不能为空") private String name; + @Schema(description = "产品 Key", requiredMode = Schema.RequiredMode.AUTO, example = "12345abc") + private String productKey; + + @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "产品分类编号不能为空") + private Long categoryId; + + @Schema(description = "产品图标", example = "https://iocoder.cn/1.svg") + private String icon; + + @Schema(description = "产品图片", example = "https://iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "产品描述", example = "描述") + private String description; + @Schema(description = "设备类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") @InEnum(value = IotProductDeviceTypeEnum.class, message = "设备类型必须是 {value}") @NotNull(message = "设备类型不能为空") @@ -44,7 +57,4 @@ public class IotProductSaveReqVO { @NotNull(message = "数据校验级别不能为空") private Integer validateType; - @Schema(description = "产品描述", example = "描述") - private String description; - } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java new file mode 100644 index 000000000..95e50a4a2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotDataBridgeController.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 数据桥梁") +@RestController +@RequestMapping("/iot/data-bridge") +@Validated +public class IotDataBridgeController { + + @Resource + private IotDataBridgeService dataBridgeService; + + @PostMapping("/create") + @Operation(summary = "创建数据桥梁") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:create')") + public CommonResult createDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO createReqVO) { + return success(dataBridgeService.createDataBridge(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据桥梁") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:update')") + public CommonResult updateDataBridge(@Valid @RequestBody IotDataBridgeSaveReqVO updateReqVO) { + dataBridgeService.updateDataBridge(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据桥梁") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:data-bridge:delete')") + public CommonResult deleteDataBridge(@RequestParam("id") Long id) { + dataBridgeService.deleteDataBridge(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据桥梁") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") + public CommonResult getDataBridge(@RequestParam("id") Long id) { + IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(id); + return success(BeanUtils.toBean(dataBridge, IotDataBridgeRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得数据桥梁分页") + @PreAuthorize("@ss.hasPermission('iot:data-bridge:query')") + public CommonResult> getDataBridgePage(@Valid IotDataBridgePageReqVO pageReqVO) { + PageResult pageResult = dataBridgeService.getDataBridgePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotDataBridgeRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java new file mode 100644 index 000000000..04e2f4570 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/IotRuleSceneController.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule; + +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "管理后台 - IoT 规则场景") +@RestController +@RequestMapping("/iot/rule-scene") +@Validated +public class IotRuleSceneController { + + @Resource + private IotRuleSceneService ruleSceneService; + + @GetMapping("/test") + @PermitAll + public void test() { + ruleSceneService.test(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java new file mode 100644 index 000000000..e4dc36ef9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgePageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +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 = "管理后台 - IoT 数据桥梁分页 Request VO") +@Data +public class IotDataBridgePageReqVO extends PageParam { + + @Schema(description = "桥梁名称", example = "赵六") + private String name; + + @Schema(description = "桥梁状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java new file mode 100644 index 000000000..38e04b2eb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeRespVO.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - IoT 数据桥梁 Response VO") +@Data +public class IotDataBridgeRespVO { + + @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") + private Long id; + + @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + private String name; + + @Schema(description = "桥梁描述", example = "随便") + private String description; + + @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer direction; + + @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "桥梁配置") + private IotDataBridgeAbstractConfig config; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java new file mode 100644 index 000000000..8441701af --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/IotDataBridgeSaveReqVO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 数据桥梁新增/修改 Request VO") +@Data +public class IotDataBridgeSaveReqVO { + + @Schema(description = "桥梁编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18564") + private Long id; + + @Schema(description = "桥梁名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotEmpty(message = "桥梁名称不能为空") + private String name; + + @Schema(description = "桥梁描述", example = "随便") + private String description; + + @Schema(description = "桥梁状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "桥梁状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "桥梁方向", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "桥梁方向不能为空") + @InEnum(IotDataBridgeDirectionEnum.class) + private Integer direction; + + @Schema(description = "桥梁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "桥梁类型不能为空") + @InEnum(IotDataBridgeTypeEnum.class) + private Integer type; + + @Schema(description = "桥梁配置") + @NotNull(message = "桥梁配置不能为空") + private IotDataBridgeAbstractConfig config; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java new file mode 100644 index 000000000..527e79b35 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeAbstractConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +/** + * IoT IotDataBridgeConfig 抽象类 + * + * 用于表示数据桥梁配置数据的通用类型,根据具体的 "type" 字段动态映射到对应的子类 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * + * @author HUIHUI + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = IotDataBridgeHttpConfig.class, name = "1"), + @JsonSubTypes.Type(value = IotDataBridgeMqttConfig.class, name = "10"), + @JsonSubTypes.Type(value = IotDataBridgeRedisStreamMQConfig.class, name = "21"), + @JsonSubTypes.Type(value = IotDataBridgeRocketMQConfig.class, name = "30"), + @JsonSubTypes.Type(value = IotDataBridgeRabbitMQConfig.class, name = "31"), + @JsonSubTypes.Type(value = IotDataBridgeKafkaMQConfig.class, name = "32"), +}) +public abstract class IotDataBridgeAbstractConfig { + + /** + * 配置类型 + * + * 枚举 {@link IotDataBridgeTypeEnum#getType()} + */ + private String type; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java new file mode 100644 index 000000000..eca35c76e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeHttpConfig.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +import java.util.Map; + +/** + * IoT HTTP 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeHttpConfig extends IotDataBridgeAbstractConfig { + + /** + * 请求 URL + */ + private String url; + /** + * 请求方法 + */ + private String method; + /** + * 请求头 + */ + private Map headers; + /** + * 请求参数 + */ + private Map query; + /** + * 请求体 + */ + private String body; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java new file mode 100644 index 000000000..1201214d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeKafkaMQConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT Kafka 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeKafkaMQConfig extends IotDataBridgeAbstractConfig { + + /** + * Kafka 服务器地址 + */ + private String bootstrapServers; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 是否启用 SSL + */ + private Boolean ssl; + + /** + * 主题 + */ + private String topic; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java new file mode 100644 index 000000000..448b21501 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeMqttConfig.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT MQTT 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeMqttConfig extends IotDataBridgeAbstractConfig { + + /** + * MQTT 服务器地址 + */ + private String url; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * 客户端编号 + */ + private String clientId; + /** + * 主题 + */ + private String topic; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java new file mode 100644 index 000000000..2c247d1d5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRabbitMQConfig.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT RabbitMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRabbitMQConfig extends IotDataBridgeAbstractConfig { + + /** + * RabbitMQ 服务器地址 + */ + private String host; + /** + * 端口 + */ + private Integer port; + /** + * 虚拟主机 + */ + private String virtualHost; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + + /** + * 交换机名称 + */ + private String exchange; + /** + * 路由键 + */ + private String routingKey; + /** + * 队列名称 + */ + private String queue; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java new file mode 100644 index 000000000..3c9bb330f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRedisStreamMQConfig.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +// TODO @puhui999:MQ 可以去掉哈。stream 更精准 +/** + * IoT Redis Stream 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRedisStreamMQConfig extends IotDataBridgeAbstractConfig { + + /** + * Redis 服务器地址 + */ + private String host; + /** + * 端口 + */ + private Integer port; + /** + * 密码 + */ + private String password; + /** + * 数据库索引 + */ + private Integer database; + + /** + * 主题 + */ + private String topic; +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java new file mode 100644 index 000000000..e23e3061a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/databridge/config/IotDataBridgeRocketMQConfig.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config; + +import lombok.Data; + +/** + * IoT RocketMQ 配置 {@link IotDataBridgeAbstractConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataBridgeRocketMQConfig extends IotDataBridgeAbstractConfig { + + /** + * RocketMQ 名称服务器地址 + */ + private String nameServer; + /** + * 访问密钥 + */ + private String accessKey; + /** + * 秘密钥匙 + */ + private String secretKey; + + /** + * 生产者组 + */ + private String group; + /** + * 主题 + */ + private String topic; + /** + * 标签 + */ + private String tags; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java new file mode 100644 index 000000000..f397e0acd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/rule/vo/package-info.java @@ -0,0 +1,2 @@ +// TODO @芋艿:占位 +package cn.iocoder.yudao.module.iot.controller.admin.rule.vo; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java new file mode 100644 index 000000000..a9c195656 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/IotStatisticsController.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsSummaryRespVO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import cn.iocoder.yudao.module.iot.service.product.IotProductCategoryService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.*; + +@Tag(name = "管理后台 - IoT 数据统计") +@RestController +@RequestMapping("/iot/statistics") +@Validated +public class IotStatisticsController { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotProductCategoryService productCategoryService; + @Resource + private IotProductService productService; + @Resource + private IotDeviceLogService deviceLogService; + + @GetMapping("/get-summary") + @Operation(summary = "获取 IoT 数据统计") + public CommonResult getIotStatisticsSummary(){ + IotStatisticsSummaryRespVO respVO = new IotStatisticsSummaryRespVO(); + // 1.1 获取总数 + respVO.setProductCategoryCount(productCategoryService.getProductCategoryCount(null)); + respVO.setProductCount(productService.getProductCount(null)); + respVO.setDeviceCount(deviceService.getDeviceCount(null)); + respVO.setDeviceMessageCount(deviceLogService.getDeviceLogCount(null)); + // 1.2 获取今日新增数量 + // TODO @super:使用 LocalDateTimeUtils.getToday() + LocalDateTime todayStart = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0); + respVO.setProductCategoryTodayCount(productCategoryService.getProductCategoryCount(todayStart)); + respVO.setProductTodayCount(productService.getProductCount(todayStart)); + respVO.setDeviceTodayCount(deviceService.getDeviceCount(todayStart)); + respVO.setDeviceMessageTodayCount(deviceLogService.getDeviceLogCount(todayStart)); + + // 2. 获取各个品类下设备数量统计 + respVO.setProductCategoryDeviceCounts(productCategoryService.getProductCategoryDeviceCountMap()); + + // 3. 获取设备状态数量统计 + Map deviceCountMap = deviceService.getDeviceCountMapByState(); + respVO.setDeviceOnlineCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.ONLINE.getState(), 0L)); + respVO.setDeviceOfflineCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.OFFLINE.getState(), 0L)); + respVO.setDeviceInactiveCount(deviceCountMap.getOrDefault(IotDeviceStateEnum.INACTIVE.getState(), 0L)); + return success(respVO); + } + + // TODO @super:要不干掉 IotStatisticsReqVO 参数,直接使用 @RequestParam 接收,简单一些。 + @GetMapping("/get-log-summary") + @Operation(summary = "获取 IoT 设备上下行消息数据统计") + public CommonResult getIotStatisticsDeviceMessageSummary( + @Valid IotStatisticsReqVO reqVO) { + return success(new IotStatisticsDeviceMessageSummaryRespVO() + .setDownstreamCounts(deviceLogService.getDeviceLogUpCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())) + .setDownstreamCounts((deviceLogService.getDeviceLogDownCountByHour(null, reqVO.getStartTime(), reqVO.getEndTime())))); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java new file mode 100644 index 000000000..15d2abccc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsDeviceMessageSummaryRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - IoT 设备上下行消息数量统计 Response VO") +@Data +public class IotStatisticsDeviceMessageSummaryRespVO { + + @Schema(description = "每小时上行数据数量统计") + private List> upstreamCounts; + + @Schema(description = "每小时下行数据数量统计") + private List> downstreamCounts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java new file mode 100644 index 000000000..741f77f3a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 统计 Request VO") +@Data +public class IotStatisticsReqVO { + + // TODO @super:前端传递的时候,还是通过 startTime 和 endTime 传递。后端转成 Long + + @Schema(description = "查询起始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1658486600000") + @NotNull(message = "查询起始时间不能为空") + private Long startTime; + + @Schema(description = "查询结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1758486600000") + @NotNull(message = "查询结束时间不能为空") + private Long endTime; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java new file mode 100644 index 000000000..21745c4ab --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/statistics/vo/IotStatisticsSummaryRespVO.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.iot.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Map; + +/** + * 管理后台 - IoT 统计 Response VO + */ +@Schema(description = "管理后台 - IoT 统计 Response VO") +@Data +public class IotStatisticsSummaryRespVO { + + @Schema(description = "品类数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long productCategoryCount; + + @Schema(description = "产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long productCount; + + @Schema(description = "设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long deviceCount; + + @Schema(description = "上报数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Long deviceMessageCount; + + @Schema(description = "今日新增品类数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long productCategoryTodayCount; + + @Schema(description = "今日新增产品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long productTodayCount; + + @Schema(description = "今日新增设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long deviceTodayCount; + + @Schema(description = "今日新增上报数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Long deviceMessageTodayCount; + + @Schema(description = "在线数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + private Long deviceOnlineCount; + + @Schema(description = "离线数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15") + private Long deviceOfflineCount; + + @Schema(description = "待激活设备数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long deviceInactiveCount; + + @Schema(description = "按品类统计的设备数量") + private Map productCategoryDeviceCounts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http new file mode 100644 index 000000000..1e1f72103 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.http @@ -0,0 +1,181 @@ +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "Temperature", + "name": "温度", + "description": "当前温度值", + "type": 1, + "property": { + "identifier": "Temperature", + "name": "温度", + "accessMode": "r", + "required": true, + "dataType": "int", + "dataSpecs": { + "dataType": "int", + "max": "200", + "min": "0", + "step": "10", + "defaultValue": "30", + "unit": "%", + "unitName": "百分比" + } + } +} + +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "switch", + "name": "开关", + "description": "温度计开关", + "type": 1, + "property": { + "identifier": "switch", + "name": "开关", + "accessMode": "rw", + "required": true, + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + } +} + +### 请求 /iot/product-thing-model/create 接口 => 成功 +POST {{baseUrl}}/iot/product-thing-model/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "argb", + "name": "温度计 argb 颜色", + "description": "温度计 argb 颜色", + "type": 1, + "property": { + "identifier": "argb", + "name": "温度计 argb 颜色", + "accessMode": "rw", + "required": true, + "dataType": "array", + "dataSpecs": { + "dataType": "array", + "size": 10, + "childDataType": "struct", + "dataSpecsList": [ + { + "identifier": "switch", + "name": "开关", + "accessMode": "rw", + "required": true, + "dataType": "struct", + "childDataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + }, + { + "identifier": "Temperature", + "name": "温度", + "accessMode": "r", + "required": true, + "dataType": "struct", + "childDataType": "int", + "dataSpecs": { + "dataType": "int", + "max": "200", + "min": "0", + "step": "10", + "defaultValue": "30", + "unit": "%", + "unitName": "百分比" + } + } + ] + } + } +} + +### 请求 /iot/product-thing-model/update 接口 => 成功 +PUT {{baseUrl}}/iot/product-thing-model/update +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 33, + "productId": 12, + "productKey": "CJVS54fObwZJ9Qe5CJVS54fObwZJ9Qe5", + "identifier": "switch", + "name": "开关", + "description": "温度计开关", + "type": 1, + "property": { + "identifier": "switch", + "name": "开关", + "accessMode": "r", + "required": true, + "dataType": "bool", + "dataSpecsList": [ + { + "dataType": "bool", + "name": "关", + "value": 0 + }, + { + "dataType": "bool", + "name": "开", + "value": 1 + } + ] + } +} + +### 请求 /iot/product-thing-model/delete 接口 => 成功 +DELETE {{baseUrl}}/iot/product-thing-model/delete?id=36 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +### 请求 /iot/product-thing-model/get 接口 => 成功 +GET {{baseUrl}}/iot/product-thing-model/get?id=67 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + + +### 请求 /iot/product-thing-model/list-by-product-id 接口 => 成功 +GET {{baseUrl}}/iot/product-thing-model/list-by-product-id?productId=1001 +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java new file mode 100644 index 000000000..382940fc4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - IoT 产品物模型") +@RestController +@RequestMapping("/iot/thing-model") +@Validated +public class IotThingModelController { + + @Resource + private IotThingModelService thingModelService; + + @PostMapping("/create") + @Operation(summary = "创建产品物模型") + @PreAuthorize("@ss.hasPermission('iot:thing-model:create')") + public CommonResult createThingModel(@Valid @RequestBody IotThingModelSaveReqVO createReqVO) { + return success(thingModelService.createThingModel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新产品物模型") + @PreAuthorize("@ss.hasPermission('iot:thing-model:update')") + public CommonResult updateThingModel(@Valid @RequestBody IotThingModelSaveReqVO updateReqVO) { + thingModelService.updateThingModel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除产品物模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:thing-model:delete')") + public CommonResult deleteThingModel(@RequestParam("id") Long id) { + thingModelService.deleteThingModel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得产品物模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult getThingModel(@RequestParam("id") Long id) { + IotThingModelDO thingModel = thingModelService.getThingModel(id); + return success(BeanUtils.toBean(thingModel, IotThingModelRespVO.class)); + } + + @GetMapping("/list-by-product-id") + @Operation(summary = "获得产品物模型") + @Parameter(name = "productId", description = "产品ID", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelListByProductId(@RequestParam("productId") Long productId) { + List list = thingModelService.getThingModelListByProductId(productId); + return success(BeanUtils.toBean(list, IotThingModelRespVO.class)); + } + + // TODO @puhui @super:getThingModelListByProductId 和 getThingModelListByProductId 可以融合么? + @GetMapping("/list") + @Operation(summary = "获得产品物模型列表") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelListByProductId(@Valid IotThingModelListReqVO reqVO) { + List list = thingModelService.getThingModelList(reqVO); + return success(BeanUtils.toBean(list, IotThingModelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得产品物模型分页") + @PreAuthorize("@ss.hasPermission('iot:thing-model:query')") + public CommonResult> getThingModelPage(@Valid IotThingModelPageReqVO pageReqVO) { + PageResult pageResult = thingModelService.getProductThingModelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, IotThingModelRespVO.class)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java new file mode 100644 index 000000000..06cc43809 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelEvent.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceEventTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的事件 + * + * @author HUIHUI + */ +@Data +public class ThingModelEvent { + + /** + * 事件标识符 + */ + @NotEmpty(message = "事件标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "事件标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 事件名称 + */ + @NotEmpty(message = "事件名称不能为空") + private String name; + /** + * 是否是标准品类的必选事件 + */ + private Boolean required; + /** + * 事件类型 + * + * 枚举 {@link IotThingModelServiceEventTypeEnum} + */ + @NotEmpty(message = "事件类型不能为空") + @InEnum(IotThingModelServiceEventTypeEnum.class) + private String type; + /** + * 事件的输出参数 + * + * 输出参数定义事件调用后返回的结果或反馈信息,用于确认操作结果或提供额外的信息。 + */ + @Valid + private List outputParams; + /** + * 标识设备需要执行的具体操作 + */ + private String method; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java new file mode 100644 index 000000000..2afad898b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelParam.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelParamDirectionEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 产品物模型中的参数 + * + * @author HUIHUI + */ +@Data +public class ThingModelParam { + + /** + * 参数标识符 + */ + @NotEmpty(message = "参数标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "参数标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 参数名称 + */ + @NotEmpty(message = "参数名称不能为空") + private String name; + /** + * 用于区分输入或输出参数 + * + * 枚举 {@link IotThingModelParamDirectionEnum} + */ + @NotEmpty(message = "参数方向不能为空") + @InEnum(IotThingModelParamDirectionEnum.class) + private String direction; + /** + * 参数的序号。从 0 开始排序,且不能重复。 + * + * TODO 考虑要不要序号,感觉是要的, 先留一手看看 + */ + private Integer paraOrder; + /** + * 参数值的数据类型,与 dataSpecs 的 dataType 保持一致 + * + * 枚举 {@link IotDataSpecsDataTypeEnum} + */ + @NotEmpty(message = "数据类型不能为空") + @InEnum(IotDataSpecsDataTypeEnum.class) + private String dataType; + /** + * 参数值的数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 参数值的数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java new file mode 100644 index 000000000..4b9a05a0e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelProperty.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的属性 + * + * dataSpecs 和 dataSpecsList 之中必须传入且只能传入一个 + * + * @author HUIHUI + */ +@Data +public class ThingModelProperty { + + /** + * 属性标识符 + */ + @NotEmpty(message = "属性标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "属性标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 属性名称 + */ + @NotEmpty(message = "属性名称不能为空") + private String name; + /** + * 云端可以对该属性进行的操作类型 + * + * 枚举 {@link IotThingModelAccessModeEnum} + */ + @NotEmpty(message = "操作类型不能为空") + @InEnum(IotThingModelAccessModeEnum.class) + private String accessMode; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * 参数值的数据类型,与 dataSpecs 的 dataType 保持一致 + * + * 枚举 {@link IotDataSpecsDataTypeEnum} + */ + @NotEmpty(message = "数据类型不能为空") + @InEnum(IotDataSpecsDataTypeEnum.class) + private String dataType; + /** + * 数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java new file mode 100644 index 000000000..c98acd824 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/ThingModelService.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelServiceCallTypeEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +import java.util.List; + +/** + * IoT 物模型中的服务 + * + * @author HUIHUI + */ +@Data +public class ThingModelService { + + /** + * 服务标识符 + */ + @NotEmpty(message = "服务标识符不能为空") + @Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_]{0,31}$", message = "服务标识符只能由字母、数字和下划线组成,必须以字母开头,长度不超过 32 个字符") + private String identifier; + /** + * 服务名称 + */ + @NotEmpty(message = "服务名称不能为空") + private String name; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * 调用类型 + * + * 枚举 {@link IotThingModelServiceCallTypeEnum} + */ + @NotEmpty(message = "调用类型不能为空") + @InEnum(IotThingModelServiceCallTypeEnum.class) + private String callType; + /** + * 服务的输入参数 + * + * 输入参数定义服务调用时所需提供的信息,用于控制设备行为或执行特定任务 + */ + @Valid + private List inputParams; + /** + * 服务的输出参数 + * + * 输出参数定义服务调用后返回的结果或反馈信息,用于确认操作结果或提供额外的信息。 + */ + @Valid + private List outputParams; + /** + * 标识设备需要执行的具体操作 + */ + private String method; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java new file mode 100644 index 000000000..50011aabf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelArrayDataSpecs.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * IoT 物模型数据类型为数组的 DataSpec 定义 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelArrayDataSpecs extends ThingModelDataSpecs { + + /** + * 数组中的元素个数 + */ + private Integer size; + /** + * 数组中的元素的数据类型。可选值:struct、int、float、double 或 text + */ + private String childDataType; + /** + * 数据类型(childDataType)为列表型 struct 的数据规范存储在 dataSpecsList 中 + * 此时 struct 取值范围为:int、float、double、text、date、enum、bool + */ + private List dataSpecsList; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java new file mode 100644 index 000000000..925bc6719 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelBoolOrEnumDataSpecs.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为布尔型或枚举型的 DataSpec 定义 + * + * 数据类型,取值为 bool 或 enum。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelBoolOrEnumDataSpecs extends ThingModelDataSpecs { + + // TODO @puhui999:要不写下参数校验?这样,注释可以简洁一点 + /** + * 枚举项的名称。 + * 可包含中文、大小写英文字母、数字、下划线(_)和短划线(-) + * 必须以中文、英文字母或数字开头,长度不超过 20 个字符 + */ + private String name; + /** + * 枚举值。 + */ + private Integer value; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java new file mode 100644 index 000000000..d9fc12dd9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDataSpecs.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +/** + * IoT ThingModelDataSpecs 抽象类 + * + * 用于表示物模型数据的通用类型,根据具体的 "dataType" 字段动态映射到对应的子类。 + * 提供多态支持,适用于不同类型的数据结构序列化和反序列化场景。 + * + * @author HUIHUI + */ +@Data +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "dataType", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "int"), + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "float"), + @JsonSubTypes.Type(value = ThingModelNumericDataSpec.class, name = "double"), + @JsonSubTypes.Type(value = ThingModelDateOrTextDataSpecs.class, name = "text"), + @JsonSubTypes.Type(value = ThingModelDateOrTextDataSpecs.class, name = "date"), + @JsonSubTypes.Type(value = ThingModelBoolOrEnumDataSpecs.class, name = "bool"), + @JsonSubTypes.Type(value = ThingModelBoolOrEnumDataSpecs.class, name = "enum"), + @JsonSubTypes.Type(value = ThingModelArrayDataSpecs.class, name = "array"), + @JsonSubTypes.Type(value = ThingModelStructDataSpecs.class, name = "struct") +}) +public abstract class ThingModelDataSpecs { + + /** + * 数据类型 + */ + private String dataType; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java new file mode 100644 index 000000000..62500bc56 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelDateOrTextDataSpecs.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为时间型或文本型的 DataSpec 定义 + * + * 数据类型,取值为 date 或 text。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelDateOrTextDataSpecs extends ThingModelDataSpecs { + + /** + * 数据长度,单位为字节。取值不能超过 2048。 + * 当 dataType 为 text 时,需传入该参数。 + */ + private Integer length; + /** + * 默认值,可选参数,用于存储默认值。 + */ + private String defaultValue; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java new file mode 100644 index 000000000..8d0827c01 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelNumericDataSpec.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * IoT 物模型数据类型为数值的 DataSpec 定义 + * + * 数据类型,取值为 int、float 或 double。 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelNumericDataSpec extends ThingModelDataSpecs { + + /** + * 最大值,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "200",而不是 200。 + */ + private String max; + /** + * 最小值,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "0",而不是 0。 + */ + private String min; + /** + * 步长,需转为字符串类型。值必须与 dataType 类型一致。 + * 例如,当 dataType 为 int 时,取值为 "10",而不是 10。 + */ + private String step; + /** + * 精度。当 dataType 为 float 或 double 时可选传入。 + */ + private String precise; + /** + * 默认值,可传入用于存储的默认值。 + */ + private String defaultValue; + /** + * 单位的符号。 + */ + private String unit; + /** + * 单位的名称。 + */ + private String unitName; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java new file mode 100644 index 000000000..6d483eeaa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/model/dataType/ThingModelStructDataSpecs.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType; + +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelAccessModeEnum; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * IoT 物模型数据类型为 struct 的 DataSpec 定义 + * + * @author HUIHUI + */ +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties({"dataType"}) // 忽略子类中的 dataType 字段,从而避免重复 +public class ThingModelStructDataSpecs extends ThingModelDataSpecs { + + /** + * 属性标识符 + */ + private String identifier; + /** + * 属性名称 + */ + private String name; + /** + * 云端可以对该属性进行的操作类型 + * + * 枚举 {@link IotThingModelAccessModeEnum} + */ + private String accessMode; + /** + * 是否是标准品类的必选服务 + */ + private Boolean required; + /** + * struct 数据的数据类型 + */ + private String childDataType; + /** + * 数据类型(dataType)为非列表型(int、float、double、text、date、array)的数据规范存储在 dataSpecs 中 + */ + private ThingModelDataSpecs dataSpecs; + /** + * 数据类型(dataType)为列表型(enum、bool、struct)的数据规范存储在 dataSpecsList 中 + */ + private List dataSpecsList; + +} + diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java new file mode 100644 index 000000000..5b92256bb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品物模型 List Request VO") +@Data +public class IotThingModelListReqVO { + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "功能标识", example = "temperature") + private String identifier; + + @Schema(description = "功能名称", example = "温度") + private String name; + + @Schema(description = "功能类型", example = "1") + @InEnum(IotThingModelTypeEnum.class) + private Integer type; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java new file mode 100644 index 000000000..8064b10e5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelPageReqVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - IoT 产品物模型分页 Request VO") +@Data +public class IotThingModelPageReqVO extends PageParam { + + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "产品编号不能为空") + private Long productId; + + @Schema(description = "功能标识", example = "temperature") + private String identifier; + + @Schema(description = "功能名称", example = "温度") + private String name; + + @Schema(description = "功能类型", example = "1") + @InEnum(IotThingModelTypeEnum.class) + private Integer type; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java index 9ef3f58d8..15a5b9f95 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java @@ -1,42 +1,41 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -import java.util.List; @Schema(description = "管理后台 - IoT 产品物模型 Response VO") @Data @ExcelIgnoreUnannotated -public class IotThinkModelFunctionRespVO { +public class IotThingModelRespVO { - @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816") + @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21816") @ExcelProperty("产品ID") private Long id; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long productId; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_sensor") @ExcelProperty("产品标识") private String productKey; - @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature") private String identifier; - @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度") private String name; - @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "测量当前环境温度") private String description; - @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer type; @Schema(description = "属性", requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java similarity index 66% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java index 7d51ce504..1e8564df4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelSaveReqVO.java @@ -1,18 +1,19 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; +package cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo; import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @Schema(description = "管理后台 - IoT 产品物模型新增/修改 Request VO") @Data -public class IotThinkModelFunctionSaveReqVO { +public class IotThingModelSaveReqVO { @Schema(description = "编号", example = "1") private Long id; @@ -21,33 +22,36 @@ public class IotThinkModelFunctionSaveReqVO { @NotNull(message = "产品ID不能为空") private Long productId; - @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature_001") @NotEmpty(message = "产品标识不能为空") private String productKey; - @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "temp_monitor") @NotEmpty(message = "功能标识不能为空") private String identifier; - @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "温度监测器") @NotEmpty(message = "功能名称不能为空") private String name; - @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "用于监测环境温度的传感器") private String description; - @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "功能类型不能为空") - @InEnum(IotProductFunctionTypeEnum.class) + @InEnum(IotThingModelTypeEnum.class) private Integer type; @Schema(description = "属性", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelProperty property; @Schema(description = "服务", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelService service; @Schema(description = "事件", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid private ThingModelEvent event; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http index 56464dd80..84446b0ce 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.http @@ -1,7 +1,7 @@ ### 请求 /iot/think-model-function/create 接口 => 成功 POST {{baseUrl}}/iot/think-model-function/create Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} { @@ -32,7 +32,7 @@ Authorization: Bearer {{token}} ### 请求 /iot/think-model-function/create 接口 => 成功 POST {{baseUrl}}/iot/think-model-function/create Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} { @@ -66,7 +66,7 @@ Authorization: Bearer {{token}} ### 请求 /iot/think-model-function/update 接口 => 成功 PUT {{baseUrl}}/iot/think-model-function/update Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} { @@ -97,16 +97,16 @@ Authorization: Bearer {{token}} ### 请求 /iot/think-model-function/delete 接口 => 成功 DELETE {{baseUrl}}/iot/think-model-function/delete?id=7 -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} ### 请求 /iot/think-model-function/get 接口 => 成功 GET {{baseUrl}}/iot/think-model-function/get?id=10 -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} ### 请求 /iot/think-model-function/list-by-product-id 接口 => 成功 GET {{baseUrl}}/iot/think-model-function/list-by-product-id?productId=1001 -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java deleted file mode 100644 index 4f48f3628..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/IotThinkModelFunctionController.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.convert.thinkmodelfunction.IotThinkModelFunctionConvert; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.service.thinkmodelfunction.IotThinkModelFunctionService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - IoT 产品物模型") -@RestController -@RequestMapping("/iot/think-model-function") -@Validated -public class IotThinkModelFunctionController { - - @Resource - private IotThinkModelFunctionService thinkModelFunctionService; - - @PostMapping("/create") - @Operation(summary = "创建产品物模型") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:create')") - public CommonResult createThinkModelFunction(@Valid @RequestBody IotThinkModelFunctionSaveReqVO createReqVO) { - return success(thinkModelFunctionService.createThinkModelFunction(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新产品物模型") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:update')") - public CommonResult updateThinkModelFunction(@Valid @RequestBody IotThinkModelFunctionSaveReqVO updateReqVO) { - thinkModelFunctionService.updateThinkModelFunction(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除产品物模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:think-model-function:delete')") - public CommonResult deleteThinkModelFunction(@RequestParam("id") Long id) { - thinkModelFunctionService.deleteThinkModelFunction(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得产品物模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult getThinkModelFunction(@RequestParam("id") Long id) { - IotThinkModelFunctionDO function = thinkModelFunctionService.getThinkModelFunction(id); - return success(IotThinkModelFunctionConvert.INSTANCE.convert(function)); - } - - @GetMapping("/list-by-product-id") - @Operation(summary = "获得产品物模型") - @Parameter(name = "productId", description = "产品ID", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult> getThinkModelFunctionListByProductId(@RequestParam("productId") Long productId) { - List list = thinkModelFunctionService.getThinkModelFunctionListByProductId(productId); - return success(IotThinkModelFunctionConvert.INSTANCE.convertList(list)); - } - - @GetMapping("/page") - @Operation(summary = "获得产品物模型分页") - @PreAuthorize("@ss.hasPermission('iot:think-model-function:query')") - public CommonResult> getThinkModelFunctionPage(@Valid IotThinkModelFunctionPageReqVO pageReqVO) { - PageResult pageResult = thinkModelFunctionService.getThinkModelFunctionPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, IotThinkModelFunctionRespVO.class)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java deleted file mode 100644 index d7fa68758..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelEvent.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import lombok.Data; -import java.util.List; - -@Data -public class ThingModelEvent { - - /** - * 事件标识符 - */ - private String identifier; - /** - * 事件名称 - */ - private String name; - /** - * 事件描述 - */ - private String description; - - /** - * 事件类型 - * - * "info"、"alert"、"error" - */ - private String type; - private List outputData; - private String method; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java deleted file mode 100644 index 025d37e76..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelProperty.java +++ /dev/null @@ -1,27 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelDataType; -import lombok.Data; - -@Data -public class ThingModelProperty { - - /** - * 属性标识符 - */ - private String identifier; - /** - * 属性名称 - */ - private String name; - /** - * 属性描述 - */ - private String description; - - private String accessMode; // "rw"、"r"、"w" - private Boolean required; - // TODO @haohao:这个是不是 dataSpecs 和 dataSpecsList?https://help.aliyun.com/zh/iot/developer-reference/api-a99t11 - private ThingModelDataType dataType; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java deleted file mode 100644 index d97e05e9c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/ThingModelService.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import lombok.Data; -import java.util.List; - -@Data -public class ThingModelService { - - /** - * 服务标识符 - */ - private String identifier; - /** - * 服务名称 - */ - private String name; - /** - * 服务描述 - */ - private String description; - - /** - * 调用类型 - * - * "sync"、"async" - */ - private String callType; - private List inputData; - private List outputData; - private String method; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java deleted file mode 100644 index 2be24004e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArgument.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelArgument { - - private String identifier; - private String name; - private ThingModelDataType dataType; - /** - * 用于区分输入或输出参数,"input" 或 "output" - */ - private String direction; - private String description; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java deleted file mode 100644 index c3faf6161..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArraySpecs.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelArraySpecs { - - /** - * 数组长度 - */ - private int size; - /** - * 数组元素的类型 - */ - private ThingModelDataType item; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java deleted file mode 100644 index bab87be0a..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelArrayType.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -// TODO @haohao:这个是不是和别的类,不太统一哈 -@Data -public class ThingModelArrayType extends ThingModelDataType { - - private ThingModelArraySpecs specs; - -} - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java deleted file mode 100644 index b8ca64195..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelBoolType.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -public class ThingModelBoolType extends ThingModelDataType { - - // Bool 类型一般不需要额外的 specs - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java deleted file mode 100644 index ec5f04bbb..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDataType.java +++ /dev/null @@ -1,24 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import lombok.Data; - -@Data -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) -@JsonSubTypes({ - @JsonSubTypes.Type(value = ThingModelIntType.class, name = "int"), - @JsonSubTypes.Type(value = ThingModelFloatType.class, name = "float"), - @JsonSubTypes.Type(value = ThingModelDoubleType.class, name = "double"), - @JsonSubTypes.Type(value = ThingModelTextType.class, name = "text"), - @JsonSubTypes.Type(value = ThingModelDateType.class, name = "date"), - @JsonSubTypes.Type(value = ThingModelBoolType.class, name = "bool"), - @JsonSubTypes.Type(value = ThingModelEnumType.class, name = "enum"), - @JsonSubTypes.Type(value = ThingModelStructType.class, name = "struct"), - @JsonSubTypes.Type(value = ThingModelArrayType.class, name = "array") -}) -public abstract class ThingModelDataType { - - private String type; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java deleted file mode 100644 index 854229339..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDateType.java +++ /dev/null @@ -1,10 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelDateType extends ThingModelDataType { - - // Date 类型一般不需要额外的 specs - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java deleted file mode 100644 index e5f3ad268..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelDoubleType.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelDoubleType extends ThingModelDataType { - private ThingModelDoubleSpecs specs; -} - -@Data -class ThingModelDoubleSpecs { - - private Double min; - private Double max; - private Double step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java deleted file mode 100644 index 3dcb068e9..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelEnumType.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -import java.util.Map; - -@Data -public class ThingModelEnumType extends ThingModelDataType { - - /** - * 枚举值和描述的键值对 - */ - private Map specs; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java deleted file mode 100644 index 27926fa49..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelFloatType.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -@Data -@EqualsAndHashCode(callSuper = true) -public class ThingModelFloatType extends ThingModelDataType { - private ThingModelFloatSpecs specs; -} - -@Data -class ThingModelFloatSpecs { - - private Float min; - private Float max; - private Float step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java deleted file mode 100644 index a126eb749..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelIntType.java +++ /dev/null @@ -1,18 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelIntType extends ThingModelDataType { - private ThingModelIntSpecs specs; -} - -@Data -class ThingModelIntSpecs { - - private Integer min; - private Integer max; - private Integer step; - private String unit; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java deleted file mode 100644 index 5e079f22b..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructField.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelStructField { - - private String identifier; - private String name; - private ThingModelDataType dataType; - private String description; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java deleted file mode 100644 index f0996513c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelStructType.java +++ /dev/null @@ -1,14 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -import java.util.List; - -@Data -public class ThingModelStructType extends ThingModelDataType { - - private List specs; - -} - - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java deleted file mode 100644 index 16d1e402e..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/thingModel/dataType/ThingModelTextType.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType; - -import lombok.Data; - -@Data -public class ThingModelTextType extends ThingModelDataType { - - private ThingModelTextSpecs specs; - -} - -@Data -class ThingModelTextSpecs { - - /** - * 最大长度 - */ - private Integer length; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java deleted file mode 100644 index 8a590d429..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thinkmodelfunction/vo/IotThinkModelFunctionPageReqVO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import cn.iocoder.yudao.framework.common.validation.InEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -@Schema(description = "管理后台 - IoT 产品物模型分页 Request VO") -@Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) -public class IotThinkModelFunctionPageReqVO extends PageParam { - - @Schema(description = "功能标识") - private String identifier; - - @Schema(description = "功能名称", example = "张三") - private String name; - - @Schema(description = "功能类型", example = "1") - @InEnum(IotProductFunctionTypeEnum.class) - private Integer type; - - @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED) - @NotNull(message = "产品ID不能为空") - private Long productId; - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java index c196c25c3..18d7ad21d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/package-info.java @@ -1 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ package cn.iocoder.yudao.module.iot.convert; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java new file mode 100644 index 000000000..9577b18f7 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thingmodel/IotThingModelConvert.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.convert.thingmodel; + +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.Objects; + +@Mapper +public interface IotThingModelConvert { + + IotThingModelConvert INSTANCE = Mappers.getMapper(IotThingModelConvert.class); + + @Mapping(target = "property", expression = "java(convertToProperty(bean))") + @Mapping(target = "event", expression = "java(convertToEvent(bean))") + @Mapping(target = "service", expression = "java(convertToService(bean))") + IotThingModelDO convert(IotThingModelSaveReqVO bean); + + @Named("convertToProperty") + default ThingModelProperty convertToProperty(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + return bean.getProperty(); + } + return null; + } + + @Named("convertToEvent") + default ThingModelEvent convertToEvent(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.EVENT.getType())) { + return bean.getEvent(); + } + return null; + } + + @Named("convertToService") + default ThingModelService convertToService(IotThingModelSaveReqVO bean) { + if (Objects.equals(bean.getType(), IotThingModelTypeEnum.SERVICE.getType())) { + return bean.getService(); + } + return null; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java deleted file mode 100644 index 764d4c030..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/convert/thinkmodelfunction/IotThinkModelFunctionConvert.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.iocoder.yudao.module.iot.convert.thinkmodelfunction; - -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionRespVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.factory.Mappers; - -import java.util.List; -import java.util.Objects; - -@Mapper -public interface IotThinkModelFunctionConvert { - - IotThinkModelFunctionConvert INSTANCE = Mappers.getMapper(IotThinkModelFunctionConvert.class); - - // 将 SaveReqVO 转换为 DO - @Mapping(target = "property", expression = "java(convertToProperty(bean))") - @Mapping(target = "event", expression = "java(convertToEvent(bean))") - @Mapping(target = "service", expression = "java(convertToService(bean))") - IotThinkModelFunctionDO convert(IotThinkModelFunctionSaveReqVO bean); - - default ThingModelProperty convertToProperty(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - return bean.getProperty(); - } - return null; - } - - default ThingModelEvent convertToEvent(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.EVENT.getType())) { - return bean.getEvent(); - } - return null; - } - - default ThingModelService convertToService(IotThinkModelFunctionSaveReqVO bean) { - if (Objects.equals(bean.getType(), IotProductFunctionTypeEnum.SERVICE.getType())) { - return bean.getService(); - } - return null; - } - - // 将 DO 转换为 RespVO - @Mapping(target = "property", source = "property") - @Mapping(target = "event", source = "event") - @Mapping(target = "service", source = "service") - IotThinkModelFunctionRespVO convert(IotThinkModelFunctionDO bean); - - // 批量转换 - List convertList(List list); -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index d3f6547a1..9633d2feb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -1,30 +1,32 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.device; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongSetTypeHandler; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.Set; /** * IoT 设备 DO * * @author haohao */ -@TableName("iot_device") +@TableName(value = "iot_device", autoResultMap = true) @KeySequence("iot_device_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class IotDeviceDO extends BaseDO { +public class IotDeviceDO extends TenantBaseDO { /** * 设备 ID,主键,自增 @@ -33,6 +35,8 @@ public class IotDeviceDO extends BaseDO { private Long id; /** * 设备唯一标识符,全局唯一,用于识别设备 + * + * 类似阿里云 QueryDeviceInfo 的 IotInstanceId */ private String deviceKey; /** @@ -47,6 +51,17 @@ public class IotDeviceDO extends BaseDO { * 设备序列号 */ private String serialNumber; + /** + * 设备图片 + */ + private String picUrl; + /** + * 设备分组编号集合 + * + * 关联 {@link IotDeviceGroupDO#getId()} + */ + @TableField(typeHandler = LongSetTypeHandler.class) + private Set groupIds; /** * 产品编号 @@ -66,13 +81,6 @@ public class IotDeviceDO extends BaseDO { * 冗余 {@link IotProductDO#getDeviceType()} */ private Integer deviceType; - - /** - * 设备状态 - *

- * 枚举 {@link IotDeviceStatusEnum} - */ - private Integer status; /** * 网关设备编号 *

@@ -83,17 +91,19 @@ public class IotDeviceDO extends BaseDO { private Long gatewayId; /** - * 设备状态最后更新时间 + * 设备状态 + *

+ * 枚举 {@link IotDeviceStateEnum} */ - private LocalDateTime statusLastUpdateTime; + private Integer state; /** * 最后上线时间 */ - private LocalDateTime lastOnlineTime; + private LocalDateTime onlineTime; /** * 最后离线时间 */ - private LocalDateTime lastOfflineTime; + private LocalDateTime offlineTime; /** * 设备激活时间 */ @@ -104,10 +114,13 @@ public class IotDeviceDO extends BaseDO { */ private String ip; /** - * 设备的固件版本 + * 固件编号 + * + * 关联 {@link IotOtaFirmwareDO#getId()} */ - private String firmwareVersion; + private String firmwareId; + // TODO @芋艿:【待定 003】:要不要增加 username?目前 tl 有,阿里云之类的没有 /** * 设备密钥,用于设备认证,需安全存储 */ @@ -130,6 +143,7 @@ public class IotDeviceDO extends BaseDO { // TODO @haohao:是不是要枚举哈 private String authType; + // TODO @芋艿:【待定 002】:1)设备维护的时候,设置位置?类似 tl?;2)设备上传的时候,设置位置,类似 it? /** * 设备位置的纬度 */ @@ -149,4 +163,11 @@ public class IotDeviceDO extends BaseDO { */ private String address; + /** + * 设备配置 + * + * JSON 格式,可下发给 device 进行自定义配置 + */ + private String config; + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java new file mode 100644 index 000000000..7865a4424 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceGroupDO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 设备分组 DO + * + * @author 芋道源码 + */ +@TableName("iot_device_group") +@KeySequence("iot_device_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceGroupDO extends BaseDO { + + /** + * 分组 ID + */ + @TableId + private Long id; + /** + * 分组名字 + */ + private String name; + /** + * 分组状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 分组描述 + */ + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java new file mode 100644 index 000000000..55cfb19d4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * IoT 设备日志数据 DO + * + * 目前使用 TDengine 存储 + * + * @author alwayssuper + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDeviceLogDO { + + /** + * 日志编号 + * + * 通过 {@link IdUtil#fastSimpleUUID()} 生成 + */ + private String id; + + /** + * 请求编号 + * + * 对应 {@link IotDeviceMessage#getRequestId()} 字段 + */ + private String requestId; + + /** + * 产品标识 + *

+ * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private String deviceName; + /** + * 设备标识 + *

+ * 关联 {@link IotDeviceDO#getDeviceKey()}} + */ + private String deviceKey; // 非存储字段,用于 TDengine 的 TAG + + /** + * 日志类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 数据内容 + * + * 存储具体的消息数据内容,通常是 JSON 格式 + */ + private String content; + /** + * 响应码 + * + * 目前只有 server 下行消息给 device 设备时,才会有响应码 + */ + private Integer code; + + /** + * 上报时间戳 + */ + private Long reportTime; + + /** + * 时序时间 + */ + private Long ts; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java new file mode 100644 index 000000000..afb328894 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDevicePropertyDO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.device; + +import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * IoT 设备属性项 Redis DO + * + * @see cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants#DEVICE_PROPERTY + * @see DevicePropertyRedisDAO + * + * @author haohao + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDevicePropertyDO { + + /** + * 属性值(最新) + */ + private Object value; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java new file mode 100644 index 000000000..fa56f6938 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaFirmwareDO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT OTA 固件 DO + * + * @see https://help.aliyun.com/zh/iot/user-guide/ota-upgrade-overview + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_firmware", autoResultMap = true) +@KeySequence("iot_ota_firmware_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaFirmwareDO extends BaseDO { + + /** + * 固件编号 + */ + @TableField + private Long id; + /** + * 固件名称 + */ + private String name; + /** + * 固件版本 + */ + private String description; + /** + * 版本号 + */ + private String version; + + /** + * 产品编号 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + // TODO @li:帮我改成 Long 哈,写错了 + private String productId; + /** + * 产品标识 + * + * 冗余 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getProductKey()} + */ + private String productKey; + + /** + * 签名方式 + * + * 例如说:MD5、SHA256 + */ + private String signMethod; + /** + * 固件文件签名 + */ + private String fileSign; + /** + * 固件文件大小 + */ + private Long fileSize; + /** + * 固件文件 URL + */ + private String fileUrl; + + /** + * 自定义信息,建议使用 JSON 格式 + */ + private String information; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java new file mode 100644 index 000000000..ff4f0e7a0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeRecordDO.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * IoT OTA 升级记录 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_upgrade_record", autoResultMap = true) +@KeySequence("iot_ota_upgrade_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaUpgradeRecordDO extends BaseDO { + + @TableId + private Long id; + + /** + * 固件编号 + * + * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + /** + * 任务编号 + * + * 关联 {@link IotOtaUpgradeTaskDO#getId()} + */ + private Long taskId; + + /** + * 产品标识 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO#getId()} + */ + private String productKey; + /** + * 设备名称 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + private String deviceName; + /** + * 设备编号 + * + * 关联 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO#getId()} + */ + private String deviceId; + /** + * 来源的固件编号 + * + * 关联 {@link IotDeviceDO#getFirmwareId()} + */ + private Long fromFirmwareId; + + /** + * 升级状态 + * + * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum} + */ + private Integer status; + /** + * 升级进度,百分比 + */ + private Integer progress; + /** + * 升级进度描述 + * + * 注意,只记录设备最后一次的升级进度描述 + * 如果想看历史记录,可以查看 {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO} 设备日志 + */ + private String description; + /** + * 升级开始时间 + */ + private LocalDateTime startTime; + /** + * 升级结束时间 + */ + private LocalDateTime endTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java new file mode 100644 index 000000000..221bdc56c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/ota/IotOtaUpgradeTaskDO.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.ota; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * IoT OTA 升级任务 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_ota_upgrade_task", autoResultMap = true) +@KeySequence("iot_ota_upgrade_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotOtaUpgradeTaskDO extends BaseDO { + + /** + * 任务编号 + */ + @TableField + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务描述 + */ + private String description; + + /** + * 固件编号 + *

+ * 关联 {@link IotOtaFirmwareDO#getId()} + */ + private Long firmwareId; + + /** + * 任务状态 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum} + */ + private Integer status; + + /** + * 升级范围 + *

+ * 关联 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum} + */ + private Integer scope; + /** + * 设备数量 + */ + private Long deviceCount; + /** + * 选中的设备编号数组 + *

+ * 关联 {@link IotDeviceDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List deviceIds; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java new file mode 100644 index 000000000..cb247fc30 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginConfigDO.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 插件配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_plugin_config") +@KeySequence("iot_plugin_config_seq") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotPluginConfigDO extends TenantBaseDO { + + /** + * 主键 ID + */ + @TableId + private Long id; + /** + * 插件包标识符 + */ + private String pluginKey; + /** + * 插件名称 + */ + private String name; + /** + * 插件描述 + */ + private String description; + /** + * 部署方式 + *

+ * 枚举 {@link IotPluginDeployTypeEnum} + */ + private Integer deployType; + // TODO @芋艿:如果是外置的插件,fileName 和 version 的选择~ + /** + * 插件包文件名 + */ + private String fileName; + /** + * 插件版本 + */ + private String version; + // TODO @芋艿:type 字典的定义 + /** + * 插件类型 + *

+ * 枚举 {@link IotPluginTypeEnum} + */ + private Integer type; + /** + * 设备插件协议类型 + */ + // TODO @芋艿:枚举字段 + private String protocol; + // TODO @haohao:这个字段,是不是直接用 CommonStatus,开启、禁用;然后插件实例那,online 是否在线 + /** + * 状态 + *

+ * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // TODO @芋艿:configSchema、config 示例字段 + /** + * 插件配置项描述信息 + */ + private String configSchema; + /** + * 插件配置信息 + */ + private String config; + + // TODO @芋艿:script 后续的使用 + /** + * 插件脚本 + */ + private String script; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java new file mode 100644 index 000000000..34abe893e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.plugin; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * IoT 插件实例 DO + * + * @author 芋道源码 + */ +@TableName("iot_plugin_instance") +@KeySequence("iot_plugin_instance_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotPluginInstanceDO extends TenantBaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 插件编号 + *

+ * 关联 {@link IotPluginConfigDO#getId()} + */ + private Long pluginId; + /** + * 插件进程编号 + * + * 一般格式是:hostIp@processId@${uuid} + */ + private String processId; + + /** + * 插件实例所在 IP + */ + private String hostIp; + /** + * 设备下行端口 + */ + private Integer downstreamPort; + + /** + * 是否在线 + */ + private Boolean online; + /** + * 在线时间 + */ + private LocalDateTime onlineTime; + /** + * 离线时间 + */ + private LocalDateTime offlineTime; + /** + * 心跳时间 + * + * 目的:心路时间超过一定时间后,会被进行下线处理 + */ + private LocalDateTime heartbeatTime; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java new file mode 100644 index 000000000..174342afb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductCategoryDO.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.product; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * IoT 产品分类 DO + * + * @author 芋道源码 + */ +@TableName("iot_product_category") +@KeySequence("iot_product_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotProductCategoryDO extends BaseDO { + + /** + * 分类 ID + */ + @TableId + private Long id; + /** + * 分类名字 + */ + private String name; + /** + * 分类排序 + */ + private Integer sort; + /** + * 分类状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 分类描述 + */ + private String description; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java index eef466eda..3caebbccb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.dal.dataobject.product; -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -14,15 +14,13 @@ import lombok.*; @TableName("iot_product") @KeySequence("iot_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class IotProductDO extends BaseDO { +public class IotProductDO extends TenantBaseDO { /** - * 产品ID + * 产品 ID */ @TableId private Long id; @@ -30,17 +28,24 @@ public class IotProductDO extends BaseDO { * 产品名称 */ private String name; - // TODO @haohao:这个字段,要不改成 identifier,和阿里云更统一些 /** * 产品标识 */ private String productKey; /** - * 产品所属品类编号 + * 产品分类编号 *

- * TODO 外键:后续加 + * 关联 {@link IotProductCategoryDO#getId()} */ private Long categoryId; + /** + * 产品图标 + */ + private String icon; + /** + * 产品图片 + */ + private String picUrl; /** * 产品描述 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java new file mode 100644 index 000000000..c6a2390ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertConfig.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotAlertConfigReceiveTypeEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * IoT 告警配置 DO + * + * @author 芋道源码 + */ +@TableName("iot_alert_config") +@KeySequence("iot_alert_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotAlertConfig extends BaseDO { + + /** + * 配置编号 + */ + @TableId + private Long id; + /** + * 配置名称 + */ + private String name; + /** + * 配置描述 + */ + private String description; + /** + * 配置状态 + * + * TODO 数据字典 + */ + private Integer level; + /** + * 配置状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + + /** + * 关联的规则场景编号数组 + * + * 关联 {@link IotRuleSceneDO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List ruleSceneIds; + + /** + * 接收的用户编号数组 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List receiveUserIds; + /** + * 接收的类型数组 + * + * 枚举 {@link IotAlertConfigReceiveTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List receiveTypes; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java new file mode 100644 index 000000000..840111078 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotAlertRecordDO.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * IoT 告警记录 DO + * + * @author 芋道源码 + */ +@TableName("iot_alert_record") +@KeySequence("iot_alert_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotAlertRecordDO extends BaseDO { + + /** + * 记录编号 + */ + @TableField + private Long id; + /** + * 告警名称 + * + * 冗余 {@link IotAlertConfig#getName()} + */ + private Long configId; + /** + * 告警名称 + * + * 冗余 {@link IotAlertConfig#getName()} + */ + private String name; + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} ()} + */ + private String productKey; + /** + * 设备名称 + * + * 冗余 {@link IotDeviceDO#getDeviceName()} + */ + private String deviceName; + + // TODO @芋艿:有没更好的方式 + /** + * 触发的设备消息 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private IotDeviceMessage deviceMessage; + + // TODO @芋艿:换成枚举,枚举对应 ApiErrorLogProcessStatusEnum + /** + * 处理状态 + * + * true - 已处理 + * false - 未处理 + */ + private Boolean processStatus; + /** + * 处理结果(备注) + */ + private String processRemark; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java new file mode 100644 index 000000000..fed429872 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotDataBridgeDO.java @@ -0,0 +1,67 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeAbstractConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeDirectionEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * IoT 数据桥梁 DO + * + * @author 芋道源码 + */ +@TableName(value = "iot_data_bridge", autoResultMap = true) +@KeySequence("iot_data_bridge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotDataBridgeDO extends BaseDO { + + /** + * 桥梁编号 + */ + @TableId + private Long id; + /** + * 桥梁名称 + */ + private String name; + /** + * 桥梁描述 + */ + private String description; + /** + * 桥梁状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 桥梁方向 + * + * 枚举 {@link IotDataBridgeDirectionEnum} + */ + private Integer direction; + + /** + * 桥梁类型 + * + * 枚举 {@link IotDataBridgeTypeEnum} + */ + private Integer type; + + /** + * 桥梁配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private IotDataBridgeAbstractConfig config; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java new file mode 100644 index 000000000..f50101a4e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java @@ -0,0 +1,243 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule; + +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; +import java.util.Map; + +/** + * IoT 规则场景(场景联动) DO + * + * @author 芋道源码 + */ +@TableName("iot_rule_scene") +@KeySequence("iot_rule_scene_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IotRuleSceneDO extends TenantBaseDO { + + /** + * 场景编号 + */ + @TableId + private Long id; + /** + * 场景名称 + */ + private String name; + /** + * 场景描述 + */ + private String description; + /** + * 场景状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + + /** + * 触发器数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List triggers; + + /** + * 执行器数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List actions; + + /** + * 触发器配置 + */ + @Data + public static class TriggerConfig { + + /** + * 触发类型 + * + * 枚举 {@link IotRuleSceneTriggerTypeEnum} + */ + private Integer type; + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称数组 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private List deviceNames; + + /** + * 触发条件数组 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 时 + * 条件与条件之间,是“或”的关系 + */ + private List conditions; + + /** + * CRON 表达式 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneTriggerTypeEnum#TIMER} 时 + */ + private String cronExpression; + + } + + /** + * 触发条件 + */ + @Data + public static class TriggerCondition { + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 消息标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 参数数组 + * + * 参数与参数之间,是“或”的关系 + */ + private List parameters; + + } + + /** + * 触发条件参数 + */ + @Data + public static class TriggerConditionParameter { + + /** + * 标识符(属性、事件、服务) + * + * 关联 {@link IotThingModelDO#getIdentifier()} + */ + private String identifier; + + /** + * 操作符 + * + * 枚举 {@link IotRuleSceneTriggerConditionParameterOperatorEnum} + */ + private String operator; + + /** + * 比较值 + * + * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。 + * 例如说,{@link IotRuleSceneTriggerConditionParameterOperatorEnum#IN}、{@link IotRuleSceneTriggerConditionParameterOperatorEnum#BETWEEN} + */ + private String value; + + } + + /** + * 执行器配置 + */ + @Data + public static class ActionConfig { + + /** + * 执行类型 + * + * 枚举 {@link IotRuleSceneActionTypeEnum} + */ + private Integer type; + + /** + * 设备控制 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DEVICE_CONTROL} 时 + */ + private ActionDeviceControl deviceControl; + + /** + * 数据桥接编号 + * + * 必填:当 {@link #type} 为 {@link IotRuleSceneActionTypeEnum#DATA_BRIDGE} 时 + * 关联:{@link IotDataBridgeDO#getId()} + */ + private Long dataBridgeId; + + } + + /** + * 执行设备控制 + */ + @Data + public static class ActionDeviceControl { + + /** + * 产品标识 + * + * 关联 {@link IotProductDO#getProductKey()} + */ + private String productKey; + /** + * 设备名称数组 + * + * 关联 {@link IotDeviceDO#getDeviceName()} + */ + private List deviceNames; + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum#PROPERTY}、{@link IotDeviceMessageTypeEnum#SERVICE} + */ + private String type; + /** + * 消息标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + * + * 1. 属性设置:对应 {@link IotDeviceMessageIdentifierEnum#PROPERTY_SET} + * 2. 服务调用:对应 {@link IotDeviceMessageIdentifierEnum#SERVICE_INVOKE} + */ + private String identifier; + + /** + * 具体数据 + * + * 1. 属性设置:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#PROPERTY} 时,对应 properties + * 2. 服务调用:在 {@link #type} 是 {@link IotDeviceMessageTypeEnum#SERVICE} 时,对应 params + */ + private Map data; + + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java similarity index 63% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java index 02b2f9707..e3b4a6d9a 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thinkmodelfunction/IotThinkModelFunctionDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/IotThingModelDO.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction; +package cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -19,17 +19,17 @@ import lombok.NoArgsConstructor; /** * IoT 产品物模型功能 DO *

- * 每个 {@link IotProductDO} 和 {@link IotThinkModelFunctionDO} 是“一对多”的关系,它的每个属性、事件、服务都对应一条记录 + * 每个 {@link IotProductDO} 和 {@link IotThingModelDO} 是“一对多”的关系,它的每个属性、事件、服务都对应一条记录 * * @author 芋道源码 */ -@TableName(value = "iot_think_model_function", autoResultMap = true) -@KeySequence("iot_think_model_function_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@TableName(value = "iot_thing_model", autoResultMap = true) +@KeySequence("iot_thing_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @Builder @NoArgsConstructor @AllArgsConstructor -public class IotThinkModelFunctionDO extends BaseDO { +public class IotThingModelDO extends BaseDO { /** * 物模型功能编号 @@ -66,7 +66,7 @@ public class IotThinkModelFunctionDO extends BaseDO { /** * 功能类型 *

- * 枚举 {@link IotProductFunctionTypeEnum} + * 枚举 {@link IotThingModelTypeEnum} */ private Integer type; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java new file mode 100644 index 000000000..1f80ae455 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceGroupMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * IoT 设备分组 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotDeviceGroupMapper extends BaseMapperX { + + default PageResult selectPage(IotDeviceGroupPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDeviceGroupDO::getName, reqVO.getName()) + .betweenIfPresent(IotDeviceGroupDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDeviceGroupDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotDeviceGroupDO::getStatus, status); + } + + default IotDeviceGroupDO selectByName(String name) { + return selectOne(IotDeviceGroupDO::getName, name); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java index 0e5552f83..babbf29e7 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java @@ -1,12 +1,19 @@ package cn.iocoder.yudao.module.iot.dal.mysql.device; +import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + /** * IoT 设备 Mapper * @@ -17,27 +24,19 @@ public interface IotDeviceMapper extends BaseMapperX { default PageResult selectPage(IotDevicePageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotDeviceDO::getDeviceKey, reqVO.getDeviceKey()) .likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName()) .eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId()) - .eqIfPresent(IotDeviceDO::getProductKey, reqVO.getProductKey()) .eqIfPresent(IotDeviceDO::getDeviceType, reqVO.getDeviceType()) .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname()) - .eqIfPresent(IotDeviceDO::getGatewayId, reqVO.getGatewayId()) - .eqIfPresent(IotDeviceDO::getStatus, reqVO.getStatus()) - .betweenIfPresent(IotDeviceDO::getStatusLastUpdateTime, reqVO.getStatusLastUpdateTime()) - .betweenIfPresent(IotDeviceDO::getLastOnlineTime, reqVO.getLastOnlineTime()) - .betweenIfPresent(IotDeviceDO::getLastOfflineTime, reqVO.getLastOfflineTime()) - .betweenIfPresent(IotDeviceDO::getActiveTime, reqVO.getActiveTime()) - .eqIfPresent(IotDeviceDO::getDeviceSecret, reqVO.getDeviceSecret()) - .eqIfPresent(IotDeviceDO::getMqttClientId, reqVO.getMqttClientId()) - .likeIfPresent(IotDeviceDO::getMqttUsername, reqVO.getMqttUsername()) - .eqIfPresent(IotDeviceDO::getMqttPassword, reqVO.getMqttPassword()) - .eqIfPresent(IotDeviceDO::getAuthType, reqVO.getAuthType()) - .betweenIfPresent(IotDeviceDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(IotDeviceDO::getState, reqVO.getStatus()) + .apply(ObjectUtil.isNotNull(reqVO.getGroupId()), "FIND_IN_SET(" + reqVO.getGroupId() + ",group_ids) > 0") .orderByDesc(IotDeviceDO::getId)); } + default IotDeviceDO selectByDeviceName(String deviceName) { + return selectOne(IotDeviceDO::getDeviceName, deviceName); + } + default IotDeviceDO selectByProductKeyAndDeviceName(String productKey, String deviceName) { return selectOne(IotDeviceDO::getProductKey, productKey, IotDeviceDO::getDeviceName, deviceName); @@ -50,4 +49,48 @@ public interface IotDeviceMapper extends BaseMapperX { default Long selectCountByProductId(Long productId) { return selectCount(IotDeviceDO::getProductId, productId); } + + default IotDeviceDO selectByDeviceKey(String deviceKey) { + return selectOne(new LambdaQueryWrapper() + .apply("LOWER(device_key) = {0}", deviceKey.toLowerCase())); + } + + default List selectListByDeviceType(Integer deviceType) { + return selectList(IotDeviceDO::getDeviceType, deviceType); + } + + default List selectListByState(Integer state) { + return selectList(IotDeviceDO::getState, state); + } + + default List selectListByProductId(Long productId) { + return selectList(IotDeviceDO::getProductId, productId); + } + + default Long selectCountByGroupId(Long groupId) { + return selectCount(new LambdaQueryWrapperX() + .apply("FIND_IN_SET(" + groupId + ",group_ids) > 0")); + } + + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotDeviceDO::getCreateTime, createTime)); + } + + /** + * 查询指定产品下各状态的设备数量 + * + * @return 设备数量统计列表 + */ + // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! + List> selectDeviceCountMapByProductId(); + + // TODO @super:通过 mybatis-plus 来写哈,然后返回 Map 貌似就行了?! + /** + * 查询各个状态下的设备数量 + * + * @return 设备数量统计列表 + */ + List> selectDeviceCountGroupByState(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java new file mode 100644 index 000000000..7adf79349 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaFirmwareMapper.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 +@Mapper +public interface IotOtaFirmwareMapper extends BaseMapperX { + + /** + * 根据产品ID和固件版本号查询固件信息列表。 + * + * @param productId 产品ID,用于筛选固件信息。 + * @param version 固件版本号,用于筛选固件信息。 + * @return 返回符合条件的固件信息列表。 + */ + default List selectByProductIdAndVersion(String productId, String version) { + return selectList(IotOtaFirmwareDO::getProductId, productId, + IotOtaFirmwareDO::getVersion, version); + } + + /** + * 分页查询固件信息,支持根据名称和产品ID进行筛选,并按创建时间降序排列。 + * + * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件。 + * @return 返回分页查询结果,包含符合条件的固件信息列表。 + */ + default PageResult selectPage(IotOtaFirmwarePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotOtaFirmwareDO::getName, pageReqVO.getName()) + .eqIfPresent(IotOtaFirmwareDO::getProductId, pageReqVO.getProductId()) + .orderByDesc(IotOtaFirmwareDO::getCreateTime)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java new file mode 100644 index 000000000..5e5d8200f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeRecordMapper.java @@ -0,0 +1,159 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface IotOtaUpgradeRecordMapper extends BaseMapperX { + + // TODO @li:selectByFirmwareIdAndTaskIdAndDeviceId;让方法自解释 + /** + * 根据条件查询单个OTA升级记录 + * + * @param firmwareId 固件ID,可选参数,用于筛选固件ID匹配的记录 + * @param taskId 任务ID,可选参数,用于筛选任务ID匹配的记录 + * @param deviceId 设备ID,可选参数,用于筛选设备ID匹配的记录 + * @return 返回符合条件的单个OTA升级记录,如果不存在则返回null + */ + default IotOtaUpgradeRecordDO selectByConditions(Long firmwareId, Long taskId, String deviceId) { + // 使用LambdaQueryWrapperX构建查询条件,根据传入的参数动态添加查询条件 + return selectOne(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeRecordDO::getFirmwareId, firmwareId) + .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, taskId) + .eqIfPresent(IotOtaUpgradeRecordDO::getDeviceId, deviceId)); + } + + // TODO @li:这个是不是 groupby status 就 ok 拉? + /** + * 根据任务ID和设备名称查询OTA升级记录的状态统计信息。 + * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 + * + * @param taskId 任务ID,用于筛选特定任务的OTA升级记录。 + * @param deviceName 设备名称,支持模糊查询,用于筛选特定设备的OTA升级记录。 + * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 + */ + @Select("select count(case when status = 0 then 1 else 0) as `0` " + + "count(case when status = 1 then 1 else 0) as `1` " + + "count(case when status = 2 then 1 else 0) as `2` " + + "count(case when status = 3 then 1 else 0) as `3` " + + "count(case when status = 4 then 1 else 0) as `4` " + + "count(case when status = 5 then 1 else 0) as `5` " + + "from iot_ota_upgrade_record " + + "where task_id = #{taskId} " + + "and device_name like concat('%', #{deviceName}, '%') " + + "and status = #{status}") + List> selectOtaUpgradeRecordCount(@Param("taskId") Long taskId, + @Param("deviceName") String deviceName); + + /** + * 根据固件ID查询OTA升级记录的状态统计信息。 + * 该函数通过SQL查询统计不同状态(0到5)的记录数量,并返回一个包含统计结果的Map列表。 + * + * @param firmwareId 固件ID,用于筛选特定固件的OTA升级记录。 + * @return 返回一个Map列表,每个Map包含不同状态(0到5)的记录数量。 + */ + @Select("select count(case when status = 0 then 1 else 0) as `0` " + + "count(case when status = 1 then 1 else 0) as `1` " + + "count(case when status = 2 then 1 else 0) as `2` " + + "count(case when status = 3 then 1 else 0) as `3` " + + "count(case when status = 4 then 1 else 0) as `4` " + + "count(case when status = 5 then 1 else 0) as `5` " + + "from iot_ota_upgrade_record " + + "where firmware_id = #{firmwareId}") + List> selectOtaUpgradeRecordStatistics(Long firmwareId); + + // TODO @li:这里的注释,可以去掉哈 + /** + * 根据分页查询条件获取 OTA升级记录的分页结果 + * + * @param pageReqVO 分页查询请求参数,包含设备名称、任务ID等查询条件 + * @return 返回分页查询结果,包含符合条件的 OTA升级记录列表 + */ + // TODO @li:selectPage 就 ok 拉。 + default PageResult selectUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { + // TODO @li:这里的注释,可以去掉哈;然后下面的“如果”。。。也没必要注释 + // 使用LambdaQueryWrapperX构建查询条件,并根据请求参数动态添加查询条件 + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotOtaUpgradeRecordDO::getDeviceName, pageReqVO.getDeviceName()) // 如果设备名称存在,则添加模糊查询条件 + .eqIfPresent(IotOtaUpgradeRecordDO::getTaskId, pageReqVO.getTaskId())); // 如果任务ID存在,则添加等值查询条件 + } + + // TODO @li:这里的注释,可以去掉哈 + /** + * 根据任务ID和状态更新升级记录的状态 + *

+ * 该函数用于将符合指定任务ID和状态的升级记录的状态更新为新的状态。 + * + * @param setStatus 要设置的新状态值,类型为Integer + * @param taskId 要更新的升级记录对应的任务ID,类型为Long + * @param whereStatus 用于筛选升级记录的当前状态值,类型为Integer + */ + // TODO @li:改成 updateByTaskIdAndStatus(taskId, status, IotOtaUpgradeRecordDO) 更通用一些。 + default void updateUpgradeRecordStatusByTaskIdAndStatus(Integer setStatus, Long taskId, Integer whereStatus) { + // 使用LambdaUpdateWrapper构建更新条件,将指定状态的记录更新为指定状态 + update(new LambdaUpdateWrapper() + .set(IotOtaUpgradeRecordDO::getStatus, setStatus) + .eq(IotOtaUpgradeRecordDO::getTaskId, taskId) + .eq(IotOtaUpgradeRecordDO::getStatus, whereStatus) + ); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 根据状态查询符合条件的升级记录列表 + *

+ * 该函数使用LambdaQueryWrapperX构建查询条件,查询指定状态的升级记录。 + * + * @param state 升级记录的状态,用于筛选符合条件的记录 + * @return 返回符合指定状态的升级记录列表,类型为List + */ + default List selectUpgradeRecordListByState(Integer state) { + // 使用LambdaQueryWrapperX构建查询条件,根据状态查询符合条件的升级记录 + return selectList(new LambdaQueryWrapperX() + .eq(IotOtaUpgradeRecordDO::getStatus, state)); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 更新升级记录状态 + *

+ * 该函数用于批量更新指定ID列表中的升级记录状态。通过传入的ID列表和状态值,使用LambdaUpdateWrapper构建更新条件, + * 并执行更新操作。 + * + * @param ids 需要更新的升级记录ID列表,类型为List。传入的ID列表中的记录将被更新。 + * @param status 要更新的状态值,类型为Integer。该值将被设置到符合条件的升级记录中。 + */ + default void updateUpgradeRecordStatus(List ids, Integer status) { + // 使用LambdaUpdateWrapper构建更新条件,设置状态字段,并根据ID列表进行筛选 + update(new LambdaUpdateWrapper() + .set(IotOtaUpgradeRecordDO::getStatus, status) + .in(IotOtaUpgradeRecordDO::getId, ids) + ); + } + + // TODO @li:参考上面的建议,调整下这个方法 + /** + * 根据任务ID查询升级记录列表 + *

+ * 该函数通过任务ID查询符合条件的升级记录,并返回查询结果列表。 + * + * @param taskId 任务ID,用于筛选升级记录 + * @return 返回符合条件的升级记录列表,若未找到则返回空列表 + */ + default List selectUpgradeRecordListByTaskId(Long taskId) { + // 使用LambdaQueryWrapperX构建查询条件,根据任务ID查询符合条件的升级记录 + return selectList(new LambdaQueryWrapperX() + .eq(IotOtaUpgradeRecordDO::getTaskId, taskId)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java new file mode 100644 index 000000000..d955b1361 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/ota/IotOtaUpgradeTaskMapper.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * OTA 升级任务Mapper + * + * @author Shelly + */ +@Mapper +public interface IotOtaUpgradeTaskMapper extends BaseMapperX { + + /** + * 根据固件ID和任务名称查询升级任务列表。 + * + * @param firmwareId 固件ID,用于筛选升级任务 + * @param name 任务名称,用于筛选升级任务 + * @return 符合条件的升级任务列表 + */ + default List selectByFirmwareIdAndName(Long firmwareId, String name) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, firmwareId) + .eqIfPresent(IotOtaUpgradeTaskDO::getName, name)); + } + + /** + * 分页查询升级任务列表,支持根据固件ID和任务名称进行筛选。 + * + * @param pageReqVO 分页查询请求对象,包含分页参数和筛选条件 + * @return 分页结果,包含符合条件的升级任务列表 + */ + default PageResult selectUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotOtaUpgradeTaskDO::getFirmwareId, pageReqVO.getFirmwareId()) + .likeIfPresent(IotOtaUpgradeTaskDO::getName, pageReqVO.getName())); + } + + /** + * 根据任务状态查询升级任务列表 + *

+ * 该函数通过传入的任务状态,查询数据库中符合条件的升级任务列表。 + * + * @param status 任务状态,用于筛选升级任务的状态值 + * @return 返回符合条件的升级任务列表,列表中的每个元素为 IotOtaUpgradeTaskDO 对象 + */ + default List selectUpgradeTaskByState(Integer status) { + return selectList(IotOtaUpgradeTaskDO::getStatus, status); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java new file mode 100644 index 000000000..0e2163a3f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginConfigMapper.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.plugin; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface IotPluginConfigMapper extends BaseMapperX { + + default PageResult selectPage(PluginConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotPluginConfigDO::getName, reqVO.getName()) + .eqIfPresent(IotPluginConfigDO::getStatus, reqVO.getStatus()) + .orderByDesc(IotPluginConfigDO::getId)); + } + + default List selectListByStatusAndDeployType(Integer status, Integer deployType) { + return selectList(new LambdaQueryWrapperX() + .eq(IotPluginConfigDO::getStatus, status) + .eq(IotPluginConfigDO::getDeployType, deployType) + .orderByAsc(IotPluginConfigDO::getId)); + } + + default IotPluginConfigDO selectByPluginKey(String pluginKey) { + return selectOne(IotPluginConfigDO::getPluginKey, pluginKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java new file mode 100644 index 000000000..93ffe8728 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.plugin; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +// TODO @li:参考 IotOtaUpgradeRecordMapper 的写法 +@Mapper +public interface IotPluginInstanceMapper extends BaseMapperX { + + default IotPluginInstanceDO selectByProcessId(String processId) { + return selectOne(IotPluginInstanceDO::getProcessId, processId); + } + + default List selectListByHeartbeatTimeLt(LocalDateTime heartbeatTime) { + return selectList(new LambdaQueryWrapper() + .lt(IotPluginInstanceDO::getHeartbeatTime, heartbeatTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java new file mode 100644 index 000000000..dc9367bbd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductCategoryMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.product; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotProductCategoryMapper extends BaseMapperX { + + default PageResult selectPage(IotProductCategoryPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotProductCategoryDO::getName, reqVO.getName()) + .betweenIfPresent(IotProductCategoryDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(IotProductCategoryDO::getSort)); + } + + default List selectListByStatus(Integer status) { + return selectList(IotProductCategoryDO::getStatus, status); + } + + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotProductCategoryDO::getCreateTime, createTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java index 0341e2492..5ba4a8177 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java @@ -3,10 +3,15 @@ package cn.iocoder.yudao.module.iot.dal.mysql.product; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; + /** * IoT 产品 Mapper * @@ -23,7 +28,14 @@ public interface IotProductMapper extends BaseMapperX { } default IotProductDO selectByProductKey(String productKey) { - return selectOne(new LambdaQueryWrapperX().eq(IotProductDO::getProductKey, productKey)); + return selectOne(new LambdaQueryWrapper() + .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); } + default Long selectCountByCreateTime(@Nullable LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotProductDO::getCreateTime, createTime)); + } + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java new file mode 100644 index 000000000..303579116 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotDataBridgeMapper.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * IoT 数据桥梁 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface IotDataBridgeMapper extends BaseMapperX { + + default PageResult selectPage(IotDataBridgePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(IotDataBridgeDO::getName, reqVO.getName()) + .eqIfPresent(IotDataBridgeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(IotDataBridgeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(IotDataBridgeDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java new file mode 100644 index 000000000..e5e069a0c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.rule; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface IotRuleSceneMapper extends BaseMapperX { + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java new file mode 100644 index 000000000..082386b4e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thingmodel/IotThingModelMapper.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.iot.dal.mysql.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品物模型 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface IotThingModelMapper extends BaseMapperX { + + default PageResult selectPage(IotThingModelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(IotThingModelDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) + .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) + .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) + // TODO @芋艿:看看要不要加枚举 + .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") + .orderByDesc(IotThingModelDO::getId)); + } + + default List selectList(IotThingModelListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(IotThingModelDO::getIdentifier, reqVO.getIdentifier()) + .likeIfPresent(IotThingModelDO::getName, reqVO.getName()) + .eqIfPresent(IotThingModelDO::getType, reqVO.getType()) + .eqIfPresent(IotThingModelDO::getProductId, reqVO.getProductId()) + // TODO @芋艿:看看要不要加枚举 + .notIn(IotThingModelDO::getIdentifier, "get", "set", "post") + .orderByDesc(IotThingModelDO::getId)); + } + + default IotThingModelDO selectByProductIdAndIdentifier(Long productId, String identifier) { + return selectOne(IotThingModelDO::getProductId, productId, + IotThingModelDO::getIdentifier, identifier); + } + + default List selectListByProductId(Long productId) { + return selectList(IotThingModelDO::getProductId, productId); + } + + default List selectListByProductKey(String productKey) { + return selectList(IotThingModelDO::getProductKey, productKey); + } + + default List selectListByProductIdAndType(Long productId, Integer type) { + return selectList(IotThingModelDO::getProductId, productId, + IotThingModelDO::getType, type); + } + + default List selectListByProductIdAndIdentifiersAndTypes(Long productId, + List identifiers, + List types) { + return selectList(new LambdaQueryWrapperX() + .eq(IotThingModelDO::getProductId, productId) + .in(IotThingModelDO::getIdentifier, identifiers) + .in(IotThingModelDO::getType, types)); + } + + default IotThingModelDO selectByProductIdAndName(Long productId, String name) { + return selectOne(IotThingModelDO::getProductId, productId, + IotThingModelDO::getName, name); + } + + // TODO @super:用不到,删除下; + /** + * 统计物模型数量 + * + * @param createTime 创建时间,如果为空,则统计所有物模型数量 + * @return 物模型数量 + */ + default Long selectCountByCreateTime(LocalDateTime createTime) { + return selectCount(new LambdaQueryWrapperX() + .geIfPresent(IotThingModelDO::getCreateTime, createTime)); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java deleted file mode 100644 index e8b96e022..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/thinkmodelfunction/IotThinkModelFunctionMapper.java +++ /dev/null @@ -1,58 +0,0 @@ -package cn.iocoder.yudao.module.iot.dal.mysql.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.List; - -/** - * IoT 产品物模型 Mapper - * - * @author 芋道源码 - */ -@Mapper -public interface IotThinkModelFunctionMapper extends BaseMapperX { - - default PageResult selectPage(IotThinkModelFunctionPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .eqIfPresent(IotThinkModelFunctionDO::getIdentifier, reqVO.getIdentifier()) - .likeIfPresent(IotThinkModelFunctionDO::getName, reqVO.getName()) - .eqIfPresent(IotThinkModelFunctionDO::getType, reqVO.getType()) - .eqIfPresent(IotThinkModelFunctionDO::getProductId, reqVO.getProductId()) - .notIn(IotThinkModelFunctionDO::getIdentifier, "get", "set", "post") - .orderByDesc(IotThinkModelFunctionDO::getId)); - } - - default IotThinkModelFunctionDO selectByProductIdAndIdentifier(Long productId, String identifier) { - return selectOne(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getIdentifier, identifier); - } - - default List selectListByProductId(Long productId) { - return selectList(IotThinkModelFunctionDO::getProductId, productId); - } - - default List selectListByProductIdAndType(Long productId, Integer type) { - return selectList(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getType, type); - } - - default List selectListByProductIdAndIdentifiersAndTypes(Long productId, - List identifiers, - List types){ - return selectList(new LambdaQueryWrapperX() - .eq(IotThinkModelFunctionDO::getProductId, productId) - .in(IotThinkModelFunctionDO::getIdentifier, identifiers) - .in(IotThinkModelFunctionDO::getType, types)); - } - - default IotThinkModelFunctionDO selectByProductIdAndName(Long productId, String name) { - return selectOne(IotThinkModelFunctionDO::getProductId, productId, - IotThinkModelFunctionDO::getName, name); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..d09dac72d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.dal.redis; + +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; + +/** + * IoT Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 设备属性的数据缓存,采用 HASH 结构 + *

+ * KEY 格式:device_property:{deviceKey} + * HASH KEY:identifier 属性标识 + * VALUE 数据类型:String(JSON) {@link IotDevicePropertyDO} + */ + String DEVICE_PROPERTY = "iot:device_property:%s"; + + /** + * 设备的最后上报时间,采用 ZSET 结构 + * + * KEY 格式:{deviceKey} + * SCORE:上报时间 + */ + String DEVICE_REPORT_TIMES = "iot:device_report_times"; + + /** + * 设备信息的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:device_${productKey}_${deviceKey} + * VALUE 数据类型:String(JSON) + */ + String DEVICE = "iot:device"; + + /** + * 物模型的数据缓存,使用 Spring Cache 操作(忽略租户) + * + * KEY 格式:thing_model_${productKey} + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表 + */ + String THING_MODEL_LIST = "iot:thing_model_list"; + + /** + * 设备插件的插件进程编号的映射,采用 HASH 结构 + * + * KEY 格式:device_plugin_instance_process_ids + * HASH KEY:${deviceKey} + * VALUE:插件进程编号,对应 {@link IotPluginInstanceDO#getProcessId()} 字段 + */ + String DEVICE_PLUGIN_INSTANCE_PROCESS_IDS = "iot:device_plugin_instance_process_ids"; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java new file mode 100644 index 000000000..0f1196ab6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DevicePropertyRedisDAO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.dal.redis.device; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Collections; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants.DEVICE_PROPERTY; + +/** + * {@link IotDevicePropertyDO} 的 Redis DAO + */ +@Repository +public class DevicePropertyRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public Map get(String deviceKey) { + String redisKey = formatKey(deviceKey); + Map entries = stringRedisTemplate.opsForHash().entries(redisKey); + if (CollUtil.isEmpty(entries)) { + return Collections.emptyMap(); + } + return convertMap(entries.entrySet(), + entry -> (String) entry.getKey(), + entry -> JsonUtils.parseObject((String) entry.getValue(), IotDevicePropertyDO.class)); + } + + public void putAll(String deviceKey, Map properties) { + if (CollUtil.isEmpty(properties)) { + return; + } + String redisKey = formatKey(deviceKey); + stringRedisTemplate.opsForHash().putAll(redisKey, convertMap(properties.entrySet(), + Map.Entry::getKey, + entry -> JsonUtils.toJsonString(entry.getValue()))); + } + + private static String formatKey(String deviceKey) { + return String.format(DEVICE_PROPERTY, deviceKey); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java new file mode 100644 index 000000000..d84af7543 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceReportTimeRedisDAO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.iot.dal.redis.device; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 设备的最后上报时间的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class DeviceReportTimeRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public void update(String deviceKey, LocalDateTime reportTime) { + stringRedisTemplate.opsForZSet().add(RedisKeyConstants.DEVICE_REPORT_TIMES, deviceKey, + LocalDateTimeUtil.toEpochMilli(reportTime)); + } + + public Set range(LocalDateTime maxReportTime) { + return stringRedisTemplate.opsForZSet().rangeByScore(RedisKeyConstants.DEVICE_REPORT_TIMES, 0, + LocalDateTimeUtil.toEpochMilli(maxReportTime)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java new file mode 100644 index 000000000..32559d703 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/plugin/DevicePluginProcessIdRedisDAO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.iot.dal.redis.plugin; + +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +/** + * 设备插件的插件进程编号的缓存的 Redis DAO + */ +@Repository +public class DevicePluginProcessIdRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public void put(String deviceKey, String processId) { + stringRedisTemplate.opsForHash().put(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey, processId); + } + + public String get(String deviceKey) { + return (String) stringRedisTemplate.opsForHash().get(RedisKeyConstants.DEVICE_PLUGIN_INSTANCE_PROCESS_IDS, deviceKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java new file mode 100644 index 000000000..96741e609 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogMapper.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 设备日志 {@link IotDeviceLogDO} Mapper 接口 + */ +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDeviceLogMapper { + + /** + * 创建设备日志超级表 + */ + void createDeviceLogSTable(); + + /** + * 查询设备日志表是否存在 + * + * @return 存在则返回表名;不存在则返回 null + */ + String showDeviceLogSTable(); + + /** + * 插入设备日志数据 + * + * 如果子表不存在,会自动创建子表 + * + * @param log 设备日志数据 + */ + void insert(IotDeviceLogDO log); + + /** + * 获得设备日志分页 + * + * @param reqVO 分页查询条件 + * @return 设备日志列表 + */ + IPage selectPage(IPage page, + @Param("reqVO") IotDeviceLogPageReqVO reqVO); + + /** + * 统计设备日志数量 + * + * @param createTime 创建时间,如果为空,则统计所有日志数量 + * @return 日志数量 + */ + Long selectCountByCreateTime(@Param("createTime") Long createTime); + + // TODO @super:1)上行、下行,不写在 mapper 里,而是通过参数传递,这样,selectDeviceLogUpCountByHour、selectDeviceLogDownCountByHour 可以合并; + // TODO @super:2)不能只基于 identifier 来计算,而是要 type + identifier 成对 + /** + * 查询每个小时设备上行消息数量 + */ + List> selectDeviceLogUpCountByHour(@Param("deviceKey") String deviceKey, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime); + + /** + * 查询每个小时设备下行消息数量 + */ + List> selectDeviceLogDownCountByHour(@Param("deviceKey") String deviceKey, + @Param("startTime") Long startTime, + @Param("endTime") Long endTime); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java new file mode 100644 index 000000000..37a72e4b0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDevicePropertyMapper.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.iot.dal.tdengine; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS; +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.metadata.IPage; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Mapper +@TDengineDS +@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错 +public interface IotDevicePropertyMapper { + + List getProductPropertySTableFieldList(@Param("productKey") String productKey); + + void createProductPropertySTable(@Param("productKey") String productKey, + @Param("fields") List fields); + + @SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性 + default void alterProductPropertySTable(String productKey, + List oldFields, + List newFields) { + oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(), + TDengineTableField.FIELD_TS, "report_time", "device_key")); + List addFields = newFields.stream().filter( // 新增的字段 + newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField()))) + .collect(Collectors.toList()); + List dropFields = oldFields.stream().filter( // 删除的字段 + oldField -> newFields.stream().noneMatch(n -> n.getField().equals(oldField.getField()))) + .collect(Collectors.toList()); + List modifyTypeFields = new ArrayList<>(); // 变更类型的字段 + List modifyLengthFields = new ArrayList<>(); // 变更长度的字段 + newFields.forEach(newField -> { + TDengineTableField oldField = CollUtil.findOne(oldFields, field -> field.getField().equals(newField.getField())); + if (oldField == null) { + return; + } + if (ObjectUtil.notEqual(oldField.getType(), newField.getType())) { + modifyTypeFields.add(newField); + return; + } + if (newField.getLength() != null) { + if (newField.getLength() > oldField.getLength()) { + modifyLengthFields.add(newField); + } else if (newField.getLength() < oldField.getLength()) { + // 特殊:TDengine 长度修改时,只允许变长,所以此时认为是修改类型 + modifyTypeFields.add(newField); + } + } + }); + + // 执行 + addFields.forEach(field -> alterProductPropertySTableAddField(productKey, field)); + dropFields.forEach(field -> alterProductPropertySTableDropField(productKey, field)); + modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productKey, field)); + modifyTypeFields.forEach(field -> { + alterProductPropertySTableDropField(productKey, field); + alterProductPropertySTableAddField(productKey, field); + }); + } + + void alterProductPropertySTableAddField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void alterProductPropertySTableModifyField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void alterProductPropertySTableDropField(@Param("productKey") String productKey, + @Param("field") TDengineTableField field); + + void insert(@Param("device") IotDeviceDO device, + @Param("properties") Map properties, + @Param("reportTime") Long reportTime); + + IPage selectPageByHistory(IPage page, + @Param("reqVO") IotDevicePropertyHistoryPageReqVO reqVO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java deleted file mode 100644 index b466113f7..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.callback; - -import cn.iocoder.yudao.module.iot.emq.client.EmqxClient; -import cn.iocoder.yudao.module.iot.emq.service.EmqxService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; -import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -// TODO @芋艿:详细再瞅瞅 -/** - * 用于处理MQTT连接的回调,如连接断开、消息到达、消息发布完成、连接完成等事件。 - * - * @author ahh - */ -@Slf4j -@Component -public class EmqxCallback implements MqttCallbackExtended { - - @Lazy - @Resource - private EmqxService emqxService; - - @Lazy - @Resource - private EmqxClient emqxClient; - - @Override - public void connectionLost(Throwable throwable) { - log.info("MQTT 连接断开", throwable); - } - - @Override - public void messageArrived(String topic, MqttMessage mqttMessage) { - emqxService.subscribeCallback(topic, mqttMessage); - } - - @Override - public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { - log.info("消息发送成功: {}", iMqttDeliveryToken.getMessageId()); - } - - @Override - public void connectComplete(boolean reconnect, String serverURI) { - log.info("MQTT 已连接到服务器: {}", serverURI); - emqxService.subscribe(emqxClient.getMqttClient()); - } -} - - - diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java deleted file mode 100644 index de24585b0..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.client; - -import cn.iocoder.yudao.module.iot.emq.callback.EmqxCallback; -import cn.iocoder.yudao.module.iot.emq.config.MqttConfig; -import jakarta.annotation.Resource; -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.springframework.stereotype.Component; - -/** - * MQTT客户端类,负责建立与MQTT服务器的连接,提供发布消息和订阅主题的功能 - * - * @author ahh - */ -@Slf4j -@Data -@Component -public class EmqxClient { - - @Resource - private EmqxCallback emqxCallback; - @Resource - private MqttConfig mqttConfig; - - private MqttClient mqttClient; - - public void connect() { - if (mqttClient == null) { - createMqttClient(); - } - try { - mqttClient.connect(createMqttOptions()); - log.info("MQTT客户端连接成功"); - } catch (MqttException e) { - log.error("MQTT客户端连接失败", e); - } - } - - private void createMqttClient() { - try { - mqttClient = new MqttClient(mqttConfig.getHostUrl(), "yudao" + mqttConfig.getClientId(), new MemoryPersistence()); - mqttClient.setCallback(emqxCallback); - } catch (MqttException e) { - log.error("创建MQTT客户端失败", e); - } - } - - private MqttConnectOptions createMqttOptions() { - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(mqttConfig.getUsername()); - options.setPassword(mqttConfig.getPassword().toCharArray()); - options.setConnectionTimeout(mqttConfig.getTimeout()); - options.setKeepAliveInterval(mqttConfig.getKeepalive()); - options.setCleanSession(mqttConfig.isClearSession()); - return options; - } - - public void publish(String topic, String message) { - try { - if (mqttClient == null || !mqttClient.isConnected()) { - connect(); - } - mqttClient.publish(topic, new MqttMessage(message.getBytes())); - log.info("消息已发布到主题: {}", topic); - } catch (MqttException e) { - log.error("消息发布失败", e); - } - } - - public void subscribe(String topic) { - try { - if (mqttClient == null || !mqttClient.isConnected()) { - connect(); - } - mqttClient.subscribe(topic); - log.info("订阅了主题: {}", topic); - } catch (MqttException e) { - log.error("订阅主题失败", e); - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java deleted file mode 100644 index 9d128903c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -// TODO @芋艿:详细再瞅瞅 - -/** - * 配置类,用于读取MQTT连接的配置信息,如用户名、密码、连接地址等 - * - * @author ahh - */ -@Data -@Component -@ConfigurationProperties("iot.emq") -public class MqttConfig { - - /** - * 用户名 - */ - private String username; - /** - * 密码 - */ - private String password; - - /** - * 连接地址 - */ - private String hostUrl; - - /** - * 客户Id - */ - private String clientId; - - /** - * 默认连接话题 - */ - private String defaultTopic; - - /** - * 超时时间 - */ - private int timeout; - - /** - * 保持连接数 - */ - private int keepalive; - - /** - * 是否清除session - */ - private boolean clearSession; - - /** - * 是否共享订阅 - */ - private boolean isShared; - - /** - * 分组共享订阅 - */ - private boolean isSharedGroup; - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java deleted file mode 100644 index 0d564c39f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxService.java +++ /dev/null @@ -1,28 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.service; - -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttMessage; - -// TODO @芋艿:在瞅瞅 -/** - * 用于处理MQTT消息的具体业务逻辑,如订阅回调 - * - * @author ahh - */ -public interface EmqxService { - - /** - * 订阅回调 - * - * @param topic 主题 - * @param mqttMessage 消息 - */ - void subscribeCallback(String topic, MqttMessage mqttMessage); - - /** - * 订阅主题 - * - * @param client MQTT 客户端 - */ - void subscribe(MqttClient client); -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java deleted file mode 100644 index 0c1a87f7f..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.service; - -import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.springframework.stereotype.Service; - -// TODO @芋艿:在瞅瞅 - -/** - * 用于处理MQTT消息的具体业务逻辑,如订阅回调 - * - * @author ahh - */ -@Slf4j -@Service -public class EmqxServiceImpl implements EmqxService { - - // TODO 多线程处理消息 - @Override - public void subscribeCallback(String topic, MqttMessage mqttMessage) { - log.info("收到消息,主题: {}, 内容: {}", topic, new String(mqttMessage.getPayload())); - // 根据不同的主题,处理不同的业务逻辑 - if (topic.contains("/property/post")) { - // 设备上报数据 - } - } - - @Override - public void subscribe(MqttClient client) { - try { - // 订阅默认主题,可以根据需要修改 -// client.subscribe("$share/yudao/+/+/#", 1); - log.info("订阅默认主题成功"); - } catch (Exception e) { - log.error("订阅默认主题失败", e); - } - } -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java deleted file mode 100644 index 0c316b66c..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.iocoder.yudao.module.iot.emq.start; - -import cn.iocoder.yudao.module.iot.emq.client.EmqxClient; -import jakarta.annotation.Resource; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -// TODO @芋艿:在瞅瞅 - -/** - * 用于在应用启动时自动连接MQTT服务器 - * - * @author ahh - */ -@Component -public class EmqxStart implements ApplicationRunner { - - @Resource - private EmqxClient emqxClient; - - @Override - public void run(ApplicationArguments applicationArguments) { - emqxClient.connect(); - } -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java new file mode 100644 index 000000000..7cd6f0961 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/config/IotJobConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.framework.job.config; + +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + * IoT 模块的 Job 自动配置类 + * + * @author 芋道源码 + */ +@Configuration +public class IotJobConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotSchedulerManager iotSchedulerManager(DataSource dataSource, + ApplicationContext applicationContext) { + return new IotSchedulerManager(dataSource, applicationContext); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java new file mode 100644 index 000000000..015b9ec3f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/job/core/IotSchedulerManager.java @@ -0,0 +1,186 @@ +package cn.iocoder.yudao.module.iot.framework.job.core; + +import lombok.extern.slf4j.Slf4j; +import org.quartz.*; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.scheduling.quartz.SpringBeanJobFactory; + +import javax.sql.DataSource; +import java.util.Map; +import java.util.Properties; + +/** + * IoT 模块的 Scheduler 管理类,基于 Quartz 实现 + * + * 疑问:为什么 IoT 模块不复用全局的 SchedulerManager 呢? + * 回复:yudao-cloud 项目,使用的是 XXL-Job 作为调度中心,无法动态添加任务。 + * + * @author 芋道源码 + */ +@Slf4j +public class IotSchedulerManager { + + private static final String SCHEDULER_NAME = "iotScheduler"; + + private final SchedulerFactoryBean schedulerFactoryBean; + + private Scheduler scheduler; + + public IotSchedulerManager(DataSource dataSource, + ApplicationContext applicationContext) { + // 1. 参考 SchedulerFactoryBean 类 + SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); + SpringBeanJobFactory jobFactory = new SpringBeanJobFactory(); + jobFactory.setApplicationContext(applicationContext); + schedulerFactoryBean.setJobFactory(jobFactory); + schedulerFactoryBean.setAutoStartup(true); + schedulerFactoryBean.setSchedulerName(SCHEDULER_NAME); + schedulerFactoryBean.setDataSource(dataSource); + schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(true); + Properties properties = new Properties(); + schedulerFactoryBean.setQuartzProperties(properties); + // 2. 参考 application-local.yaml 配置文件 + // 2.1 Scheduler 相关配置 + properties.put("org.quartz.scheduler.instanceName", SCHEDULER_NAME); + properties.put("org.quartz.scheduler.instanceId", "AUTO"); + // 2.2 JobStore 相关配置 + properties.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); + properties.put("org.quartz.jobStore.isClustered", "true"); + properties.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); + properties.put("org.quartz.jobStore.misfireThreshold", "60000"); + // 2.3 线程池相关配置 + properties.put("org.quartz.threadPool.threadCount", "25"); + properties.put("org.quartz.threadPool.threadPriority", "5"); + properties.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); + this.schedulerFactoryBean = schedulerFactoryBean; + } + + public void start() throws Exception { + log.info("[start][Scheduler 初始化开始]"); + // 初始化 + schedulerFactoryBean.afterPropertiesSet(); + schedulerFactoryBean.start(); + // 获得 Scheduler 对象 + this.scheduler = schedulerFactoryBean.getScheduler(); + log.info("[start][Scheduler 初始化完成]"); + } + + public void stop() { + log.info("[stop][Scheduler 关闭开始]"); + schedulerFactoryBean.stop(); + this.scheduler = null; + log.info("[stop][Scheduler 关闭完成]"); + } + + // ========== 参考 SchedulerManager 实现 ========== + + /** + * 添加或更新 Job 到 Quartz 中 + * + * @param jobClass 任务处理器的类 + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @param jobDataMap 任务数据 + * @throws SchedulerException 添加异常 + */ + public void addOrUpdateJob(Class jobClass, String jobName, + String cronExpression, Map jobDataMap) + throws SchedulerException { + if (scheduler.checkExists(new JobKey(jobName))) { + this.updateJob(jobName, cronExpression); + } else { + this.addJob(jobClass, jobName, cronExpression, jobDataMap); + } + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobClass 任务处理器的类 + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @param jobDataMap 任务数据 + * @throws SchedulerException 添加异常 + */ + public void addJob(Class jobClass, String jobName, + String cronExpression, Map jobDataMap) + throws SchedulerException { + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(jobClass) + .usingJobData(new JobDataMap(jobDataMap)) + .withIdentity(jobName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobName, cronExpression); + // 新增 Job 调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobName 任务名 + * @param cronExpression CRON 表达式 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobName, String cronExpression) + throws SchedulerException { + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobName, cronExpression); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobName) throws SchedulerException { + // 暂停 Trigger 对象 + scheduler.pauseTrigger(new TriggerKey(jobName)); + // 取消并删除 Job 调度 + scheduler.unscheduleJob(new TriggerKey(jobName)); + scheduler.deleteJob(new JobKey(jobName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobName) throws SchedulerException { + scheduler.pauseJob(new JobKey(jobName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobName) throws SchedulerException { + scheduler.resumeJob(new JobKey(jobName)); + scheduler.resumeTrigger(new TriggerKey(jobName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobName 任务名 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(String jobName) throws SchedulerException { + scheduler.triggerJob(new JobKey(jobName)); + } + + private Trigger buildTrigger(String jobName, String cronExpression) { + return TriggerBuilder.newTrigger() + .withIdentity(jobName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .build(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java new file mode 100644 index 000000000..0a2812ac8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/config/IotPluginConfiguration.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.config; + +import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStartRunner; +import cn.iocoder.yudao.module.iot.framework.plugin.core.IotPluginStateListener; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.nio.file.Paths; + +/** + * IoT 插件配置类 + * + * @author haohao + */ +@Configuration +@Slf4j +public class IotPluginConfiguration { + + @Bean + public IotPluginStartRunner pluginStartRunner(SpringPluginManager pluginManager, + IotPluginConfigService pluginConfigService) { + return new IotPluginStartRunner(pluginManager, pluginConfigService); + } + + // TODO @芋艿:需要 review 下 + @Bean + public SpringPluginManager pluginManager(@Value("${pf4j.pluginsDir:pluginsDir}") String pluginsDir) { + log.info("[init][实例化 SpringPluginManager]"); + SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) { + + @Override + public void startPlugins() { + // 禁用插件启动,避免插件启动时,启动所有插件 + log.info("[init][禁用默认启动所有插件]"); + } + + }; + springPluginManager.addPluginStateListener(new IotPluginStateListener()); + return springPluginManager; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java new file mode 100644 index 000000000..64d258514 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStartRunner.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.core; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +import java.util.List; + +/** + * IoT 插件启动 Runner + * + * 用于 Spring Boot 启动时,启动 {@link IotPluginDeployTypeEnum#JAR} 部署类型的插件 + */ +@RequiredArgsConstructor +@Slf4j +public class IotPluginStartRunner implements ApplicationRunner { + + private final SpringPluginManager springPluginManager; + + private final IotPluginConfigService pluginConfigService; + + @Override + public void run(ApplicationArguments args) { + List pluginConfigList = TenantUtils.executeIgnore( + () -> pluginConfigService.getPluginConfigListByStatusAndDeployType( + IotPluginStatusEnum.RUNNING.getStatus(), IotPluginDeployTypeEnum.JAR.getDeployType())); + if (CollUtil.isEmpty(pluginConfigList)) { + log.info("[run][没有需要启动的插件]"); + return; + } + + // 遍历插件列表,逐个启动 + pluginConfigList.forEach(pluginConfig -> { + try { + log.info("[run][插件({}) 启动开始]", pluginConfig.getPluginKey()); + springPluginManager.startPlugin(pluginConfig.getPluginKey()); + log.info("[run][插件({}) 启动完成]", pluginConfig.getPluginKey()); + } catch (Exception e) { + log.error("[run][插件({}) 启动异常]", pluginConfig.getPluginKey(), e); + } + }); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java new file mode 100644 index 000000000..bbc73c619 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/core/IotPluginStateListener.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.iot.framework.plugin.core; + +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginStateEvent; +import org.pf4j.PluginStateListener; + +/** + * IoT 插件状态监听器,用于 log 插件的状态变化 + * + * @author haohao + */ +@Slf4j +public class IotPluginStateListener implements PluginStateListener { + + @Override + public void pluginStateChanged(PluginStateEvent event) { + log.info("[pluginStateChanged][插件({}) 状态变化,从 {} 变为 {}]", event.getPlugin().getPluginId(), + event.getOldState().toString(), event.getPluginState().toString()); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..9cf00cc10 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.iot.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.iot.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * IoT 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "iotSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("iotAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java new file mode 100644 index 000000000..c714d1027 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.iot.framework.security.core; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java new file mode 100644 index 000000000..3517e1e58 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitRunner.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.config; + +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +/** + * TDengine 表初始化的 Configuration + * + * @author alwayssuper + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class TDengineTableInitRunner implements ApplicationRunner { + + private final IotDeviceLogService deviceLogService; + + @Override + public void run(ApplicationArguments args) { + try { + // 初始化设备日志表 + deviceLogService.defineDeviceLog(); + } catch (Exception ex) { + // 初始化失败时打印错误日志并退出系统 + log.error("[run][TDengine初始化设备日志表结构失败,系统无法正常运行,即将退出]", ex); + System.exit(1); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java new file mode 100644 index 000000000..e3bbdd204 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * TDEngine 表字段 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TDengineTableField { + + /** + * 字段名 - TDengine 默认 ts 字段,默认会被 TDengine 创建 + */ + public static final String FIELD_TS = "ts"; + + public static final String TYPE_TINYINT = "TINYINT"; + public static final String TYPE_INT = "INT"; + public static final String TYPE_FLOAT = "FLOAT"; + public static final String TYPE_DOUBLE = "DOUBLE"; + public static final String TYPE_BOOL = "BOOL"; + public static final String TYPE_NCHAR = "NCHAR"; + public static final String TYPE_TIMESTAMP = "TIMESTAMP"; + + /** + * 注释 - TAG 字段 + */ + public static final String NOTE_TAG = "TAG"; + + /** + * 字段名 + */ + private String field; + + /** + * 字段类型 + */ + private String type; + + /** + * 字段长度 + */ + private Integer length; + + /** + * 注释 + */ + private String note; + + public TDengineTableField(String field, String type) { + this.field = field; + this.type = type; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java new file mode 100644 index 000000000..e3960d026 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/annotation/TDengineDS.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation; + +import com.baomidou.dynamic.datasource.annotation.DS; + +import java.lang.annotation.*; + +/** + * TDEngine 数据源 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@DS("tdengine") +public @interface TDengineDS { +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java new file mode 100644 index 000000000..f92428f7b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/package-info.java @@ -0,0 +1,4 @@ +/** + * iot 模块的 tdengine 拓展封装 + */ +package cn.iocoder.yudao.module.iot.framework.tdengine; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java new file mode 100644 index 000000000..4e9e9ecff --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/device/IotDeviceOfflineCheckJob.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.job.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * IoT 设备离线检查 Job + * + * 检测逻辑:设备最后一条 {@link cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage} 消息超过一定时间,则认为设备离线 + * + * @author 芋道源码 + */ +@Component +public class IotDeviceOfflineCheckJob implements JobHandler { + + /** + * 设备离线超时时间 + * + * TODO 芋艿:暂定 10 分钟,后续看看要不要基于设备或者全局有配置文件 + */ + public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + + @Override + @TenantJob + public String execute(String param) { + // 1.1 获得在线设备列表 + List devices = deviceService.getDeviceListByState(IotDeviceStateEnum.ONLINE.getState()); + if (CollUtil.isEmpty(devices)) { + return JsonUtils.toJsonString(Collections.emptyList()); + } + // 1.2 获取超时的 deviceKey 集合 + Set timeoutDeviceKeys = devicePropertyService.getDeviceKeysByReportTime( + LocalDateTime.now().minus(OFFLINE_TIMEOUT)); + + // 2. 下线设备 + List offlineDeviceKeys = CollUtil.newArrayList(); + for (IotDeviceDO device : devices) { + if (!timeoutDeviceKeys.contains(device.getDeviceKey())) { + continue; + } + offlineDeviceKeys.add(device.getDeviceKey()); + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 + deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) + new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((IotDeviceStateEnum.OFFLINE.getState()))); + } + return JsonUtils.toJsonString(offlineDeviceKeys); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java new file mode 100644 index 000000000..ff93dc8db --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.iot.job.plugin; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler; +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * IoT 插件实例离线检查 Job + * + * @author 芋道源码 + */ +@Component +public class IotPluginInstancesJob implements JobHandler { + + /** + * 插件离线超时时间 + * + * TODO 芋艿:暂定 10 分钟,后续看要不要做配置 + */ + public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10); + + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Override + @TenantJob + public String execute(String param) { + int count = pluginInstanceService.offlineTimeoutPluginInstance( + LocalDateTime.now().minus(OFFLINE_TIMEOUT)); + return StrUtil.format("离线超时插件实例数量为: {}", count); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java new file mode 100644 index 000000000..594f9ef0b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/rule/IotRuleSceneJob.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.iot.job.rule; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobExecutionContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import java.util.Map; + +/** + * IoT 规则场景 Job,用于执行 {@link IotRuleSceneTriggerTypeEnum#TIMER} 类型的规则场景 + * + * @author 芋道源码 + */ +@Slf4j +public class IotRuleSceneJob extends QuartzJobBean { + + /** + * JobData Key - 规则场景编号 + */ + public static final String JOB_DATA_KEY_RULE_SCENE_ID = "ruleSceneId"; + + @Resource + private IotRuleSceneService ruleSceneService; + + @Override + protected void executeInternal(JobExecutionContext context) { + // 获得规则场景编号 + Long ruleSceneId = context.getMergedJobDataMap().getLong(JOB_DATA_KEY_RULE_SCENE_ID); + + // 执行规则场景 + ruleSceneService.executeRuleSceneByTimer(ruleSceneId); + } + + /** + * 创建 JobData Map + * + * @param ruleSceneId 规则场景编号 + * @return JobData Map + */ + public static Map buildJobDataMap(Long ruleSceneId) { + return MapUtil.of(JOB_DATA_KEY_RULE_SCENE_ID, ruleSceneId); + } + + /** + * 创建 Job 名字 + * + * @param ruleSceneId 规则场景编号 + * @return Job 名字 + */ + public static String buildJobName(Long ruleSceneId) { + return String.format("%s_%d", IotRuleSceneJob.class.getSimpleName(), ruleSceneId); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java new file mode 100644 index 000000000..297267791 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceLogMessageConsumer.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.data.IotDeviceLogService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,记录设备日志 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDeviceLogMessageConsumer { + + @Resource + private IotDeviceLogService deviceLogService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + log.info("[onMessage][消息内容({})]", message); + deviceLogService.createDeviceLog(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java new file mode 100644 index 000000000..f0e49bd47 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDeviceOnlineMessageConsumer.java @@ -0,0 +1,85 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,将离线的设备,自动标记为上线 + * + * 注意:只有设备上行消息,才会触发该逻辑 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotDeviceOnlineMessageConsumer { + + @Resource + private IotDeviceService deviceService; + + @Resource + private IotDeviceUpstreamService deviceUpstreamService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + // 1.1 只处理上行消息。因为,只有设备上行的消息,才会触发设备上线的逻辑 + if (!isUpstreamMessage(message)) { + return; + } + // 1.2 如果设备已在线,则不做处理 + log.info("[onMessage][消息内容({})]", message); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + message.getProductKey(), message.getDeviceName()); + if (device == null) { + log.error("[onMessage][消息({}) 对应的设备部存在]", message); + return; + } + if (IotDeviceStateEnum.isOnline(device.getState())) { + return; + } + + // 2. 标记设备为在线 + // 为什么不直接更新状态呢?因为通过 IotDeviceMessage 可以经过一系列的处理,例如说记录日志等等 + deviceUpstreamService.updateDeviceState(((IotDeviceStateUpdateReqDTO) + new IotDeviceStateUpdateReqDTO().setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((IotDeviceStateEnum.ONLINE.getState()))); + } + + private boolean isUpstreamMessage(IotDeviceMessage message) { + // 设备属性 + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) + && Objects.equals(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { + return true; + } + // 设备事件 + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { + return true; + } + // 设备服务 + // noinspection RedundantIfStatement + if (Objects.equals(message.getType(), IotDeviceMessageTypeEnum.SERVICE.getType()) + && !StrUtil.endWith(message.getIdentifier(), IotDeviceMessageIdentifierEnum.SERVICE_REPLY_SUFFIX.getIdentifier())) { + return true; + } + return false; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java new file mode 100644 index 000000000..bf9cc5332 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/device/IotDevicePropertyMessageConsumer.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.device; + +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import jakarta.annotation.Resource; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,记录设备属性 + * + * @author alwayssuper + */ +@Component +@Slf4j +public class IotDevicePropertyMessageConsumer { + + @Resource + private IotDevicePropertyService deviceDataService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + if (ObjectUtil.notEqual(message.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType()) + || ObjectUtil.notEqual(message.getIdentifier(), IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier())) { + return; + } + log.info("[onMessage][消息内容({})]", message); + + // 保存设备属性 + deviceDataService.saveDeviceProperty(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java new file mode 100644 index 000000000..e6ea3e22d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.iot.mq.consumer.rule; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneMessageHandler { + + @Resource + private IotRuleSceneService ruleSceneService; + + @EventListener + @Async + public void onMessage(IotDeviceMessage message) { + log.info("[onMessage][消息内容({})]", message); + ruleSceneService.executeRuleSceneByDevice(message); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java new file mode 100644 index 000000000..0e8309a82 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/message/IotDeviceMessage.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.mq.message; + +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +// TODO @芋艿:参考阿里云的物模型,优化 IoT 上下行消息的设计,尽量保持一致(渐进式,不要一口气)! +/** + * IoT 设备消息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class IotDeviceMessage { + + /** + * 请求编号 + */ + private String requestId; + + /** + * 设备信息 + */ + private String productKey; + /** + * 设备名称 + */ + private String deviceName; + /** + * 设备标识 + */ + private String deviceKey; + + /** + * 消息类型 + * + * 枚举 {@link IotDeviceMessageTypeEnum} + */ + private String type; + /** + * 标识符 + * + * 枚举 {@link IotDeviceMessageIdentifierEnum} + */ + private String identifier; + + /** + * 请求参数 + * + * 例如说:属性上报的 properties、事件上报的 params + */ + private Object data; + /** + * 响应码 + * + * 目前只有 server 下行消息给 device 设备时,才会有响应码 + */ + private Integer code; + + /** + * 上报时间 + */ + private LocalDateTime reportTime; + + /** + * 租户编号 + */ + private Long tenantId; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java new file mode 100644 index 000000000..11d5d96be --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/device/IotDeviceProducer.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.mq.producer.device; + +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * IoT 设备相关消息的 Producer + * + * @author alwayssuper + * @since 2024/12/17 16:35 + */ +@Slf4j +@Component +public class IotDeviceProducer { + + @Resource + private ApplicationContext applicationContext; + + /** + * 发送 {@link IotDeviceMessage} 消息 + * + * @param thingModelMessage 物模型消息 + */ + public void sendDeviceMessage(IotDeviceMessage thingModelMessage) { + applicationContext.publishEvent(thingModelMessage); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java new file mode 100644 index 000000000..37d0ba016 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:临时占位 + */ +package cn.iocoder.yudao.module.iot.mq.producer; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java deleted file mode 100644 index 2ae08bb94..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceServiceImpl.java +++ /dev/null @@ -1,227 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.device; - -import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceStatusUpdateReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; -import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; -import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum; -import cn.iocoder.yudao.module.iot.service.product.IotProductService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.time.LocalDateTime; -import java.util.UUID; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -/** - * IoT 设备 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class DeviceServiceImpl implements IotDeviceService { - - @Resource - private IotDeviceMapper deviceMapper; - @Resource - private IotProductService productService; - - /** - * 创建 IoT 设备 - * - * @param createReqVO 创建请求 VO - * @return 设备 ID - */ - @Override - @Transactional(rollbackFor = Exception.class) - public Long createDevice(IotDeviceSaveReqVO createReqVO) { - // 1.1 校验产品是否存在 - IotProductDO product = productService.getProduct(createReqVO.getProductId()); - if (product == null) { - throw exception(PRODUCT_NOT_EXISTS); - } - // 1.2 校验设备名称在同一产品下是否唯一 - if (StrUtil.isBlank(createReqVO.getDeviceName())) { - createReqVO.setDeviceName(generateUniqueDeviceName(product.getProductKey())); - } else { - validateDeviceNameUnique(product.getProductKey(), createReqVO.getDeviceName()); - } - - // 2.1 转换 VO 为 DO - IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class) - .setProductKey(product.getProductKey()) - .setDeviceType(product.getDeviceType()); - // 2.2 生成并设置必要的字段 - device.setDeviceKey(generateUniqueDeviceKey()); - device.setDeviceSecret(generateDeviceSecret()); - device.setMqttClientId(generateMqttClientId()); - device.setMqttUsername(generateMqttUsername(device.getDeviceName(), device.getProductKey())); - device.setMqttPassword(generateMqttPassword()); - // 2.3 设置设备状态为未激活 - device.setStatus(IotDeviceStatusEnum.INACTIVE.getStatus()); - device.setStatusLastUpdateTime(LocalDateTime.now()); - // 2.4 插入到数据库 - deviceMapper.insert(device); - return device.getId(); - } - - /** - * 校验设备名称在同一产品下是否唯一 - * - * @param productKey 产品 Key - * @param deviceName 设备名称 - */ - private void validateDeviceNameUnique(String productKey, String deviceName) { - IotDeviceDO existingDevice = deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); - if (existingDevice != null) { - throw exception(DEVICE_NAME_EXISTS); - } - } - - /** - * 生成唯一的 deviceKey - * - * @return 生成的 deviceKey - */ - private String generateUniqueDeviceKey() { - return UUID.randomUUID().toString(); - } - - /** - * 生成 deviceSecret - * - * @return 生成的 deviceSecret - */ - private String generateDeviceSecret() { - return IdUtil.fastSimpleUUID(); - } - - /** - * 生成 MQTT Client ID - * - * @return 生成的 MQTT Client ID - */ - private String generateMqttClientId() { - return UUID.randomUUID().toString(); - } - - /** - * 生成 MQTT Username - * - * @param deviceName 设备名称 - * @param productKey 产品 Key - * @return 生成的 MQTT Username - */ - private String generateMqttUsername(String deviceName, String productKey) { - return deviceName + "&" + productKey; - } - - /** - * 生成 MQTT Password - * - * @return 生成的 MQTT Password - */ - private String generateMqttPassword() { - // TODO @haohao:【后续优化】在实际应用中,建议使用更安全的方法生成 MQTT Password,如加密或哈希 - return UUID.randomUUID().toString(); - } - - /** - * 生成唯一的 DeviceName - * - * @param productKey 产品标识 - * @return 生成的唯一 DeviceName - */ - private String generateUniqueDeviceName(String productKey) { - for (int i = 0; i < Short.MAX_VALUE; i++) { - String deviceName = IdUtil.fastSimpleUUID().substring(0, 20); - if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { - return deviceName; - } - } - throw new IllegalArgumentException("生成 DeviceName 失败"); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateDevice(IotDeviceSaveReqVO updateReqVO) { - // 1. 校验存在 - validateDeviceExists(updateReqVO.getId()); - - // 2. 更新到数据库 - IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class) - .setDeviceName(null).setProductId(null); // 设备名称 和 产品 ID 不能修改 - deviceMapper.updateById(updateObj); - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteDevice(Long id) { - // 1.1 校验存在 - IotDeviceDO device = validateDeviceExists(id); - // 1.2 如果是网关设备,检查是否有子设备 - if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(id) > 0) { - throw exception(DEVICE_HAS_CHILDREN); - } - - // 2. 删除设备 - deviceMapper.deleteById(id); - } - - /** - * 校验设备是否存在 - * - * @param id 设备 ID - * @return 设备对象 - */ - private IotDeviceDO validateDeviceExists(Long id) { - IotDeviceDO device = deviceMapper.selectById(id); - if (device == null) { - throw exception(DEVICE_NOT_EXISTS); - } - return device; - } - - @Override - public IotDeviceDO getDevice(Long id) { - IotDeviceDO device = deviceMapper.selectById(id); - if (device == null) { - throw exception(DEVICE_NOT_EXISTS); - } - return device; - } - - @Override - public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { - return deviceMapper.selectPage(pageReqVO); - } - - @Override - public void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO) { - // 校验存在 - validateDeviceExists(updateReqVO.getId()); - - // 更新状态和更新时间 - IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); - deviceMapper.updateById(updateObj); - } - - @Override - public Long getDeviceCountByProductId(Long productId) { - return deviceMapper.selectCountByProductId(productId); - } - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java new file mode 100644 index 000000000..5d074adb5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupService.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * IoT 设备分组 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDeviceGroupService { + + /** + * 创建设备分组 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeviceGroup(@Valid IotDeviceGroupSaveReqVO createReqVO); + + /** + * 更新设备分组 + * + * @param updateReqVO 更新信息 + */ + void updateDeviceGroup(@Valid IotDeviceGroupSaveReqVO updateReqVO); + + /** + * 删除设备分组 + * + * @param id 编号 + */ + void deleteDeviceGroup(Long id); + + /** + * 校验设备分组是否存在 + * + * @param ids 设备分组 ID 数组 + */ + default List validateDeviceGroupExists(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return ListUtil.empty(); + } + return convertList(ids, this::validateDeviceGroupExists); + } + + /** + * 校验设备分组是否存在 + * + * @param id 设备分组 ID + * @return 设备分组 + */ + IotDeviceGroupDO validateDeviceGroupExists(Long id); + + /** + * 获得设备分组 + * + * @param id 编号 + * @return 设备分组 + */ + IotDeviceGroupDO getDeviceGroup(Long id); + + /** + * 获得设备分组 + * + * @param name 名称 + * @return 设备分组 + */ + IotDeviceGroupDO getDeviceGroupByName(String name); + + /** + * 获得设备分组分页 + * + * @param pageReqVO 分页查询 + * @return 设备分组分页 + */ + PageResult getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO); + + /** + * 获得设备分组列表 + * + * @param status 状态 + * @return 设备分组列表 + */ + List getDeviceGroupListByStatus(Integer status); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java new file mode 100644 index 000000000..06e5cb11d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceGroupServiceImpl.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.group.IotDeviceGroupSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceGroupMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_GROUP_NOT_EXISTS; + +/** + * IoT 设备分组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotDeviceGroupServiceImpl implements IotDeviceGroupService { + + @Resource + private IotDeviceGroupMapper deviceGroupMapper; + + @Resource + private IotDeviceService deviceService; + + @Override + public Long createDeviceGroup(IotDeviceGroupSaveReqVO createReqVO) { + // 插入 + IotDeviceGroupDO deviceGroup = BeanUtils.toBean(createReqVO, IotDeviceGroupDO.class); + deviceGroupMapper.insert(deviceGroup); + // 返回 + return deviceGroup.getId(); + } + + @Override + public void updateDeviceGroup(IotDeviceGroupSaveReqVO updateReqVO) { + // 校验存在 + validateDeviceGroupExists(updateReqVO.getId()); + // 更新 + IotDeviceGroupDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceGroupDO.class); + deviceGroupMapper.updateById(updateObj); + } + + @Override + public void deleteDeviceGroup(Long id) { + // 1.1 校验存在 + validateDeviceGroupExists(id); + // 1.2 校验是否存在设备 + if (deviceService.getDeviceCountByGroupId(id) > 0) { + throw exception(DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS); + } + + // 删除 + deviceGroupMapper.deleteById(id); + } + + @Override + public IotDeviceGroupDO validateDeviceGroupExists(Long id) { + IotDeviceGroupDO group = deviceGroupMapper.selectById(id); + if (group == null) { + throw exception(DEVICE_GROUP_NOT_EXISTS); + } + return group; + } + + @Override + public IotDeviceGroupDO getDeviceGroup(Long id) { + return deviceGroupMapper.selectById(id); + } + + @Override + public IotDeviceGroupDO getDeviceGroupByName(String name) { + return deviceGroupMapper.selectByName(name); + } + + @Override + public PageResult getDeviceGroupPage(IotDeviceGroupPageReqVO pageReqVO) { + return deviceGroupMapper.selectPage(pageReqVO); + } + + @Override + public List getDeviceGroupListByStatus(Integer status) { + return deviceGroupMapper.selectListByStatus(status); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java index 032a7478e..1dda3f333 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java @@ -1,9 +1,17 @@ package cn.iocoder.yudao.module.iot.service.device; -import jakarta.validation.*; -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*; -import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; /** * IoT 设备 Service 接口 @@ -13,13 +21,25 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; public interface IotDeviceService { /** - * 创建设备 + * 【管理员】创建设备 * * @param createReqVO 创建信息 * @return 编号 */ Long createDevice(@Valid IotDeviceSaveReqVO createReqVO); + /** + * 【设备注册】创建设备 + * + * @param productKey 产品标识 + * @param deviceName 设备名称 + * @param gatewayId 网关设备 ID + * @return 设备 + */ + IotDeviceDO createDevice(@NotEmpty(message = "产品标识不能为空") String productKey, + @NotEmpty(message = "设备名称不能为空") String deviceName, + Long gatewayId); + /** * 更新设备 * @@ -27,13 +47,55 @@ public interface IotDeviceService { */ void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO); + // TODO @芋艿:先这么实现。未来看情况,要不要自己实现 + /** - * 删除设备 + * 更新设备的所属网关 + * + * @param id 编号 + * @param gatewayId 网关设备 ID + */ + default void updateDeviceGateway(Long id, Long gatewayId) { + updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId)); + } + + /** + * 更新设备状态 + * + * @param id 编号 + * @param state 状态 + */ + void updateDeviceState(Long id, Integer state); + + /** + * 更新设备分组 + * + * @param updateReqVO 更新信息 + */ + void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); + + /** + * 删除单个设备 * * @param id 编号 */ void deleteDevice(Long id); + /** + * 删除多个设备 + * + * @param ids 编号数组 + */ + void deleteDeviceList(Collection ids); + + /** + * 校验设备是否存在 + * + * @param id 设备 ID + * @return 设备对象 + */ + IotDeviceDO validateDeviceExists(Long id); + /** * 获得设备 * @@ -42,6 +104,14 @@ public interface IotDeviceService { */ IotDeviceDO getDevice(Long id); + /** + * 根据设备 key 获得设备 + * + * @param deviceKey 编号 + * @return IoT 设备 + */ + IotDeviceDO getDeviceByDeviceKey(String deviceKey); + /** * 获得设备分页 * @@ -51,17 +121,102 @@ public interface IotDeviceService { PageResult getDevicePage(IotDevicePageReqVO pageReqVO); /** - * 更新设备状态 + * 基于设备类型,获得设备列表 * - * @param updateReqVO 更新信息 + * @param deviceType 设备类型 + * @return 设备列表 */ - void updateDeviceStatus(IotDeviceStatusUpdateReqVO updateReqVO); + List getDeviceListByDeviceType(@Nullable Integer deviceType); /** - * 获得设备数量 + * 获得状态,获得设备列表 + * + * @param state 状态 + * @return 设备列表 + */ + List getDeviceListByState(Integer state); + + /** + * 根据产品ID获取设备列表 + * + * @param productId 产品ID,用于查询特定产品的设备列表 + * @return 返回与指定产品ID关联的设备列表,列表中的每个元素为IotDeviceDO对象 + */ + List getDeviceListByProductId(Long productId); + + /** + * 根据设备ID列表获取设备信息列表 + * + * @param deviceIdList 设备ID列表,包含需要查询的设备ID + * @return 返回与设备ID列表对应的设备信息列表,列表中的每个元素为IotDeviceDO对象 + */ + List getDeviceListByIdList(List deviceIdList); + + /** + * 基于产品编号,获得设备数量 * * @param productId 产品编号 * @return 设备数量 */ Long getDeviceCountByProductId(Long productId); + + /** + * 获得设备数量 + * + * @param groupId 分组编号 + * @return 设备数量 + */ + Long getDeviceCountByGroupId(Long groupId); + + /** + * 【缓存】根据产品 key 和设备名称,获得设备信息 + *

+ * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param productKey 产品 key + * @param deviceName 设备名称 + * @return 设备信息 + */ + IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); + + /** + * 导入设备 + * + * @param importDevices 导入设备列表 + * @param updateSupport 是否支持更新 + * @return 导入结果 + */ + IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport); + + /** + * 获得设备数量 + * + * @param createTime 创建时间,如果为空,则统计所有设备数量 + * @return 设备数量 + */ + Long getDeviceCount(@Nullable LocalDateTime createTime); + + /** + * 获取 MQTT 连接参数 + * + * @param deviceId 设备 ID + * @return MQTT 连接参数 + */ + IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId); + + /** + * 获得各个产品下的设备数量 Map + * + * @return key: 产品编号, value: 设备数量 + */ + Map getDeviceCountMapByProductId(); + + /** + * 获得各个状态下的设备数量 Map + * + * @return key: 设备状态枚举 {@link IotDeviceStateEnum} + * value: 设备数量 + */ + Map getDeviceCountMapByState(); + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java new file mode 100644 index 000000000..989f10a09 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java @@ -0,0 +1,454 @@ +package cn.iocoder.yudao.module.iot.service.device; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +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.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 设备 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceServiceImpl implements IotDeviceService { + + @Resource + private IotDeviceMapper deviceMapper; + + @Resource + private IotProductService productService; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDeviceGroupService deviceGroupService; + + @Override + public Long createDevice(IotDeviceSaveReqVO createReqVO) { + // 1.1 校验产品是否存在 + IotProductDO product = productService.getProduct(createReqVO.getProductId()); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + // 1.2 统一校验 + validateCreateDeviceParam(product.getProductKey(), createReqVO.getDeviceName(), createReqVO.getDeviceKey(), + createReqVO.getGatewayId(), product); + // 1.3 校验分组存在 + deviceGroupService.validateDeviceGroupExists(createReqVO.getGroupIds()); + + // 2. 插入到数据库 + IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); + initDevice(device, product); + deviceMapper.insert(device); + return device.getId(); + } + + @Override + public IotDeviceDO createDevice(String productKey, String deviceName, Long gatewayId) { + String deviceKey = generateDeviceKey(); + // 1.1 校验产品是否存在 + IotProductDO product = TenantUtils.executeIgnore(() -> productService.getProductByProductKey(productKey)); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + return TenantUtils.execute(product.getTenantId(), () -> { + // 1.2 校验设备名称在同一产品下是否唯一 + validateCreateDeviceParam(productKey, deviceName, deviceKey, gatewayId, product); + + // 2. 插入到数据库 + IotDeviceDO device = new IotDeviceDO().setDeviceName(deviceName).setDeviceKey(deviceKey) + .setGatewayId(gatewayId); + initDevice(device, product); + deviceMapper.insert(device); + return device; + }); + } + + private void validateCreateDeviceParam(String productKey, String deviceName, String deviceKey, + Long gatewayId, IotProductDO product) { + TenantUtils.executeIgnore(() -> { + // 校验设备名称在同一产品下是否唯一 + if (deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName) != null) { + throw exception(DEVICE_NAME_EXISTS); + } + // 校验设备标识是否唯一 + if (deviceMapper.selectByDeviceKey(deviceKey) != null) { + throw exception(DEVICE_KEY_EXISTS); + } + }); + + // 校验父设备是否为合法网关 + if (IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType()) + && gatewayId != null) { + validateGatewayDeviceExists(gatewayId); + } + } + + private void initDevice(IotDeviceDO device, IotProductDO product) { + device.setProductId(product.getId()).setProductKey(product.getProductKey()) + .setDeviceType(product.getDeviceType()); + // 生成密钥 + device.setDeviceSecret(generateDeviceSecret()); + // 设置设备状态为未激活 + device.setState(IotDeviceStateEnum.INACTIVE.getState()); + } + + @Override + public void updateDevice(IotDeviceSaveReqVO updateReqVO) { + updateReqVO.setDeviceKey(null).setDeviceName(null).setProductId(null); // 不允许更新 + // 1.1 校验存在 + IotDeviceDO device = validateDeviceExists(updateReqVO.getId()); + // 1.2 校验父设备是否为合法网关 + if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType()) + && updateReqVO.getGatewayId() != null) { + validateGatewayDeviceExists(updateReqVO.getGatewayId()); + } + // 1.3 校验分组存在 + deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); + + // 2. 更新到数据库 + IotDeviceDO updateObj = BeanUtils.toBean(updateReqVO, IotDeviceDO.class); + deviceMapper.updateById(updateObj); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDeviceGroup(IotDeviceUpdateGroupReqVO updateReqVO) { + // 1.1 校验设备存在 + List devices = deviceMapper.selectBatchIds(updateReqVO.getIds()); + if (CollUtil.isEmpty(devices)) { + return; + } + // 1.2 校验分组存在 + deviceGroupService.validateDeviceGroupExists(updateReqVO.getGroupIds()); + + // 3. 更新设备分组 + deviceMapper.updateBatch(convertList(devices, device -> new IotDeviceDO() + .setId(device.getId()).setGroupIds(updateReqVO.getGroupIds()))); + + // 4. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + public void deleteDevice(Long id) { + // 1.1 校验存在 + IotDeviceDO device = validateDeviceExists(id); + // 1.2 如果是网关设备,检查是否有子设备 + if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(id) > 0) { + throw exception(DEVICE_HAS_CHILDREN); + } + + // 2. 删除设备 + deviceMapper.deleteById(id); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDeviceList(Collection ids) { + // 1.1 校验存在 + if (CollUtil.isEmpty(ids)) { + return; + } + List devices = deviceMapper.selectBatchIds(ids); + if (CollUtil.isEmpty(devices)) { + return; + } + // 1.2 校验网关设备是否存在 + for (IotDeviceDO device : devices) { + if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(device.getId()) > 0) { + throw exception(DEVICE_HAS_CHILDREN); + } + } + + // 2. 删除设备 + deviceMapper.deleteByIds(ids); + + // 3. 清空对应缓存 + deleteDeviceCache(devices); + } + + @Override + public IotDeviceDO validateDeviceExists(Long id) { + IotDeviceDO device = deviceMapper.selectById(id); + if (device == null) { + throw exception(DEVICE_NOT_EXISTS); + } + return device; + } + + /** + * 校验网关设备是否存在 + * + * @param id 设备 ID + */ + private void validateGatewayDeviceExists(Long id) { + IotDeviceDO device = deviceMapper.selectById(id); + if (device == null) { + throw exception(DEVICE_GATEWAY_NOT_EXISTS); + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + throw exception(DEVICE_NOT_GATEWAY); + } + } + + @Override + public IotDeviceDO getDevice(Long id) { + return deviceMapper.selectById(id); + } + + @Override + public IotDeviceDO getDeviceByDeviceKey(String deviceKey) { + return deviceMapper.selectByDeviceKey(deviceKey); + } + + @Override + public PageResult getDevicePage(IotDevicePageReqVO pageReqVO) { + return deviceMapper.selectPage(pageReqVO); + } + + @Override + public List getDeviceListByDeviceType(@Nullable Integer deviceType) { + return deviceMapper.selectListByDeviceType(deviceType); + } + + @Override + public List getDeviceListByState(Integer state) { + return deviceMapper.selectListByState(state); + } + + @Override + public List getDeviceListByProductId(Long productId) { + return deviceMapper.selectListByProductId(productId); + } + + @Override + public List getDeviceListByIdList(List deviceIdList) { + return deviceMapper.selectByIds(deviceIdList); + } + + @Override + public void updateDeviceState(Long id, Integer state) { + // 1. 校验存在 + IotDeviceDO device = validateDeviceExists(id); + + // 2. 更新状态和时间 + IotDeviceDO updateObj = new IotDeviceDO().setId(id).setState(state); + if (device.getOnlineTime() == null + && Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { + updateObj.setActiveTime(LocalDateTime.now()); + } + if (Objects.equals(state, IotDeviceStateEnum.ONLINE.getState())) { + updateObj.setOnlineTime(LocalDateTime.now()); + } else if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())) { + updateObj.setOfflineTime(LocalDateTime.now()); + } + deviceMapper.updateById(updateObj); + + // 3. 清空对应缓存 + deleteDeviceCache(device); + } + + @Override + public Long getDeviceCountByProductId(Long productId) { + return deviceMapper.selectCountByProductId(productId); + } + + @Override + public Long getDeviceCountByGroupId(Long groupId) { + return deviceMapper.selectCountByGroupId(groupId); + } + + @Override + @Cacheable(value = RedisKeyConstants.DEVICE, key = "#productKey + '_' + #deviceName", unless = "#result == null") + @TenantIgnore // 忽略租户信息,跨租户 productKey + deviceName 是唯一的 + public IotDeviceDO getDeviceByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { + return deviceMapper.selectByProductKeyAndDeviceName(productKey, deviceName); + } + + /** + * 生成 deviceKey + * + * @return 生成的 deviceKey + */ + private String generateDeviceKey() { + return RandomUtil.randomString(16); + } + + /** + * 生成 deviceSecret + * + * @return 生成的 deviceSecret + */ + private String generateDeviceSecret() { + return IdUtil.fastSimpleUUID(); + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public IotDeviceImportRespVO importDevice(List importDevices, boolean updateSupport) { + // 1. 参数校验 + if (CollUtil.isEmpty(importDevices)) { + throw exception(DEVICE_IMPORT_LIST_IS_EMPTY); + } + + // 2. 遍历,逐个创建 or 更新 + IotDeviceImportRespVO respVO = IotDeviceImportRespVO.builder().createDeviceNames(new ArrayList<>()) + .updateDeviceNames(new ArrayList<>()).failureDeviceNames(new LinkedHashMap<>()).build(); + importDevices.forEach(importDevice -> { + try { + // 2.1.1 校验字段是否符合要求 + try { + ValidationUtils.validate(importDevice); + } catch (ConstraintViolationException ex) { + respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); + return; + } + // 2.1.2 校验产品是否存在 + IotProductDO product = productService.validateProductExists(importDevice.getProductKey()); + // 2.1.3 校验父设备是否存在 + Long gatewayId = null; + if (StrUtil.isNotEmpty(importDevice.getParentDeviceName())) { + IotDeviceDO gatewayDevice = deviceMapper.selectByDeviceName(importDevice.getParentDeviceName()); + if (gatewayDevice == null) { + throw exception(DEVICE_GATEWAY_NOT_EXISTS); + } + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { + throw exception(DEVICE_NOT_GATEWAY); + } + gatewayId = gatewayDevice.getId(); + } + // 2.1.4 校验设备分组是否存在 + Set groupIds = new HashSet<>(); + if (StrUtil.isNotEmpty(importDevice.getGroupNames())) { + String[] groupNames = importDevice.getGroupNames().split(","); + for (String groupName : groupNames) { + IotDeviceGroupDO group = deviceGroupService.getDeviceGroupByName(groupName); + if (group == null) { + throw exception(DEVICE_GROUP_NOT_EXISTS); + } + groupIds.add(group.getId()); + } + } + + // 2.2.1 判断如果不存在,在进行插入 + IotDeviceDO existDevice = deviceMapper.selectByDeviceName(importDevice.getDeviceName()); + if (existDevice == null) { + createDevice(new IotDeviceSaveReqVO() + .setDeviceName(importDevice.getDeviceName()).setDeviceKey(generateDeviceKey()) + .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)); + respVO.getCreateDeviceNames().add(importDevice.getDeviceName()); + return; + } + // 2.2.2 如果存在,判断是否允许更新 + if (updateSupport) { + throw exception(DEVICE_KEY_EXISTS); + } + updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId()) + .setGatewayId(gatewayId).setGroupIds(groupIds)); + respVO.getUpdateDeviceNames().add(importDevice.getDeviceName()); + } catch (ServiceException ex) { + respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); + } + }); + return respVO; + } + + @Override + public IotDeviceMqttConnectionParamsRespVO getMqttConnectionParams(Long deviceId) { + IotDeviceDO device = validateDeviceExists(deviceId); + MqttSignResult mqttSignResult = MqttSignUtils.calculate(device.getProductKey(), device.getDeviceName(), + device.getDeviceSecret()); + return new IotDeviceMqttConnectionParamsRespVO() + .setMqttClientId(mqttSignResult.getClientId()) + .setMqttUsername(mqttSignResult.getUsername()) + .setMqttPassword(mqttSignResult.getPassword()); + } + + private void deleteDeviceCache(IotDeviceDO device) { + // 保证 Spring AOP 触发 + getSelf().deleteDeviceCache0(device); + } + + private void deleteDeviceCache(List devices) { + devices.forEach(this::deleteDeviceCache); + } + + @CacheEvict(value = RedisKeyConstants.DEVICE, key = "#device.productKey + '_' + #device.deviceName") + public void deleteDeviceCache0(IotDeviceDO device) { + } + + private IotDeviceServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + + @Override + public Long getDeviceCount(LocalDateTime createTime) { + return deviceMapper.selectCountByCreateTime(createTime); + } + + // TODO @super:简化 + @Override + public Map getDeviceCountMapByProductId() { + // 查询结果转换成Map + List> list = deviceMapper.selectDeviceCountMapByProductId(); + return list.stream().collect(Collectors.toMap( + map -> Long.valueOf(map.get("key").toString()), + map -> Integer.valueOf(map.get("value").toString()) + )); + } + + @Override + public Map getDeviceCountMapByState() { + // 查询结果转换成Map + List> list = deviceMapper.selectDeviceCountGroupByState(); + return list.stream().collect(Collectors.toMap( + map -> Integer.valueOf(map.get("key").toString()), + map -> Long.valueOf(map.get("value").toString()) + )); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java new file mode 100644 index 000000000..f09604dea --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamService.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.validation.Valid; + +/** + * IoT 设备下行 Service 接口 + * + * 目的:服务端 -> 插件 -> 设备 + * + * @author 芋道源码 + */ +public interface IotDeviceDownstreamService { + + /** + * 设备下行,可用于设备模拟 + * + * @param downstreamReqVO 设备下行请求 VO + * @return 下发消息 + */ + IotDeviceMessage downstreamDevice(@Valid IotDeviceDownstreamReqVO downstreamReqVO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java new file mode 100644 index 000000000..3aab53de9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java @@ -0,0 +1,354 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED; + +/** + * IoT 设备下行 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamService { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private RestTemplate restTemplate; + + @Resource + private IotDeviceProducer deviceProducer; + + @Override + public IotDeviceMessage downstreamDevice(IotDeviceDownstreamReqVO downstreamReqVO) { + // 校验设备是否存在 + IotDeviceDO device = deviceService.validateDeviceExists(downstreamReqVO.getId()); + // TODO @芋艿:离线设备,不允许推送 + // TODO 芋艿:父设备的处理 + IotDeviceDO parentDevice = null; + + // 服务调用 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.SERVICE.getType())) { + return invokeDeviceService(downstreamReqVO, device, parentDevice); + } + // 属性相关 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { + // 属性设置 + if (Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier())) { + return setDeviceProperty(downstreamReqVO, device, parentDevice); + } + // 属性设置 + if (Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.PROPERTY_GET.getIdentifier())) { + return getDeviceProperty(downstreamReqVO, device, parentDevice); + } + } + // 配置下发 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.CONFIG.getType()) + && Objects.equals(downstreamReqVO.getIdentifier(), + IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier())) { + return setDeviceConfig(downstreamReqVO, device, parentDevice); + } + // OTA 升级 + if (Objects.equals(downstreamReqVO.getType(), IotDeviceMessageTypeEnum.OTA.getType())) { + return otaUpgrade(downstreamReqVO, device, parentDevice); + } + // TODO @芋艿:取消设备的网关的时,要不要下发 REGISTER_UNREGISTER_SUB ? + throw new IllegalArgumentException("不支持的下行消息类型:" + downstreamReqVO); + } + + /** + * 调用设备服务 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage invokeDeviceService(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + // TODO @super:【可优化】过滤掉不合法的服务 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/%s", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice), + downstreamReqVO.getIdentifier()); + IotDeviceServiceInvokeReqDTO reqDTO = new IotDeviceServiceInvokeReqDTO() + .setParams((Map) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.SERVICE.getType()).setIdentifier(reqDTO.getIdentifier()) + .setData(reqDTO.getParams()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[invokeDeviceService][设备({})服务调用失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设置设备属性 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage setDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + // TODO @super:【可优化】过滤掉不合法的属性 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/property/set", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDevicePropertySetReqDTO reqDTO = new IotDevicePropertySetReqDTO() + .setProperties((Map) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) + .setData(reqDTO.getProperties()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[setDeviceProperty][设备({})属性设置失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 获取设备属性 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings("unchecked") + private IotDeviceMessage getDeviceProperty(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof List)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 List 类型"); + } + // TODO @super:【可优化】过滤掉不合法的属性 + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/property/get", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDevicePropertyGetReqDTO reqDTO = new IotDevicePropertyGetReqDTO() + .setIdentifiers((List) downstreamReqVO.getData()); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) + .setData(reqDTO.getIdentifiers()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[getDeviceProperty][设备({})属性获取失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设置设备配置 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + @SuppressWarnings({ "unchecked", "unused" }) + private IotDeviceMessage setDeviceConfig(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数转换,无需校验 + Map config = JsonUtils.parseObject(device.getConfig(), Map.class); + + // 2. 发送请求 + String url = String.format("sys/%s/%s/thing/service/config/set", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDeviceConfigSetReqDTO reqDTO = new IotDeviceConfigSetReqDTO() + .setConfig(config); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.CONFIG.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.CONFIG_SET.getIdentifier()) + .setData(reqDTO.getConfig()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[setDeviceConfig][设备({})配置下发失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 设备 OTA 升级 + * + * @param downstreamReqVO 下行请求 + * @param device 设备 + * @param parentDevice 父设备 + * @return 下发消息 + */ + private IotDeviceMessage otaUpgrade(IotDeviceDownstreamReqVO downstreamReqVO, + IotDeviceDO device, IotDeviceDO parentDevice) { + // 1. 参数校验 + if (!(downstreamReqVO.getData() instanceof Map data)) { + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); + } + + // 2. 发送请求 + String url = String.format("ota/%s/%s/upgrade", + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); + IotDeviceOtaUpgradeReqDTO reqDTO = IotDeviceOtaUpgradeReqDTO.build(data); + CommonResult result = requestPlugin(url, reqDTO, device); + + // 3. 发送设备消息 + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) + .setType(IotDeviceMessageTypeEnum.OTA.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.OTA_UPGRADE.getIdentifier()) + .setData(downstreamReqVO.getData()); + sendDeviceMessage(message, device, result.getCode()); + + // 4. 如果不成功,抛出异常,提示用户 + if (result.isError()) { + log.error("[otaUpgrade][设备({}) OTA 升级失败,请求参数:({}),响应结果:({})]", + device.getDeviceKey(), reqDTO, result); + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); + } + return message; + } + + /** + * 请求插件 + * + * @param url URL + * @param reqDTO 请求参数,只需要设置子类的参数! + * @param device 设备 + * @return 响应结果 + */ + @SuppressWarnings({ "unchecked", "HttpUrlsUsage" }) + private CommonResult requestPlugin(String url, IotDeviceDownstreamAbstractReqDTO reqDTO, + IotDeviceDO device) { + // 获得设备对应的插件实例 + IotPluginInstanceDO pluginInstance = pluginInstanceService.getPluginInstanceByDeviceKey(device.getDeviceKey()); + if (pluginInstance == null) { + throw exception(DEVICE_DOWNSTREAM_FAILED, "设备找不到对应的插件实例"); + } + + // 补充通用参数 + reqDTO.setRequestId(IdUtil.fastSimpleUUID()); + + // 执行请求 + ResponseEntity> responseEntity; + try { + responseEntity = restTemplate.postForEntity( + String.format("http://%s:%d/%s", pluginInstance.getHostIp(), pluginInstance.getDownstreamPort(), + url), + reqDTO, (Class>) (Class) CommonResult.class); + Assert.isTrue(responseEntity.getStatusCode().is2xxSuccessful(), + "HTTP 状态码不是 2xx,而是" + responseEntity.getStatusCode()); + Assert.notNull(responseEntity.getBody(), "响应结果不能为空"); + } catch (Exception ex) { + log.error("[requestPlugin][设备({}) url({}) 下行消息失败,请求参数({})]", device.getDeviceKey(), url, reqDTO, ex); + throw exception(DEVICE_DOWNSTREAM_FAILED, ExceptionUtil.getMessage(ex)); + } + return responseEntity.getBody(); + } + + private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device, Integer code) { + // 1. 完善消息 + message.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName()) + .setDeviceKey(device.getDeviceKey()) + .setTenantId(device.getTenantId()); + Assert.notNull(message.getRequestId(), "requestId 不能为空"); + if (message.getReportTime() == null) { + message.setReportTime(LocalDateTime.now()); + } + message.setCode(code); + + // 2. 发送消息 + try { + deviceProducer.sendDeviceMessage(message); + log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); + } catch (Exception e) { + log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); + } + } + + private String getDeviceName(IotDeviceDO device, IotDeviceDO parentDevice) { + return parentDevice != null ? parentDevice.getDeviceName() : device.getDeviceName(); + } + + private String getProductKey(IotDeviceDO device, IotDeviceDO parentDevice) { + return parentDevice != null ? parentDevice.getProductKey() : device.getProductKey(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java new file mode 100644 index 000000000..dba529df2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamService.java @@ -0,0 +1,72 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import jakarta.validation.Valid; + +/** + * IoT 设备上行 Service 接口 + * + * 目的:设备 -> 插件 -> 服务端 + * + * @author 芋道源码 + */ +public interface IotDeviceUpstreamService { + + /** + * 设备上行,可用于设备模拟 + * + * @param simulatorReqVO 设备上行请求 VO + */ + void upstreamDevice(@Valid IotDeviceUpstreamReqVO simulatorReqVO); + + /** + * 更新设备状态 + * + * @param updateReqDTO 更新设备状态 DTO + */ + void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO); + + /** + * 上报设备属性数据 + * + * @param reportReqDTO 上报设备属性数据 DTO + */ + void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO); + + /** + * 上报设备事件数据 + * + * @param reportReqDTO 设备事件 + */ + void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO); + + /** + * 注册设备 + * + * @param registerReqDTO 注册设备 DTO + */ + void registerDevice(IotDeviceRegisterReqDTO registerReqDTO); + + /** + * 注册子设备 + * + * @param registerReqDTO 注册子设备 DTO + */ + void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO); + + /** + * 添加设备拓扑 + * + * @param addReqDTO 添加设备拓扑 DTO + */ + void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO); + + /** + * Emqx 连接认证 + * + * @param authReqDTO Emqx 连接认证 DTO + */ + boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java new file mode 100644 index 000000000..6c80e75ac --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceUpstreamServiceImpl.java @@ -0,0 +1,344 @@ +package cn.iocoder.yudao.module.iot.service.device.control; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceUpstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.mq.producer.device.IotDeviceProducer; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils; +import cn.iocoder.yudao.module.iot.util.MqttSignUtils.MqttSignResult; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Objects; + +/** + * IoT 设备上行 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotDeviceUpstreamServiceImpl implements IotDeviceUpstreamService { + + @Resource + private IotDeviceService deviceService; + @Resource + private IotDevicePropertyService devicePropertyService; + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private IotDeviceProducer deviceProducer; + + @Override + @SuppressWarnings("unchecked") + public void upstreamDevice(IotDeviceUpstreamReqVO simulatorReqVO) { + // 1. 校验存在 + IotDeviceDO device = deviceService.validateDeviceExists(simulatorReqVO.getId()); + + // 2.1 情况一:属性上报 + String requestId = IdUtil.fastSimpleUUID(); + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.PROPERTY.getType())) { + reportDeviceProperty(((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO() + .setRequestId(requestId).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setProperties((Map) simulatorReqVO.getData())); + return; + } + // 2.2 情况二:事件上报 + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.EVENT.getType())) { + reportDeviceEvent(((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) + .setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setIdentifier(simulatorReqVO.getIdentifier()) + .setParams((Map) simulatorReqVO.getData())); + return; + } + // 2.3 情况三:状态变更 + if (Objects.equals(simulatorReqVO.getType(), IotDeviceMessageTypeEnum.STATE.getType())) { + updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() + .setRequestId(IdUtil.fastSimpleUUID()).setReportTime(LocalDateTime.now()) + .setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName())) + .setState((Integer) simulatorReqVO.getData())); + return; + } + throw new IllegalArgumentException("未知的类型:" + simulatorReqVO.getType()); + } + + @Override + public void updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + Assert.isTrue(ObjectUtils.equalsAny(updateReqDTO.getState(), + IotDeviceStateEnum.ONLINE.getState(), IotDeviceStateEnum.OFFLINE.getState()), + "状态不合法"); + // 1.1 获得设备 + log.info("[updateDeviceState][更新设备状态: {}]", updateReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); + if (device == null) { + log.error("[updateDeviceState][设备({}/{}) 不存在]", + updateReqDTO.getProductKey(), updateReqDTO.getDeviceName()); + return; + } + TenantUtils.execute(device.getTenantId(), () -> { + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, updateReqDTO); + // 1.3 当前状态一致,不处理 + if (Objects.equals(device.getState(), updateReqDTO.getState())) { + return; + } + + // 2. 更新设备状态 + deviceService.updateDeviceState(device.getId(), updateReqDTO.getState()); + + // 3. TODO 芋艿:子设备的关联 + + // 4. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(updateReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.STATE.getType()) + .setIdentifier(ObjUtil.equals(updateReqDTO.getState(), IotDeviceStateEnum.ONLINE.getState()) + ? IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier() + : IotDeviceMessageIdentifierEnum.STATE_OFFLINE.getIdentifier()); + sendDeviceMessage(message, device); + }); + } + + @Override + public void reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + // 1.1 获得设备 + log.info("[reportDeviceProperty][上报设备属性: {}]", reportReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + if (device == null) { + log.error("[reportDeviceProperty][设备({}/{})不存在]", + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, reportReqDTO); + + // 2. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()) + .setData(reportReqDTO.getProperties()); + sendDeviceMessage(message, device); + } + + @Override + public void reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + // 1.1 获得设备 + log.info("[reportDeviceEvent][上报设备事件: {}]", reportReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + if (device == null) { + log.error("[reportDeviceEvent][设备({}/{})不存在]", + reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, reportReqDTO); + + // 2. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(reportReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.EVENT.getType()) + .setIdentifier(reportReqDTO.getIdentifier()) + .setData(reportReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + @Override + public void registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + log.info("[registerDevice][注册设备: {}]", registerReqDTO); + registerDevice0(registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), null, registerReqDTO); + } + + private void registerDevice0(String productKey, String deviceName, Long gatewayId, + IotDeviceUpstreamAbstractReqDTO registerReqDTO) { + // 1.1 注册设备 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); + boolean registerNew = device == null; + if (device == null) { + device = deviceService.createDevice(productKey, deviceName, gatewayId); + log.info("[registerDevice0][消息({}) 设备({}/{}) 成功注册]", registerReqDTO, productKey, device); + } else if (gatewayId != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) { + Long deviceId = device.getId(); + TenantUtils.execute(device.getTenantId(), + () -> deviceService.updateDeviceGateway(deviceId, gatewayId)); + log.info("[registerDevice0][消息({}) 设备({}/{}) 更新网关设备编号({})]", + registerReqDTO, productKey, device, gatewayId); + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, registerReqDTO); + + // 2. 发送设备消息 + if (registerNew) { + IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER.getIdentifier()); + sendDeviceMessage(message, device); + } + } + + @Override + public void registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + // 1.1 注册子设备 + log.info("[registerSubDevice][注册子设备: {}]", registerReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + if (device == null) { + log.error("[registerSubDevice][设备({}/{}) 不存在]", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName()); + return; + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + log.error("[registerSubDevice][设备({}/{}) 不是网关设备({}),无法进行注册]", + registerReqDTO.getProductKey(), registerReqDTO.getDeviceName(), device); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, registerReqDTO); + + // 2. 处理子设备 + if (CollUtil.isNotEmpty(registerReqDTO.getParams())) { + registerReqDTO.getParams().forEach(subDevice -> registerDevice0( + subDevice.getProductKey(), subDevice.getDeviceName(), device.getId(), registerReqDTO)); + // TODO @芋艿:后续要处理,每个设备是否成功 + } + + // 3. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(registerReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.REGISTER.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.REGISTER_REGISTER_SUB.getIdentifier()) + .setData(registerReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + @Override + public void addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + // 1.1 获得设备 + log.info("[addDeviceTopology][添加设备拓扑: {}]", addReqDTO); + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + addReqDTO.getProductKey(), addReqDTO.getDeviceName()); + if (device == null) { + log.error("[addDeviceTopology][设备({}/{}) 不存在]", + addReqDTO.getProductKey(), addReqDTO.getDeviceName()); + return; + } + if (!IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { + log.error("[addDeviceTopology][设备({}/{}) 不是网关设备({}),无法进行拓扑添加]", + addReqDTO.getProductKey(), addReqDTO.getDeviceName(), device); + return; + } + // 1.2 记录设备的最后时间 + updateDeviceLastTime(device, addReqDTO); + + // 2. 处理拓扑 + if (CollUtil.isNotEmpty(addReqDTO.getParams())) { + TenantUtils.execute(device.getTenantId(), () -> { + addReqDTO.getParams().forEach(subDevice -> { + IotDeviceDO subDeviceDO = deviceService.getDeviceByProductKeyAndDeviceNameFromCache( + subDevice.getProductKey(), subDevice.getDeviceName()); + // TODO @芋艿:后续要处理,每个设备是否成功 + if (subDeviceDO == null) { + log.error("[addDeviceTopology][子设备({}/{}) 不存在]", + subDevice.getProductKey(), subDevice.getDeviceName()); + return; + } + deviceService.updateDeviceGateway(subDeviceDO.getId(), device.getId()); + log.info("[addDeviceTopology][子设备({}/{}) 添加到网关设备({}) 成功]", + subDevice.getProductKey(), subDevice.getDeviceName(), device); + }); + }); + } + + // 3. 发送设备消息 + IotDeviceMessage message = BeanUtils.toBean(addReqDTO, IotDeviceMessage.class) + .setType(IotDeviceMessageTypeEnum.TOPOLOGY.getType()) + .setIdentifier(IotDeviceMessageIdentifierEnum.TOPOLOGY_ADD.getIdentifier()) + .setData(addReqDTO.getParams()); + sendDeviceMessage(message, device); + } + + // TODO @芋艿:后续需要考虑,http 的认证 + @Override + public boolean authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + log.info("[authenticateEmqxConnection][认证 Emqx 连接: {}]", authReqDTO); + // 1.1 校验设备是否存在。username 格式:${DeviceName}&${ProductKey} + String[] usernameParts = authReqDTO.getUsername().split("&"); + if (usernameParts.length != 2) { + log.error("[authenticateEmqxConnection][认证失败,username 格式不正确]"); + return false; + } + String deviceName = usernameParts[0]; + String productKey = usernameParts[1]; + // 1.2 获得设备 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(productKey, deviceName); + if (device == null) { + log.error("[authenticateEmqxConnection][设备({}/{}) 不存在]", productKey, deviceName); + return false; + } + // TODO @haohao:需要记录,记录设备的最后时间 + + // 2. 校验密码 + String deviceSecret = device.getDeviceSecret(); + String clientId = authReqDTO.getClientId(); + MqttSignResult sign = MqttSignUtils.calculate(productKey, deviceName, deviceSecret, clientId); + // TODO 建议,先失败,return false; + if (StrUtil.equals(sign.getPassword(), authReqDTO.getPassword())) { + log.info("[authenticateEmqxConnection][认证成功]"); + return true; + } + log.error("[authenticateEmqxConnection][认证失败,密码不正确]"); + return false; + } + + private void updateDeviceLastTime(IotDeviceDO device, IotDeviceUpstreamAbstractReqDTO reqDTO) { + // 1. 【异步】记录设备与插件实例的映射 + pluginInstanceService.updateDevicePluginInstanceProcessIdAsync(device.getDeviceKey(), reqDTO.getProcessId()); + + // 2. 【异步】更新设备的最后时间 + devicePropertyService.updateDeviceReportTimeAsync(device.getDeviceKey(), LocalDateTime.now()); + } + + private void sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { + // 1. 完善消息 + message.setDeviceKey(device.getDeviceKey()) + .setTenantId(device.getTenantId()); + if (StrUtil.isEmpty(message.getRequestId())) { + message.setRequestId(IdUtil.fastSimpleUUID()); + } + if (message.getReportTime() == null) { + message.setReportTime(LocalDateTime.now()); + } + + // 2. 发送消息 + try { + deviceProducer.sendDeviceMessage(message); + log.info("[sendDeviceMessage][message({}) 发送消息成功]", message); + } catch (Exception e) { + log.error("[sendDeviceMessage][message({}) 发送消息失败]", message, e); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java new file mode 100644 index 000000000..b79732911 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogService.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * IoT 设备日志数据 Service 接口 + * + * @author alwayssuper + */ +public interface IotDeviceLogService { + + /** + * 初始化 TDengine 超级表 + * + * 系统启动时,会自动初始化一次 + */ + void defineDeviceLog(); + + /** + * 插入设备日志 + * + * @param message 设备数据 + */ + void createDeviceLog(IotDeviceMessage message); + + /** + * 获得设备日志分页 + * + * @param pageReqVO 分页查询 + * @return 设备日志分页 + */ + PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO); + + /** + * 获得设备日志数量 + * + * @param createTime 创建时间,如果为空,则统计所有日志数量 + * @return 日志数量 + */ + Long getDeviceLogCount(@Nullable LocalDateTime createTime); + + // TODO @super:deviceKey 是不是用不上哈? + /** + * 获得每个小时设备上行消息数量统计 + * + * @param deviceKey 设备标识 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return key: 时间戳, value: 消息数量 + */ + List> getDeviceLogUpCountByHour(@Nullable String deviceKey, + @Nullable Long startTime, + @Nullable Long endTime); + + /** + * 获得每个小时设备下行消息数量统计 + * + * @param deviceKey 设备标识 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return key: 时间戳, value: 消息数量 + */ + List> getDeviceLogDownCountByHour(@Nullable String deviceKey, + @Nullable Long startTime, + @Nullable Long endTime); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java new file mode 100644 index 000000000..2ed2312bb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDeviceLogServiceImpl.java @@ -0,0 +1,112 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDeviceLogPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceLogMapper; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * IoT 设备日志数据 Service 实现类 + * + * @author alwayssuper + */ +@Service +@Slf4j +@Validated +public class IotDeviceLogServiceImpl implements IotDeviceLogService { + + @Resource + private IotDeviceLogMapper deviceLogMapper; + + @Override + public void defineDeviceLog() { + if (StrUtil.isNotEmpty(deviceLogMapper.showDeviceLogSTable())) { + log.info("[defineDeviceLog][设备日志超级表已存在,创建跳过]"); + return; + } + + log.info("[defineDeviceLog][设备日志超级表不存在,创建开始...]"); + deviceLogMapper.createDeviceLogSTable(); + log.info("[defineDeviceLog][设备日志超级表不存在,创建成功]"); + } + + @Override + public void createDeviceLog(IotDeviceMessage message) { + IotDeviceLogDO log = BeanUtils.toBean(message, IotDeviceLogDO.class) + .setId(IdUtil.fastSimpleUUID()) + .setContent(JsonUtils.toJsonString(message.getData())); + deviceLogMapper.insert(log); + } + + @Override + public PageResult getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO) { + try { + IPage page = deviceLogMapper.selectPage( + new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } catch (Exception exception) { + if (exception.getMessage().contains("Table does not exist")) { + return PageResult.empty(); + } + throw exception; + } + } + + @Override + public Long getDeviceLogCount(LocalDateTime createTime) { + return deviceLogMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + } + + // TODO @super:加一个参数,Boolean upstream:true 上行,false 下行,null 不过滤 + @Override + public List> getDeviceLogUpCountByHour(String deviceKey, Long startTime, Long endTime) { + // TODO @super:不能只基于数据库统计。因为有一些小时,可能出现没数据的情况,导致前端展示的图是不全的。可以参考 CrmStatisticsCustomerService 来实现 + List> list = deviceLogMapper.selectDeviceLogUpCountByHour(deviceKey, startTime, endTime); + return list.stream() + .map(map -> { + // 从Timestamp获取时间戳 + Timestamp timestamp = (Timestamp) map.get("time"); + Long timeMillis = timestamp.getTime(); + // 消息数量转换 + Integer count = ((Number) map.get("data")).intValue(); + return MapUtil.of(timeMillis, count); + }) + .collect(Collectors.toList()); + } + + // TODO @super:getDeviceLogDownCountByHour 融合到 getDeviceLogUpCountByHour + @Override + public List> getDeviceLogDownCountByHour(String deviceKey, Long startTime, Long endTime) { + List> list = deviceLogMapper.selectDeviceLogDownCountByHour(deviceKey, startTime, endTime); + return list.stream() + .map(map -> { + // 从Timestamp获取时间戳 + Timestamp timestamp = (Timestamp) map.get("time"); + Long timeMillis = timestamp.getTime(); + // 消息数量转换 + Integer count = ((Number) map.get("data")).intValue(); + return MapUtil.of(timeMillis, count); + }) + .collect(Collectors.toList()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java new file mode 100644 index 000000000..2f0626865 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyService.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.Set; + +/** + * IoT 设备【属性】数据 Service 接口 + * + * @author 芋道源码 + */ +public interface IotDevicePropertyService { + + // ========== 设备属性相关操作 ========== + + /** + * 定义设备属性数据的结构 + * + * @param productId 产品编号 + */ + void defineDevicePropertyData(Long productId); + + /** + * 保存设备数据 + * + * @param message 设备消息 + */ + void saveDeviceProperty(IotDeviceMessage message); + + /** + * 获得设备属性最新数据 + * + * @param deviceId 设备编号 + * @return 设备属性最新数据 + */ + Map getLatestDeviceProperties(Long deviceId); + + /** + * 获得设备属性历史数据 + * + * @param pageReqVO 分页请求 + * @return 设备属性历史数据 + */ + PageResult getHistoryDevicePropertyPage(@Valid IotDevicePropertyHistoryPageReqVO pageReqVO); + + // ========== 设备时间相关操作 ========== + + /** + * 获得最后上报时间小于指定时间的设备标识 + * + * @param maxReportTime 最大上报时间 + * @return 设备标识列表 + */ + Set getDeviceKeysByReportTime(LocalDateTime maxReportTime); + + /** + * 异步更新设备上报时间 + * + * @param deviceKey 设备标识 + * @param reportTime 上报时间 + */ + void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java new file mode 100644 index 000000000..77dde64a6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/data/IotDevicePropertyServiceImpl.java @@ -0,0 +1,200 @@ +package cn.iocoder.yudao.module.iot.service.device.data; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyHistoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO; +import cn.iocoder.yudao.module.iot.dal.redis.device.DeviceReportTimeRedisDAO; +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; + +/** + * IoT 设备【属性】数据 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { + + /** + * 物模型的数据类型,与 TDengine 数据类型的映射关系 + */ + private static final Map TYPE_MAPPING = MapUtil.builder() + .put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT) + .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT) + .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE) + .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? + .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR) + .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP) + .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! + .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! + .build(); + + @Resource + private IotDeviceService deviceService; + @Resource + private IotThingModelService thingModelService; + @Resource + private IotProductService productService; + + @Resource + private DevicePropertyRedisDAO deviceDataRedisDAO; + @Resource + private DeviceReportTimeRedisDAO deviceReportTimeRedisDAO; + + @Resource + private IotDevicePropertyMapper devicePropertyMapper; + + // ========== 设备属性相关操作 ========== + + @Override + public void defineDevicePropertyData(Long productId) { + // 1.1 查询产品和物模型 + IotProductDO product = productService.validateProductExists(productId); + List thingModels = filterList(thingModelService.getThingModelListByProductId(productId), + thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType())); + // 1.2 解析 DB 里的字段 + List oldFields = new ArrayList<>(); + try { + oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getProductKey())); + } catch (Exception e) { + if (!e.getMessage().contains("Table does not exist")) { + throw e; + } + } + + // 2.1 情况一:如果是新增的时候,需要创建表 + List newFields = buildTableFieldList(thingModels); + if (CollUtil.isEmpty(oldFields)) { + if (CollUtil.isEmpty(newFields)) { + log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId); + return; + } + devicePropertyMapper.createProductPropertySTable(product.getProductKey(), newFields); + return; + } + // 2.2 情况二:如果是修改的时候,需要更新表 + devicePropertyMapper.alterProductPropertySTable(product.getProductKey(), oldFields, newFields); + } + + private List buildTableFieldList(List thingModels) { + return convertList(thingModels, thingModel -> { + TDengineTableField field = new TDengineTableField( + StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写 + TYPE_MAPPING.get(thingModel.getProperty().getDataType())); + if (thingModel.getProperty().getDataType().equals(IotDataSpecsDataTypeEnum.TEXT.getDataType())) { + field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength()); + } + return field; + }); + } + + @Override + @TenantIgnore + public void saveDeviceProperty(IotDeviceMessage message) { + if (!(message.getData() instanceof Map)) { + log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); + return; + } + // 1. 获得设备信息 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(message.getProductKey(), message.getDeviceName()); + if (device == null) { + log.error("[saveDeviceProperty][消息({}) 对应的设备不存在]", message); + return; + } + + // 2. 根据物模型,拼接合法的属性 + // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? + List thingModels = thingModelService.getThingModelListByProductKeyFromCache(device.getProductKey()); + Map properties = new HashMap<>(); + ((Map) message.getData()).forEach((key, value) -> { + if (CollUtil.findOne(thingModels, thingModel -> thingModel.getIdentifier().equals(key)) == null) { + log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); + return; + } + properties.put((String) key, value); + }); + if (CollUtil.isEmpty(properties)) { + log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); + return; + } + + // 3.1 保存设备属性【数据】 + devicePropertyMapper.insert(device, properties, + LocalDateTimeUtil.toEpochMilli(message.getReportTime())); + + // 3.2 保存设备属性【日志】 + deviceDataRedisDAO.putAll(message.getDeviceKey(), convertMap(properties.entrySet(), Map.Entry::getKey, + entry -> IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build())); + } + + @Override + public Map getLatestDeviceProperties(Long deviceId) { + // 获取设备信息 + IotDeviceDO device = deviceService.validateDeviceExists(deviceId); + + // 获得设备属性 + return deviceDataRedisDAO.get(device.getDeviceKey()); + } + + @Override + public PageResult getHistoryDevicePropertyPage(IotDevicePropertyHistoryPageReqVO pageReqVO) { + // 获取设备信息 + IotDeviceDO device = deviceService.validateDeviceExists(pageReqVO.getDeviceId()); + pageReqVO.setDeviceKey(device.getDeviceKey()); + + try { + IPage page = devicePropertyMapper.selectPageByHistory( + new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); + return new PageResult<>(page.getRecords(), page.getTotal()); + } catch (Exception exception) { + if (exception.getMessage().contains("Table does not exist")) { + return PageResult.empty(); + } + throw exception; + } + } + + // ========== 设备时间相关操作 ========== + + @Override + public Set getDeviceKeysByReportTime(LocalDateTime maxReportTime) { + return deviceReportTimeRedisDAO.range(maxReportTime); + } + + @Override + @Async + public void updateDeviceReportTimeAsync(String deviceKey, LocalDateTime reportTime) { + deviceReportTimeRedisDAO.update(deviceKey, reportTime); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java new file mode 100644 index 000000000..99e3b382a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareService.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import jakarta.validation.Valid; + +// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 +/** + * OTA 固件管理 Service + * + * @author Shelly Chan + */ +public interface IotOtaFirmwareService { + + /** + * 创建 OTA 固件 + * + * @param saveReqVO OTA固件保存请求对象,包含固件的相关信息 + * @return 返回新创建的固件的ID + */ + Long createOtaFirmware(@Valid IotOtaFirmwareCreateReqVO saveReqVO); + + /** + * 更新 OTA 固件信息 + * + * @param updateReqVO OTA固件保存请求对象,包含需要更新的固件信息 + */ + void updateOtaFirmware(@Valid IotOtaFirmwareUpdateReqVO updateReqVO); + + /** + * 根据 ID 获取 OTA 固件信息 + * + * @param id OTA固件的唯一标识符 + * @return 返回OTA固件的详细信息对象 + */ + IotOtaFirmwareDO getOtaFirmware(Long id); + + /** + * 分页查询 OTA 固件信息 + * + * @param pageReqVO 包含分页查询条件的请求对象 + * @return 返回分页查询结果,包含固件信息列表和分页信息 + */ + PageResult getOtaFirmwarePage(@Valid IotOtaFirmwarePageReqVO pageReqVO); + + /** + * 验证物联网 OTA 固件是否存在 + * + * @param id 固件的唯一标识符 + * 该方法用于检查系统中是否存在与给定ID关联的物联网OTA固件信息 + * 主要目的是在进行固件更新操作前,确保目标固件已经存在并可以被访问 + * 如果固件不存在,该方法可能抛出异常或返回错误信息,具体行为未定义 + */ + IotOtaFirmwareDO validateFirmwareExists(Long id); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java new file mode 100644 index 000000000..7c0ddba7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaFirmwareServiceImpl.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareCreateReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwarePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.firmware.IotOtaFirmwareUpdateReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaFirmwareMapper; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_NOT_EXISTS; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE; + +@Slf4j +@Service +@Validated +public class IotOtaFirmwareServiceImpl implements IotOtaFirmwareService { + + @Resource + private IotOtaFirmwareMapper otaFirmwareMapper; + @Lazy + @Resource + private IotProductService productService; + + @Override + public Long createOtaFirmware(IotOtaFirmwareCreateReqVO saveReqVO) { + // 1. 校验固件产品 + 版本号不能重复 + validateProductAndVersionDuplicate(saveReqVO.getProductId(), saveReqVO.getVersion()); + + // 2.1.转化数据格式,准备存储到数据库中 + IotOtaFirmwareDO firmware = BeanUtils.toBean(saveReqVO, IotOtaFirmwareDO.class); + // 2.2.查询ProductKey + // TODO @li:productService.getProduct(Convert.toLong(firmware.getProductId())) 放到 1. 后面,先做参考校验。逻辑两段:1)先参数校验;2)构建对象 + 存储 + IotProductDO product = productService.getProduct(Convert.toLong(firmware.getProductId())); + firmware.setProductKey(Objects.requireNonNull(product).getProductKey()); + // TODO @芋艿: 附件、附件签名等属性的计算 + otaFirmwareMapper.insert(firmware); + return firmware.getId(); + } + + @Override + public void updateOtaFirmware(IotOtaFirmwareUpdateReqVO updateReqVO) { + // 1. 校验存在 + validateFirmwareExists(updateReqVO.getId()); + + // 2. 更新数据 + IotOtaFirmwareDO updateObj = BeanUtils.toBean(updateReqVO, IotOtaFirmwareDO.class); + otaFirmwareMapper.updateById(updateObj); + } + + @Override + public IotOtaFirmwareDO getOtaFirmware(Long id) { + return otaFirmwareMapper.selectById(id); + } + + @Override + public PageResult getOtaFirmwarePage(IotOtaFirmwarePageReqVO pageReqVO) { + return otaFirmwareMapper.selectPage(pageReqVO); + } + + @Override + public IotOtaFirmwareDO validateFirmwareExists(Long id) { + IotOtaFirmwareDO firmware = otaFirmwareMapper.selectById(id); + if (firmware == null) { + throw exception(OTA_FIRMWARE_NOT_EXISTS); + } + return firmware; + } + + // TODO @li:注释有点冗余 + /** + * 验证产品和版本号是否重复 + *

+ * 该方法用于确保在系统中不存在具有相同产品ID和版本号的固件条目 + * 它通过调用otaFirmwareMapper的selectByProductIdAndVersion方法来查询数据库中是否存在匹配的产品ID和版本号的固件信息 + * 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在,从而避免数据重复 + * + * @param productId 产品ID,用于数据库查询 + * @param version 版本号,用于数据库查询 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,提示固件信息已存在 + */ + private void validateProductAndVersionDuplicate(String productId, String version) { + // 查询数据库中是否存在具有相同产品ID和版本号的固件信息 + List list = otaFirmwareMapper.selectByProductIdAndVersion(productId, version); + // 如果查询结果非空且不为null,则抛出异常,提示固件信息已存在 + if (CollUtil.isNotEmpty(list)) { + throw exception(OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java new file mode 100644 index 000000000..cbf900ac0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordService.java @@ -0,0 +1,104 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import jakarta.validation.Valid; + +import java.util.List; +import java.util.Map; + +// TODO @li:注释写的有点冗余,可以看看别的模块哈。= = AI 生成的注释,有的时候太啰嗦了,需要处理下的哈 +/** + * IotOtaUpgradeRecordService 接口定义了与物联网设备OTA升级记录相关的操作。 + * 该接口提供了创建、更新、查询、统计和重试升级记录的功能。 + */ +public interface IotOtaUpgradeRecordService { + + /** + * 批量创建 OTA 升级记录 + * 该函数用于为指定的设备列表、固件ID和升级任务ID创建OTA升级记录。 + * + * @param deviceIds 设备ID列表,表示需要升级的设备集合。 + * @param firmwareId 固件ID,表示要升级到的固件版本。 + * @param upgradeTaskId 升级任务ID,表示此次升级任务的唯一标识。 + */ + void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId); + + /** + * 获取 OTA 升级记录的数量统计 + * + * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录数量 + */ + Map getOtaUpgradeRecordCount(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); + + /** + * 获取 OTA 升级记录的统计信息。 + * + * @return 返回一个 Map,其中键为状态码,值为对应状态的升级记录统计信息 + */ + Map getOtaUpgradeRecordStatistics(Long firmwareId); + + /** + * 重试指定的 OTA 升级记录 + * + * @param id 需要重试的升级记录的ID。 + */ + void retryUpgradeRecord(Long id); + + /** + * 获取指定 ID 的 OTA 升级记录的详细信息。 + * + * @param id 需要查询的升级记录的ID。 + * @return 返回包含升级记录详细信息的响应对象。 + */ + IotOtaUpgradeRecordDO getUpgradeRecord(Long id); + + /** + * 分页查询 OTA 升级记录。 + * + * @param pageReqVO 包含分页查询条件的请求对象,必须经过验证。 + * @return 返回包含分页查询结果的响应对象。 + */ + PageResult getUpgradeRecordPage(@Valid IotOtaUpgradeRecordPageReqVO pageReqVO); + + /** + * 根据任务 ID 取消升级记录 + *

+ * 该函数用于根据给定的任务ID,取消与该任务相关的升级记录。通常用于在任务执行失败或用户手动取消时, + * 清理或标记相关的升级记录为取消状态。 + * + * @param taskId 要取消升级记录的任务ID。该ID唯一标识一个任务,通常由任务管理系统生成。 + */ + void cancelUpgradeRecordByTaskId(Long taskId); + + // TODO @li:不要的方法,可以删除下哈。 + /** + * 根据升级状态获取升级记录列表 + * + * @param state 升级状态,用于筛选符合条件的升级记录 + * @return 返回符合指定状态的升级记录列表,列表中的元素为 {@link IotOtaUpgradeRecordDO} 对象 + */ + List getUpgradeRecordListByState(Integer state); + + /** + * 更新升级记录的状态。 + *

+ * 该函数用于批量更新指定升级记录的状态。通过传入的ID列表和状态值,将对应的升级记录的状态更新为指定的值。 + * + * @param ids 需要更新状态的升级记录的ID列表。列表中的每个元素代表一个升级记录的ID。 + * @param status 要更新的状态值。该值应为有效的状态标识符,通常为整数类型。 + */ + void updateUpgradeRecordStatus(List ids, Integer status); + + /** + * 根据任务ID获取升级记录列表 + *

+ * 该函数通过给定的任务ID,查询并返回与该任务相关的所有升级记录。 + * + * @param taskId 任务ID,用于指定需要查询的任务 + * @return 返回一个包含升级记录的列表,列表中的每个元素为IotOtaUpgradeRecordDO对象 + */ + List getUpgradeRecordListByTaskId(Long taskId); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java new file mode 100644 index 000000000..02ef39cdf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeRecordServiceImpl.java @@ -0,0 +1,229 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.record.IotOtaUpgradeRecordPageReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeRecordDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeRecordMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +// TODO @li:@Service、@Validated、@Slf4j,先用关键注解;2)类注释,简单写 +@Slf4j +@Service +@Validated +public class IotOtaUpgradeRecordServiceImpl implements IotOtaUpgradeRecordService { + + @Resource + private IotOtaUpgradeRecordMapper upgradeRecordMapper; + // TODO @li:1)@Resource 写在 @Lazy 之前,先用关键注解;2)有必要的情况下,在写 @Lazy 注解。 + @Lazy + @Resource + private IotDeviceService deviceService; + @Lazy + @Resource + private IotOtaFirmwareService firmwareService; + @Lazy + @Resource + private IotOtaUpgradeTaskService upgradeTaskService; + + @Override + public void createOtaUpgradeRecordBatch(List deviceIds, Long firmwareId, Long upgradeTaskId) { + // 1. 校验升级记录信息是否存在,并且已经取消的任务可以重新开始 + // TODO @li:批量查询。。 + deviceIds.forEach(deviceId -> validateUpgradeRecordDuplicate(firmwareId, upgradeTaskId, String.valueOf(deviceId))); + + // 2.初始化OTA升级记录列表信息 + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskService.getUpgradeTask(upgradeTaskId); + IotOtaFirmwareDO firmware = firmwareService.getOtaFirmware(firmwareId); + List deviceList = deviceService.getDeviceListByIdList(deviceIds); + List upgradeRecordList = deviceList.stream().map(device -> { + IotOtaUpgradeRecordDO upgradeRecord = new IotOtaUpgradeRecordDO(); + upgradeRecord.setFirmwareId(firmware.getId()); + upgradeRecord.setTaskId(upgradeTask.getId()); + upgradeRecord.setProductKey(device.getProductKey()); + upgradeRecord.setDeviceName(device.getDeviceName()); + upgradeRecord.setDeviceId(Convert.toStr(device.getId())); + upgradeRecord.setFromFirmwareId(Convert.toLong(device.getFirmwareId())); + upgradeRecord.setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); + upgradeRecord.setProgress(0); + return upgradeRecord; + }).toList(); + // 3.保存数据 + upgradeRecordMapper.insertBatch(upgradeRecordList); + // TODO @芋艿:在这里需要处理推送升级任务的逻辑 + + } + + // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 + /** + * 获取OTA升级记录的数量统计。 + * 该方法根据传入的查询条件,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 + * + * @param pageReqVO 包含查询条件的请求对象,主要包括任务ID和设备名称等信息。 + * @return 返回一个Map,其中键为状态常量,值为对应状态的记录数量。 + */ + @Override + @Transactional + public Map getOtaUpgradeRecordCount(IotOtaUpgradeRecordPageReqVO pageReqVO) { + // 分别查询不同状态的OTA升级记录数量 + List> upgradeRecordCountList = upgradeRecordMapper.selectOtaUpgradeRecordCount( + pageReqVO.getTaskId(), pageReqVO.getDeviceName()); + Map upgradeRecordCountMap = ObjectUtils.defaultIfNull(upgradeRecordCountList.get(0)); + Objects.requireNonNull(upgradeRecordCountMap); + return upgradeRecordCountMap.entrySet().stream().collect(Collectors.toMap( + entry -> Convert.toInt(entry.getKey()), + entry -> Convert.toLong(entry.getValue()))); + } + + // TODO @li:1)方法注释,简单写;2)父类写了注释,子类就不用写了。。。 + /** + * 获取指定固件ID的OTA升级记录统计信息。 + * 该方法通过查询数据库,统计不同状态的OTA升级记录数量,并返回一个包含各状态数量的映射。 + * + * @param firmwareId 固件ID,用于指定需要统计的固件升级记录。 + * @return 返回一个Map,其中键为升级记录状态(如PENDING、PUSHED等),值为对应状态的记录数量。 + */ + @Override + @Transactional + public Map getOtaUpgradeRecordStatistics(Long firmwareId) { + // 查询并统计不同状态的OTA升级记录数量 + List> upgradeRecordStatisticsList = upgradeRecordMapper.selectOtaUpgradeRecordStatistics(firmwareId); + Map upgradeRecordStatisticsMap = ObjectUtils.defaultIfNull(upgradeRecordStatisticsList.get(0)); + Objects.requireNonNull(upgradeRecordStatisticsMap); + return upgradeRecordStatisticsMap.entrySet().stream().collect(Collectors.toMap( + entry -> Convert.toInt(entry.getKey()), + entry -> Convert.toLong(entry.getValue()))); + } + + @Override + public void retryUpgradeRecord(Long id) { + // 1.1.校验升级记录信息是否存在 + IotOtaUpgradeRecordDO upgradeRecord = validateUpgradeRecordExists(id); + // 1.2.校验升级记录是否可以重新升级 + validateUpgradeRecordCanRetry(upgradeRecord); + + // 2. 将一些数据重置,这样定时任务轮询就可以重启任务 + // TODO @li:更新的时候,wherestatus; + upgradeRecordMapper.updateById(new IotOtaUpgradeRecordDO() + .setId(upgradeRecord.getId()).setProgress(0) + .setStatus(IotOtaUpgradeRecordStatusEnum.PENDING.getStatus())); + } + + @Override + public IotOtaUpgradeRecordDO getUpgradeRecord(Long id) { + return upgradeRecordMapper.selectById(id); + } + + @Override + public PageResult getUpgradeRecordPage(IotOtaUpgradeRecordPageReqVO pageReqVO) { + return upgradeRecordMapper.selectUpgradeRecordPage(pageReqVO); + } + + @Override + public void cancelUpgradeRecordByTaskId(Long taskId) { + // 暂定只有待推送的升级记录可以取消 TODO @芋艿:可以看看阿里云,哪些可以取消 + upgradeRecordMapper.updateUpgradeRecordStatusByTaskIdAndStatus( + IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus(), taskId, + IotOtaUpgradeRecordStatusEnum.PENDING.getStatus()); + } + + @Override + public List getUpgradeRecordListByState(Integer state) { + return upgradeRecordMapper.selectUpgradeRecordListByState(state); + } + + @Override + public void updateUpgradeRecordStatus(List ids, Integer status) { + upgradeRecordMapper.updateUpgradeRecordStatus(ids, status); + } + + @Override + public List getUpgradeRecordListByTaskId(Long taskId) { + return upgradeRecordMapper.selectUpgradeRecordListByTaskId(taskId); + } + + /** + * 验证指定的升级记录是否存在。 + *

+ * 该函数通过给定的ID查询升级记录,如果查询结果为空,则抛出异常,表示升级记录不存在。 + * + * @param id 升级记录的唯一标识符,类型为Long。 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出异常,异常类型为OTA_UPGRADE_RECORD_NOT_EXISTS。 + */ + private IotOtaUpgradeRecordDO validateUpgradeRecordExists(Long id) { + // 根据ID查询升级记录 + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectById(id); + // 如果查询结果为空,抛出异常 + if (upgradeRecord == null) { + throw exception(OTA_UPGRADE_RECORD_NOT_EXISTS); + } + return upgradeRecord; + } + + // TODO @li:注释有点冗余 + /** + * 校验固件升级记录是否重复。 + *

+ * 该函数用于检查给定的固件ID、任务ID和设备ID是否已经存在未取消的升级记录。 + * 如果存在未取消的记录,则抛出异常,提示升级记录重复。 + * + * @param firmwareId 固件ID,用于标识特定的固件版本 + * @param taskId 任务ID,用于标识特定的升级任务 + * @param deviceId 设备ID,用于标识特定的设备 + */ + private void validateUpgradeRecordDuplicate(Long firmwareId, Long taskId, String deviceId) { + // 根据条件查询升级记录 + IotOtaUpgradeRecordDO upgradeRecord = upgradeRecordMapper.selectByConditions(firmwareId, taskId, deviceId); + // 如果查询到升级记录且状态不是已取消,则抛出异常 + // TODO @li:if return,减少括号层级; + // TODO @li:ObjUtil.notEquals,尽量不用 !取否逻辑; + if (upgradeRecord != null) { + if (!IotOtaUpgradeRecordStatusEnum.CANCELED.getStatus().equals(upgradeRecord.getStatus())) { + // TODO @li:提示的时候,需要把 deviceName 给提示出来,不然用户不知道哪个重复啦。 + throw exception(OTA_UPGRADE_RECORD_DUPLICATE); + } + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级记录是否可以重试。 + *

+ * 该方法用于检查给定的升级记录是否处于允许重试的状态。如果升级记录的状态为 + * PENDING、PUSHED 或 UPGRADING,则抛出异常,表示不允许重试。 + * + * @param upgradeRecord 需要验证的升级记录对象,类型为 IotOtaUpgradeRecordDO + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,则抛出 OTA_UPGRADE_RECORD_CANNOT_RETRY 异常 + */ + // TODO @li:这种一次性的方法(不复用的),其实一步一定要抽成小方法; + private void validateUpgradeRecordCanRetry(IotOtaUpgradeRecordDO upgradeRecord) { + // 检查升级记录的状态是否为 PENDING、PUSHED 或 UPGRADING + if (ObjectUtils.equalsAny(upgradeRecord.getStatus(), + IotOtaUpgradeRecordStatusEnum.PENDING.getStatus(), + IotOtaUpgradeRecordStatusEnum.PUSHED.getStatus(), + IotOtaUpgradeRecordStatusEnum.UPGRADING.getStatus())) { + // 如果升级记录处于上述状态之一,则抛出异常,表示不允许重试 + throw exception(OTA_UPGRADE_RECORD_CANNOT_RETRY); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java new file mode 100644 index 000000000..a2a810bf0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskService.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * IoT OTA升级任务 Service 接口 + * + * @author Shelly Chan + */ +public interface IotOtaUpgradeTaskService { + + /** + * 创建OTA升级任务 + * + * @param createReqVO OTA升级任务的创建请求对象,包含创建任务所需的信息 + * @return 创建成功的OTA升级任务的ID + */ + Long createUpgradeTask(@Valid IotOtaUpgradeTaskSaveReqVO createReqVO); + + /** + * 取消OTA升级任务 + * + * @param id 要取消的OTA升级任务的ID + */ + void cancelUpgradeTask(Long id); + + /** + * 根据ID获取OTA升级任务的详细信息 + * + * @param id OTA升级任务的ID + * @return OTA升级任务的详细信息对象 + */ + IotOtaUpgradeTaskDO getUpgradeTask(Long id); + + /** + * 分页查询OTA升级任务 + * + * @param pageReqVO OTA升级任务的分页查询请求对象,包含查询条件和分页信息 + * @return 分页查询结果,包含OTA升级任务列表和总记录数 + */ + PageResult getUpgradeTaskPage(@Valid IotOtaUpgradeTaskPageReqVO pageReqVO); + + /** + * 根据任务状态获取升级任务列表 + * + * @param state 任务状态,用于筛选符合条件的升级任务 + * @return 返回符合指定状态的升级任务列表,列表中的元素为 IotOtaUpgradeTaskDO 对象 + */ + List getUpgradeTaskByState(Integer state); + + /** + * 更新升级任务的状态。 + *

+ * 该函数用于根据任务ID更新指定升级任务的状态。通常用于在任务执行过程中 + * 更新任务的状态,例如从“进行中”变为“已完成”或“失败”。 + * + * @param id 升级任务的唯一标识符,类型为Long。不能为null。 + * @param status 要更新的任务状态,类型为Integer。通常表示任务的状态码,如0表示未开始,1表示进行中,2表示已完成等。 + */ + void updateUpgradeTaskStatus(Long id, Integer status); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java new file mode 100644 index 000000000..cee3ba516 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/ota/IotOtaUpgradeTaskServiceImpl.java @@ -0,0 +1,207 @@ +package cn.iocoder.yudao.module.iot.service.ota; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.ota.vo.upgrade.task.IotOtaUpgradeTaskSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaFirmwareDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.ota.IotOtaUpgradeTaskDO; +import cn.iocoder.yudao.module.iot.dal.mysql.ota.IotOtaUpgradeTaskMapper; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskScopeEnum; +import cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeTaskStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +// TODO @li:完善注释、注解顺序 +@Slf4j +@Service +@Validated +public class IotOtaUpgradeTaskServiceImpl implements IotOtaUpgradeTaskService { + + @Resource + private IotOtaUpgradeTaskMapper upgradeTaskMapper; + + @Resource + @Lazy + private IotDeviceService deviceService; + @Resource + @Lazy + private IotOtaFirmwareService firmwareService; + @Resource + @Lazy + private IotOtaUpgradeRecordService upgradeRecordService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO) { + // 1.1 校验同一固件的升级任务名称不重复 + validateFirmwareTaskDuplicate(createReqVO.getFirmwareId(), createReqVO.getName()); + // 1.2 校验固件信息是否存在 + IotOtaFirmwareDO firmware = firmwareService.validateFirmwareExists(createReqVO.getFirmwareId()); + // 1.3 补全设备范围信息,并且校验是否又设备可以升级,如果没有设备可以升级,则报错 + validateScopeAndDevice(createReqVO.getScope(), createReqVO.getDeviceIds(), firmware.getProductId()); + + // 2. 保存 OTA 升级任务信息到数据库 + IotOtaUpgradeTaskDO upgradeTask = initOtaUpgradeTask(createReqVO, firmware.getProductId()); + upgradeTaskMapper.insert(upgradeTask); + + // 3. 生成设备升级记录信息并存储,等待定时任务轮询 + upgradeRecordService.createOtaUpgradeRecordBatch(upgradeTask.getDeviceIds(), firmware.getId(), upgradeTask.getId()); + return upgradeTask.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelUpgradeTask(Long id) { + // 1.1 校验升级任务是否存在 + IotOtaUpgradeTaskDO upgradeTask = validateUpgradeTaskExists(id); + // 1.2 校验升级任务是否可以取消 + // TODO @li:ObjUtil notequals + if (!Objects.equals(upgradeTask.getStatus(), IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus())) { + throw exception(OTA_UPGRADE_TASK_CANNOT_CANCEL); + } + + // 2. 更新 OTA 升级任务状态为已取消 + upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder() + .id(id).status(IotOtaUpgradeTaskStatusEnum.CANCELED.getStatus()) + .build()); + + // 3. 更新 OTA 升级记录状态为已取消 + upgradeRecordService.cancelUpgradeRecordByTaskId(id); + } + + @Override + public IotOtaUpgradeTaskDO getUpgradeTask(Long id) { + return upgradeTaskMapper.selectById(id); + } + + @Override + public PageResult getUpgradeTaskPage(IotOtaUpgradeTaskPageReqVO pageReqVO) { + return upgradeTaskMapper.selectUpgradeTaskPage(pageReqVO); + } + + @Override + public List getUpgradeTaskByState(Integer state) { + return upgradeTaskMapper.selectUpgradeTaskByState(state); + } + + @Override + public void updateUpgradeTaskStatus(Long id, Integer status) { + upgradeTaskMapper.updateById(IotOtaUpgradeTaskDO.builder().id(id).status(status).build()); + } + + // TODO @li:注释有点冗余 + /** + * 校验固件升级任务是否重复 + *

+ * 该方法用于检查给定固件ID和任务名称组合是否已存在于数据库中,如果存在则抛出异常, + * 表示任务名称对于该固件而言是重复的此检查确保用户不能创建具有相同名称的任务, + * 从而避免数据重复和混淆 + * + * @param firmwareId 固件的唯一标识符,用于区分不同的固件 + * @param taskName 升级任务的名称,用于与固件ID一起检查重复性 + * @throws cn.iocoder.yudao.framework.common.exception.ServerException 则抛出预定义的异常 + */ + private void validateFirmwareTaskDuplicate(Long firmwareId, String taskName) { + // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 + List upgradeTaskList = upgradeTaskMapper.selectByFirmwareIdAndName(firmwareId, taskName); + // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 + if (CollUtil.isNotEmpty(upgradeTaskList)) { + throw exception(OTA_UPGRADE_TASK_NAME_DUPLICATE); + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级任务的范围和设备列表的有效性。 + *

+ * 根据升级任务的范围(scope),验证设备列表(deviceIds)或产品ID(productId)是否有效。 + * 如果范围是“选择设备”(SELECT),则必须提供设备列表;如果范围是“所有设备”(ALL),则必须根据产品ID获取设备列表,并确保列表不为空。 + * + * @param scope 升级任务的范围,参考 IotOtaUpgradeTaskScopeEnum 枚举值 + * @param deviceIds 设备ID列表,当范围为“选择设备”时,该列表不能为空 + * @param productId 产品ID,当范围为“所有设备”时,用于获取设备列表 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException,抛出相应的异常 + */ + private void validateScopeAndDevice(Integer scope, List deviceIds, String productId) { + // TODO @li:if return + // 验证范围为“选择设备”时,设备列表不能为空 + if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.SELECT.getScope())) { + if (CollUtil.isEmpty(deviceIds)) { + throw exception(OTA_UPGRADE_TASK_DEVICE_IDS_EMPTY); + } + } else if (Objects.equals(scope, IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { + // 验证范围为“所有设备”时,根据产品ID获取的设备列表不能为空 + List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); + if (CollUtil.isEmpty(deviceList)) { + throw exception(OTA_UPGRADE_TASK_DEVICE_LIST_EMPTY); + } + } + } + + // TODO @li:注释有点冗余 + /** + * 验证升级任务是否存在 + *

+ * 通过查询数据库来验证给定ID的升级任务是否存在此方法主要用于确保后续操作所针对的升级任务是有效的 + * + * @param id 升级任务的唯一标识符如果为null或数据库中不存在对应的记录,则认为任务不存在 + * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 如果升级任务不存在,则抛出异常提示任务不存在 + */ + private IotOtaUpgradeTaskDO validateUpgradeTaskExists(Long id) { + // 查询数据库中是否有相同固件ID和任务名称的升级任务存在 + IotOtaUpgradeTaskDO upgradeTask = upgradeTaskMapper.selectById(id); + // 如果查询结果不为空,说明存在重复的任务名称,抛出异常 + if (Objects.isNull(upgradeTask)) { + throw exception(OTA_UPGRADE_TASK_NOT_EXISTS); + } + return upgradeTask; + } + + // TODO @li:注释有点冗余 + /** + * 初始化升级任务 + *

+ * 根据请求参数创建升级任务对象,并根据选择的范围初始化设备数量 + * 如果选择特定设备进行升级,则设备数量为所选设备的总数 + * 如果选择全部设备进行升级,则设备数量为该固件对应产品下的所有设备总数 + * + * @param createReqVO 升级任务保存请求对象,包含创建升级任务所需的信息 + * @return 返回初始化后的升级任务对象 + */ + // TODO @li:一次性的方法,不用特别抽小方法 + private IotOtaUpgradeTaskDO initOtaUpgradeTask(IotOtaUpgradeTaskSaveReqVO createReqVO, String productId) { + // 将请求参数转换为升级任务对象 + IotOtaUpgradeTaskDO upgradeTask = BeanUtils.toBean(createReqVO, IotOtaUpgradeTaskDO.class); + // 初始化的时候,设置设备数量和状态 + upgradeTask.setDeviceCount(Convert.toLong(CollUtil.size(createReqVO.getDeviceIds()))) + .setStatus(IotOtaUpgradeTaskStatusEnum.IN_PROGRESS.getStatus()); + // 如果选择全选,则需要查询设备数量 + if (Objects.equals(createReqVO.getScope(), IotOtaUpgradeTaskScopeEnum.ALL.getScope())) { + // 根据产品ID查询设备数量 + List deviceList = deviceService.getDeviceListByProductId(Convert.toLong(productId)); + // 设置升级任务的设备数量 + upgradeTask.setDeviceCount((long) deviceList.size()); + upgradeTask.setDeviceIds( + deviceList.stream().map(IotDeviceDO::getId).collect(Collectors.toList())); + } + // 返回初始化后的升级任务对象 + return upgradeTask; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java new file mode 100644 index 000000000..8b6610f15 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigService.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginDeployTypeEnum; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * IoT 插件配置 Service 接口 + * + * @author haohao + */ +public interface IotPluginConfigService { + + /** + * 创建插件配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createPluginConfig(@Valid PluginConfigSaveReqVO createReqVO); + + /** + * 更新插件配置 + * + * @param updateReqVO 更新信息 + */ + void updatePluginConfig(@Valid PluginConfigSaveReqVO updateReqVO); + + /** + * 删除插件配置 + * + * @param id 编号 + */ + void deletePluginConfig(Long id); + + /** + * 获得插件配置 + * + * @param id 编号 + * @return 插件配置 + */ + IotPluginConfigDO getPluginConfig(Long id); + + /** + * 获得插件配置分页 + * + * @param pageReqVO 分页查询 + * @return 插件配置分页 + */ + PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO); + + /** + * 上传插件的 JAR 包 + * + * @param id 插件id + * @param file 文件 + */ + void uploadFile(Long id, MultipartFile file); + + /** + * 更新插件的状态 + * + * @param id 插件id + * @param status 状态 {@link IotPluginStatusEnum} + */ + void updatePluginStatus(Long id, Integer status); + + /** + * 获得插件配置列表 + * + * @return 插件配置列表 + */ + List getPluginConfigList(); + + /** + * 根据状态和部署类型获得插件配置列表 + * + * @param status 状态 {@link IotPluginStatusEnum} + * @param deployType 部署类型 {@link IotPluginDeployTypeEnum} + * @return 插件配置列表 + */ + List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType); + + /** + * 根据插件包标识符获取插件配置 + * + * @param pluginKey 插件包标识符 + * @return 插件配置 + */ + IotPluginConfigDO getPluginConfigByPluginKey(@NotEmpty(message = "插件包标识符不能为空") String pluginKey); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java new file mode 100644 index 000000000..18376bc57 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginConfigServiceImpl.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.config.PluginConfigSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginConfigMapper; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; +/** + * IoT 插件配置 Service 实现类 + * + * @author haohao + */ +@Service +@Validated +@Slf4j +public class IotPluginConfigServiceImpl implements IotPluginConfigService { + + @Resource + private IotPluginConfigMapper pluginConfigMapper; + + @Resource + private IotPluginInstanceService pluginInstanceService; + + @Resource + private SpringPluginManager springPluginManager; + + @Override + public Long createPluginConfig(PluginConfigSaveReqVO createReqVO) { + // 1. 校验插件标识唯一性:确保没有其他配置使用相同的 pluginKey(新建时 id 为 null) + validatePluginKeyUnique(null, createReqVO.getPluginKey()); + IotPluginConfigDO pluginConfig = BeanUtils.toBean(createReqVO, IotPluginConfigDO.class); + // 2. 插入插件配置到数据库 + pluginConfigMapper.insert(pluginConfig); + return pluginConfig.getId(); + } + + @Override + public void updatePluginConfig(PluginConfigSaveReqVO updateReqVO) { + // 1. 校验插件配置是否存在:根据传入 ID 判断记录是否存在 + validatePluginConfigExists(updateReqVO.getId()); + // 2. 校验插件标识唯一性:确保更新后的 pluginKey 没有被其他记录占用 + validatePluginKeyUnique(updateReqVO.getId(), updateReqVO.getPluginKey()); + // 3. 将更新请求对象转换为插件配置数据对象 + IotPluginConfigDO updateObj = BeanUtils.toBean(updateReqVO, IotPluginConfigDO.class); + pluginConfigMapper.updateById(updateObj); + } + + /** + * 校验插件标识唯一性 + * + * @param id 当前插件配置的 ID(如果为 null 则说明为新建操作) + * @param pluginKey 待校验的插件标识 + */ + private void validatePluginKeyUnique(Long id, String pluginKey) { + // 1. 根据 pluginKey 从数据库中查询已有的插件配置 + IotPluginConfigDO pluginConfig = pluginConfigMapper.selectByPluginKey(pluginKey); + // 2. 如果查询到记录且记录的 ID 与当前 ID 不相同,则认为存在重复,抛出异常 + if (pluginConfig != null && !pluginConfig.getId().equals(id)) { + throw exception(PLUGIN_CONFIG_KEY_DUPLICATE); + } + } + + @Override + public void deletePluginConfig(Long id) { + // 1. 校验存在 + IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); + // 2. 未开启状态,才允许删除 + if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginConfigDO.getStatus())) { + throw exception(PLUGIN_CONFIG_DELETE_FAILED_RUNNING); + } + + // 3. 卸载插件 + pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); + // 4. 删除插件文件 + pluginInstanceService.deletePluginFile(pluginConfigDO); + + // 5. 删除插件配置 + pluginConfigMapper.deleteById(id); + } + + /** + * 校验插件配置是否存在 + * + * @param id 插件配置编号 + * @return 插件配置 + */ + private IotPluginConfigDO validatePluginConfigExists(Long id) { + IotPluginConfigDO pluginConfig = pluginConfigMapper.selectById(id); + if (pluginConfig == null) { + throw exception(PLUGIN_CONFIG_NOT_EXISTS); + } + return pluginConfig; + } + + @Override + public IotPluginConfigDO getPluginConfig(Long id) { + return pluginConfigMapper.selectById(id); + } + + @Override + public PageResult getPluginConfigPage(PluginConfigPageReqVO pageReqVO) { + return pluginConfigMapper.selectPage(pageReqVO); + } + + @Override + public void uploadFile(Long id, MultipartFile file) { + // 1. 校验插件配置是否存在 + IotPluginConfigDO pluginConfigDO = validatePluginConfigExists(id); + + // 2.1 停止并卸载旧的插件 + pluginInstanceService.stopAndUnloadPlugin(pluginConfigDO.getPluginKey()); + // 2.2 上传新的插件文件,更新插件启用状态文件 + String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file); + + // 3. 校验 file 相关参数,是否完整 + validatePluginConfigFile(pluginKeyNew); + + // 4. 更新插件配置 + IotPluginConfigDO updatedPluginConfig = new IotPluginConfigDO() + .setId(pluginConfigDO.getId()) + .setPluginKey(pluginKeyNew) + .setStatus(IotPluginStatusEnum.STOPPED.getStatus()) // TODO @haohao:这个状态,是不是非 stop 哈? + .setFileName(file.getOriginalFilename()) + .setScript("") // TODO @haohao:这个设置为 "" 会不会覆盖数据里的哈?应该从插件里读取?未来? + .setConfigSchema(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()) + .setVersion(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion()) + .setDescription(springPluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription()); + pluginConfigMapper.updateById(updatedPluginConfig); + } + + /** + * 校验 file 相关参数 + * + * @param pluginKeyNew 插件标识符 + */ + private void validatePluginConfigFile(String pluginKeyNew) { + // TODO @haohao:校验 file 相关参数,是否完整,类似:version 之类是不是可以解析到 + PluginWrapper plugin = springPluginManager.getPlugin(pluginKeyNew); + if (plugin == null) { + throw exception(PLUGIN_INSTALL_FAILED); + } + if (plugin.getDescriptor().getVersion() == null) { + throw exception(PLUGIN_INSTALL_FAILED); + } + } + + @Override + public void updatePluginStatus(Long id, Integer status) { + // 1. 校验插件配置是否存在 + IotPluginConfigDO pluginConfigDo = validatePluginConfigExists(id); + + // 2. 更新插件状态 + pluginInstanceService.updatePluginStatus(pluginConfigDo, status); + + // 3. 更新数据库中的插件状态 + pluginConfigMapper.updateById(new IotPluginConfigDO().setId(id).setStatus(status)); + } + + @Override + public List getPluginConfigList() { + return pluginConfigMapper.selectList(); + } + + @Override + public List getPluginConfigListByStatusAndDeployType(Integer status, Integer deployType) { + return pluginConfigMapper.selectListByStatusAndDeployType(status, deployType); + } + + @Override + public IotPluginConfigDO getPluginConfigByPluginKey(String pluginKey) { + return pluginConfigMapper.selectByPluginKey(pluginKey); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java new file mode 100644 index 000000000..56e1bf0f0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; + +/** + * IoT 插件实例 Service 接口 + * + * @author 芋道源码 + */ +public interface IotPluginInstanceService { + + /** + * 心跳插件实例 + * + * @param heartbeatReqDTO 心跳插件实例 DTO + */ + void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO); + + /** + * 离线超时插件实例 + * + * @param maxHeartbeatTime 最大心跳时间 + */ + int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime); + + /** + * 停止并卸载插件 + * + * @param pluginKey 插件标识符 + */ + void stopAndUnloadPlugin(String pluginKey); + + /** + * 删除插件文件 + * + * @param pluginConfigDO 插件配置 + */ + void deletePluginFile(IotPluginConfigDO pluginConfigDO); + + /** + * 上传并加载新的插件文件 + * + * @param file 插件文件 + * @return 插件标识符 + */ + String uploadAndLoadNewPlugin(MultipartFile file); + + /** + * 更新插件状态 + * + * @param pluginConfigDO 插件配置 + * @param status 新状态 + */ + void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status); + + // ========== 设备与插件的映射操作 ========== + + /** + * 更新设备对应的插件实例的进程编号 + * + * @param deviceKey 设备 Key + * @param processId 进程编号 + */ + void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId); + + /** + * 获得设备对应的插件实例 + * + * @param deviceKey 设备 Key + * @return 插件实例 + */ + IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java new file mode 100644 index 000000000..3c15ff774 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java @@ -0,0 +1,231 @@ +package cn.iocoder.yudao.module.iot.service.plugin; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginConfigDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper; +import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO; +import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginState; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPluginManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * IoT 插件实例 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private IotPluginConfigService pluginConfigService; + + @Resource + private IotPluginInstanceMapper pluginInstanceMapper; + + @Resource + private DevicePluginProcessIdRedisDAO devicePluginProcessIdRedisDAO; + + @Resource + private SpringPluginManager pluginManager; + + @Value("${pf4j.pluginsDir}") + private String pluginsDir; + + @Override + public void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + // 情况一:已存在,则进行更新 + IotPluginInstanceDO instance = TenantUtils.executeIgnore( + () -> pluginInstanceMapper.selectByProcessId(heartbeatReqDTO.getProcessId())); + if (instance != null) { + IotPluginInstanceDO.IotPluginInstanceDOBuilder updateObj = IotPluginInstanceDO.builder().id(instance.getId()) + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { + if (Boolean.FALSE.equals(instance.getOnline())) { // 当前处于离线时,才需要更新上线时间 + updateObj.onlineTime(LocalDateTime.now()); + } + } else { + updateObj.offlineTime(LocalDateTime.now()); + } + TenantUtils.execute(instance.getTenantId(), + () -> pluginInstanceMapper.updateById(updateObj.build())); + return; + } + + // 情况二:不存在,则创建 + IotPluginConfigDO info = TenantUtils.executeIgnore( + () -> pluginConfigService.getPluginConfigByPluginKey(heartbeatReqDTO.getPluginKey())); + if (info == null) { + log.error("[heartbeatPluginInstance][心跳({}) 对应的插件不存在]", heartbeatReqDTO); + return; + } + IotPluginInstanceDO.IotPluginInstanceDOBuilder insertObj = IotPluginInstanceDO.builder() + .pluginId(info.getId()).processId(heartbeatReqDTO.getProcessId()) + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort()) + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now()); + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) { + insertObj.onlineTime(LocalDateTime.now()); + } else { + insertObj.offlineTime(LocalDateTime.now()); + } + TenantUtils.execute(info.getTenantId(), + () -> pluginInstanceMapper.insert(insertObj.build())); + } + + @Override + public int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime) { + List list = pluginInstanceMapper.selectListByHeartbeatTimeLt(maxHeartbeatTime); + if (CollUtil.isEmpty(list)) { + return 0; + } + + // 更新插件实例为离线 + int count = 0; + for (IotPluginInstanceDO instance : list) { + pluginInstanceMapper.updateById(IotPluginInstanceDO.builder().id(instance.getId()) + .online(false).offlineTime(LocalDateTime.now()).build()); + count++; + } + return count; + } + + @Override + public void stopAndUnloadPlugin(String pluginKey) { + PluginWrapper plugin = pluginManager.getPlugin(pluginKey); + if (plugin == null) { + log.warn("插件不存在或已卸载: {}", pluginKey); + return; + } + if (plugin.getPluginState().equals(PluginState.STARTED)) { + pluginManager.stopPlugin(pluginKey); // 停止插件 + log.info("已停止插件: {}", pluginKey); + } + pluginManager.unloadPlugin(pluginKey); // 卸载插件 + log.info("已卸载插件: {}", pluginKey); + } + + @Override + public void deletePluginFile(IotPluginConfigDO pluginConfigDO) { + File file = new File(pluginsDir, pluginConfigDO.getFileName()); + if (!file.exists()) { + return; + } + try { + TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕 + if (!file.delete()) { + log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName()); + } + } catch (InterruptedException e) { + log.error("[deletePluginFile][删除插件文件({}) 失败]", pluginConfigDO.getFileName(), e); + } + } + + @Override + public String uploadAndLoadNewPlugin(MultipartFile file) { + String pluginKeyNew; + // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载 + Path pluginsPath = Paths.get(pluginsDir); + try { + FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录 + String filename = file.getOriginalFilename(); + if (filename != null) { + Path jarPath = pluginsPath.resolve(filename); + Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件 + pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件 + log.info("已加载插件: {}", pluginKeyNew); + } else { + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED); + } + } catch (IOException e) { + log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e); + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); + } catch (Exception e) { + log.error("[uploadAndLoadNewPlugin][加载插件失败]", e); + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e); + } + return pluginKeyNew; + } + + @Override + public void updatePluginStatus(IotPluginConfigDO pluginConfigDO, Integer status) { + String pluginKey = pluginConfigDO.getPluginKey(); + PluginWrapper plugin = pluginManager.getPlugin(pluginKey); + + if (plugin == null) { + // 插件不存在且状态为停止,抛出异常 + if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginConfigDO.getStatus())) { + throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID); + } + return; + } + + // 启动插件 + if (status.equals(IotPluginStatusEnum.RUNNING.getStatus()) + && plugin.getPluginState() != PluginState.STARTED) { + try { + pluginManager.startPlugin(pluginKey); + } catch (Exception e) { + log.error("[updatePluginStatus][启动插件({}) 失败]", pluginKey, e); + throw exception(ErrorCodeConstants.PLUGIN_START_FAILED, e); + } + log.info("已启动插件: {}", pluginKey); + } + // 停止插件 + else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus()) + && plugin.getPluginState() == PluginState.STARTED) { + try { + pluginManager.stopPlugin(pluginKey); + } catch (Exception e) { + log.error("[updatePluginStatus][停止插件({}) 失败]", pluginKey, e); + throw exception(ErrorCodeConstants.PLUGIN_STOP_FAILED, e); + } + log.info("已停止插件: {}", pluginKey); + } + } + + // ========== 设备与插件的映射操作 ========== + + @Override + public void updateDevicePluginInstanceProcessIdAsync(String deviceKey, String processId) { + devicePluginProcessIdRedisDAO.put(deviceKey, processId); + } + + @Override + public IotPluginInstanceDO getPluginInstanceByDeviceKey(String deviceKey) { + String processId = devicePluginProcessIdRedisDAO.get(deviceKey); + if (StrUtil.isEmpty(processId)) { + return null; + } + return pluginInstanceMapper.selectByProcessId(processId); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java new file mode 100644 index 000000000..8cc640556 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryService.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.module.iot.service.product; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import jakarta.validation.Valid; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * IoT 产品分类 Service 接口 + * + * @author 芋道源码 + */ +public interface IotProductCategoryService { + + /** + * 创建产品分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProductCategory(@Valid IotProductCategorySaveReqVO createReqVO); + + /** + * 更新产品分类 + * + * @param updateReqVO 更新信息 + */ + void updateProductCategory(@Valid IotProductCategorySaveReqVO updateReqVO); + + /** + * 删除产品分类 + * + * @param id 编号 + */ + void deleteProductCategory(Long id); + + /** + * 获得产品分类 + * + * @param id 编号 + * @return 产品分类 + */ + IotProductCategoryDO getProductCategory(Long id); + + /** + * 获得产品分类列表 + * + * @param ids 编号 + * @return 产品分类列表 + */ + List getProductCategoryList(Collection ids); + + /** + * 获得产品分类 Map + * + * @param ids 编号 + * @return 产品分类 Map + */ + default Map getProductCategoryMap(Collection ids) { + return convertMap(getProductCategoryList(ids), IotProductCategoryDO::getId); + } + + /** + * 获得产品分类分页 + * + * @param pageReqVO 分页查询 + * @return 产品分类分页 + */ + PageResult getProductCategoryPage(IotProductCategoryPageReqVO pageReqVO); + + /** + * 获得产品分类列表,根据状态 + * + * @param status 状态 + * @return 产品分类列表 + */ + List getProductCategoryListByStatus(Integer status); + + /** + * 获得产品分类数量 + * + * @param createTime 创建时间,如果为空,则统计所有分类数量 + * @return 产品分类数量 + */ + Long getProductCategoryCount(@Nullable LocalDateTime createTime); + + /** + * 获得各个品类下设备数量统计,其中 key 是产品分类名 + * + * @return 品类设备统计列表 + */ + Map getProductCategoryDeviceCountMap(); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java new file mode 100644 index 000000000..13a8d488e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductCategoryServiceImpl.java @@ -0,0 +1,123 @@ +package cn.iocoder.yudao.module.iot.service.product; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategoryPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.category.IotProductCategorySaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductCategoryDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductCategoryMapper; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_CATEGORY_NOT_EXISTS; + +/** + * IoT 产品分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class IotProductCategoryServiceImpl implements IotProductCategoryService { + + @Resource + private IotProductCategoryMapper iotProductCategoryMapper; + + @Resource + private IotProductService productService; + + @Resource + private IotDeviceService deviceService; + + public Long createProductCategory(IotProductCategorySaveReqVO createReqVO) { + // 插入 + IotProductCategoryDO productCategory = BeanUtils.toBean(createReqVO, IotProductCategoryDO.class); + iotProductCategoryMapper.insert(productCategory); + // 返回 + return productCategory.getId(); + } + + @Override + public void updateProductCategory(IotProductCategorySaveReqVO updateReqVO) { + // 校验存在 + validateProductCategoryExists(updateReqVO.getId()); + // 更新 + IotProductCategoryDO updateObj = BeanUtils.toBean(updateReqVO, IotProductCategoryDO.class); + iotProductCategoryMapper.updateById(updateObj); + } + + @Override + public void deleteProductCategory(Long id) { + // 校验存在 + validateProductCategoryExists(id); + // 删除 + iotProductCategoryMapper.deleteById(id); + } + + private void validateProductCategoryExists(Long id) { + if (iotProductCategoryMapper.selectById(id) == null) { + throw exception(PRODUCT_CATEGORY_NOT_EXISTS); + } + } + + @Override + public IotProductCategoryDO getProductCategory(Long id) { + return iotProductCategoryMapper.selectById(id); + } + + @Override + public List getProductCategoryList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return CollUtil.newArrayList(); + } + return iotProductCategoryMapper.selectBatchIds(ids); + } + + @Override + public PageResult getProductCategoryPage(IotProductCategoryPageReqVO pageReqVO) { + return iotProductCategoryMapper.selectPage(pageReqVO); + } + + @Override + public List getProductCategoryListByStatus(Integer status) { + return iotProductCategoryMapper.selectListByStatus(status); + } + + @Override + public Long getProductCategoryCount(LocalDateTime createTime) { + return iotProductCategoryMapper.selectCountByCreateTime(createTime); + } + + @Override + public Map getProductCategoryDeviceCountMap() { + // 1. 获取所有数据 + List categoryList = iotProductCategoryMapper.selectList(); + List productList = productService.getProductList(); + // TODO @super:不要 list 查询,返回内存,而是查询一个 Map + Map deviceCountMapByProductId = deviceService.getDeviceCountMapByProductId(); + + // 2. 统计每个分类下的设备数量 + Map categoryDeviceCountMap = new HashMap<>(); + for (IotProductCategoryDO category : categoryList) { + categoryDeviceCountMap.put(category.getName(), 0); + // TODO @super:CollectionUtils.getSumValue(),看看能不能简化下 + // 2.2 找到该分类下的所有产品,累加设备数量 + for (IotProductDO product : productList) { + if (Objects.equals(product.getCategoryId(), category.getId())) { + Integer deviceCount = deviceCountMapByProductId.getOrDefault(product.getId(), 0); + categoryDeviceCountMap.merge(category.getName(), deviceCount, Integer::sum); + } + } + } + return categoryDeviceCountMap; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java index 3fff94fd9..8497d73aa 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.module.iot.service.product; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import jakarta.validation.Valid; +import javax.annotation.Nullable; +import java.time.LocalDateTime; import java.util.List; /** @@ -45,6 +47,30 @@ public interface IotProductService { */ IotProductDO getProduct(Long id); + /** + * 根据产品 key 获得产品 + * + * @param productKey 产品 key + * @return 产品 + */ + IotProductDO getProductByProductKey(String productKey); + + /** + * 校验产品存在 + * + * @param id 编号 + * @return 产品 + */ + IotProductDO validateProductExists(Long id); + + /** + * 校验产品存在 + * + * @param productKey 产品 key + * @return 产品 + */ + IotProductDO validateProductExists(String productKey); + /** * 获得产品分页 * @@ -68,4 +94,13 @@ public interface IotProductService { */ List getProductList(); + /** + * 获得产品数量 + * + * @param createTime 创建时间,如果为空,则统计所有产品数量 + * @return 产品数量 + */ + Long getProductCount(@Nullable LocalDateTime createTime); + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java index 15391f70b..4a7263c27 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java @@ -1,20 +1,23 @@ package cn.iocoder.yudao.module.iot.service.product; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.product.vo.IotProductSaveReqVO; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.service.device.data.IotDevicePropertyService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.time.LocalDateTime; import java.util.List; import java.util.Objects; -import java.util.UUID; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; @@ -31,34 +34,27 @@ public class IotProductServiceImpl implements IotProductService { @Resource private IotProductMapper productMapper; + @Resource + @Lazy // 延迟加载,解决循环依赖 + private IotDevicePropertyService devicePropertyDataService; + @Override public Long createProduct(IotProductSaveReqVO createReqVO) { - // 1. 生成 ProductKey - createProductKey(createReqVO); + // 1. 校验 ProductKey + TenantUtils.executeIgnore(() -> { + // 为什么忽略租户?避免多个租户之间,productKey 重复,导致 TDengine 设备属性表重复 + if (productMapper.selectByProductKey(createReqVO.getProductKey()) != null) { + throw exception(PRODUCT_KEY_EXISTS); + } + }); + // 2. 插入 - IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class); + IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); productMapper.insert(product); return product.getId(); } - /** - * 创建 ProductKey - * - * @param createReqVO 创建信息 - */ - private void createProductKey(IotProductSaveReqVO createReqVO) { - String productKey = createReqVO.getProductKey(); - // 1. productKey为空,生成随机的 11 位字符串 - if (StrUtil.isEmpty(productKey)) { - productKey = UUID.randomUUID().toString().replace("-", "").substring(0, 11); - } - // 2. 校验唯一性 - if (productMapper.selectByProductKey(productKey) != null) { - throw exception(PRODUCT_IDENTIFICATION_EXISTS); - } - createReqVO.setProductKey(productKey); - } - @Override public void updateProduct(IotProductSaveReqVO updateReqVO) { updateReqVO.setProductKey(null); // 不更新产品标识 @@ -81,16 +77,26 @@ public class IotProductServiceImpl implements IotProductService { productMapper.deleteById(id); } - private IotProductDO validateProductExists(Long id) { - IotProductDO iotProductDO = productMapper.selectById(id); - if (iotProductDO == null) { + @Override + public IotProductDO validateProductExists(Long id) { + IotProductDO product = productMapper.selectById(id); + if (product == null) { throw exception(PRODUCT_NOT_EXISTS); } - return iotProductDO; + return product; } - private void validateProductStatus(IotProductDO iotProductDO) { - if (Objects.equals(iotProductDO.getStatus(), IotProductStatusEnum.PUBLISHED.getType())) { + @Override + public IotProductDO validateProductExists(String productKey) { + IotProductDO product = productMapper.selectByProductKey(productKey); + if (product == null) { + throw exception(PRODUCT_NOT_EXISTS); + } + return product; + } + + private void validateProductStatus(IotProductDO product) { + if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { throw exception(PRODUCT_STATUS_NOT_DELETE); } } @@ -100,16 +106,29 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectById(id); } + @Override + public IotProductDO getProductByProductKey(String productKey) { + return productMapper.selectByProductKey(productKey); + } + @Override public PageResult getProductPage(IotProductPageReqVO pageReqVO) { return productMapper.selectPage(pageReqVO); } @Override + @DSTransactional(rollbackFor = Exception.class) public void updateProductStatus(Long id, Integer status) { // 1. 校验存在 validateProductExists(id); - // 2. 更新 + + // 2. 更新为发布状态,需要创建产品超级表数据模型 + // TODO @芋艿:【待定 001】1)是否需要操作后,在 redis 进行缓存,实现一个“快照”的情况,类似 tl; + if (Objects.equals(status, IotProductStatusEnum.PUBLISHED.getStatus())) { + devicePropertyDataService.defineDevicePropertyData(id); + } + + // 3. 更新 IotProductDO updateObj = IotProductDO.builder().id(id).status(status).build(); productMapper.updateById(updateObj); } @@ -119,4 +138,10 @@ public class IotProductServiceImpl implements IotProductService { return productMapper.selectList(); } + @Override + public Long getProductCount(LocalDateTime createTime) { + return productMapper.selectCountByCreateTime(createTime); + } + + } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java new file mode 100644 index 000000000..18069376b --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import jakarta.validation.Valid; + +/** + * IoT 数据桥梁 Service 接口 + * + * @author HUIHUI + */ +public interface IotDataBridgeService { + + /** + * 创建数据桥梁 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataBridge(@Valid IotDataBridgeSaveReqVO createReqVO); + + /** + * 更新数据桥梁 + * + * @param updateReqVO 更新信息 + */ + void updateDataBridge(@Valid IotDataBridgeSaveReqVO updateReqVO); + + /** + * 删除数据桥梁 + * + * @param id 编号 + */ + void deleteDataBridge(Long id); + + /** + * 获得数据桥梁 + * + * @param id 编号 + * @return 数据桥梁 + */ + IotDataBridgeDO getDataBridge(Long id); + + /** + * 获得数据桥梁分页 + * + * @param pageReqVO 分页查询 + * @return 数据桥梁分页 + */ + PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java new file mode 100644 index 000000000..9e439fc99 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotDataBridgeServiceImpl.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgePageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.IotDataBridgeSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataBridgeMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_BRIDGE_NOT_EXISTS; + +/** + * IoT 数据桥梁 Service 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class IotDataBridgeServiceImpl implements IotDataBridgeService { + + @Resource + private IotDataBridgeMapper dataBridgeMapper; + + @Override + public Long createDataBridge(IotDataBridgeSaveReqVO createReqVO) { + // 插入 + IotDataBridgeDO dataBridge = BeanUtils.toBean(createReqVO, IotDataBridgeDO.class); + dataBridgeMapper.insert(dataBridge); + // 返回 + return dataBridge.getId(); + } + + @Override + public void updateDataBridge(IotDataBridgeSaveReqVO updateReqVO) { + // 校验存在 + validateDataBridgeExists(updateReqVO.getId()); + // 更新 + IotDataBridgeDO updateObj = BeanUtils.toBean(updateReqVO, IotDataBridgeDO.class); + dataBridgeMapper.updateById(updateObj); + } + + @Override + public void deleteDataBridge(Long id) { + // 校验存在 + validateDataBridgeExists(id); + // 删除 + dataBridgeMapper.deleteById(id); + } + + private void validateDataBridgeExists(Long id) { + if (dataBridgeMapper.selectById(id) == null) { + throw exception(DATA_BRIDGE_NOT_EXISTS); + } + } + + @Override + public IotDataBridgeDO getDataBridge(Long id) { + return dataBridgeMapper.selectById(id); + } + + @Override + public PageResult getDataBridgePage(IotDataBridgePageReqVO pageReqVO) { + return dataBridgeMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java new file mode 100644 index 000000000..6927b1172 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import java.util.List; + +/** + * IoT 规则场景 Service 接口 + * + * @author 芋道源码 + */ +public interface IotRuleSceneService { + + /** + * 【缓存】获得指定设备的场景列表 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @return 场景列表 + */ + List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName); + + /** + * 基于 {@link IotRuleSceneTriggerTypeEnum#DEVICE} 场景,执行规则场景 + * + * @param message 消息 + */ + void executeRuleSceneByDevice(IotDeviceMessage message); + + /** + * 基于 {@link IotRuleSceneTriggerTypeEnum#TIMER} 场景,执行规则场景 + * + * @param id 场景编号 + */ + void executeRuleSceneByTimer(Long id); + + /** + * TODO 芋艿:测试方法,需要删除 + */ + void test(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java new file mode 100644 index 000000000..2219d4bad --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java @@ -0,0 +1,438 @@ +package cn.iocoder.yudao.module.iot.service.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotRuleSceneMapper; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import cn.iocoder.yudao.module.iot.job.rule.IotRuleSceneJob; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.action.IotRuleSceneAction; +import jakarta.annotation.Resource; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerKey; +import org.quartz.impl.StdSchedulerFactory; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; + +/** + * IoT 规则场景 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotRuleSceneServiceImpl implements IotRuleSceneService { + + @Resource + private IotRuleSceneMapper ruleSceneMapper; + + @Resource + private List ruleSceneActions; + + @Resource(name = "iotSchedulerManager") + private IotSchedulerManager schedulerManager; + + // TODO 芋艿,缓存待实现 + @Override + @TenantIgnore // 忽略租户隔离:因为 IotRuleSceneMessageHandler 调用时,一般未传递租户,所以需要忽略 + public List getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) { + if (true) { + IotRuleSceneDO ruleScene01 = new IotRuleSceneDO(); + ruleScene01.setTriggers(CollUtil.newArrayList()); + IotRuleSceneDO.TriggerConfig trigger01 = new IotRuleSceneDO.TriggerConfig(); + trigger01.setType(IotRuleSceneTriggerTypeEnum.DEVICE.getType()); + trigger01.setConditions(CollUtil.newArrayList()); + // 属性 + IotRuleSceneDO.TriggerCondition condition01 = new IotRuleSceneDO.TriggerCondition(); + condition01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + condition01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()); + condition01.setParameters(CollUtil.newArrayList()); +// IotRuleSceneDO.TriggerConditionParameter parameter010 = new IotRuleSceneDO.TriggerConditionParameter(); +// parameter010.setIdentifier("width"); +// parameter010.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); +// parameter010.setValue("abc"); +// condition01.getParameters().add(parameter010); + IotRuleSceneDO.TriggerConditionParameter parameter011 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter011.setIdentifier("width"); + parameter011.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); + parameter011.setValue("1"); + condition01.getParameters().add(parameter011); + IotRuleSceneDO.TriggerConditionParameter parameter012 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter012.setIdentifier("width"); + parameter012.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.getOperator()); + parameter012.setValue("2"); + condition01.getParameters().add(parameter012); + IotRuleSceneDO.TriggerConditionParameter parameter013 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter013.setIdentifier("width"); + parameter013.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.getOperator()); + parameter013.setValue("0"); + condition01.getParameters().add(parameter013); + IotRuleSceneDO.TriggerConditionParameter parameter014 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter014.setIdentifier("width"); + parameter014.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator()); + parameter014.setValue("0"); + condition01.getParameters().add(parameter014); + IotRuleSceneDO.TriggerConditionParameter parameter015 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter015.setIdentifier("width"); + parameter015.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.getOperator()); + parameter015.setValue("2"); + condition01.getParameters().add(parameter015); + IotRuleSceneDO.TriggerConditionParameter parameter016 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter016.setIdentifier("width"); + parameter016.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.getOperator()); + parameter016.setValue("2"); + condition01.getParameters().add(parameter016); + IotRuleSceneDO.TriggerConditionParameter parameter017 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter017.setIdentifier("width"); + parameter017.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.IN.getOperator()); + parameter017.setValue("1,2,3"); + condition01.getParameters().add(parameter017); + IotRuleSceneDO.TriggerConditionParameter parameter018 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter018.setIdentifier("width"); + parameter018.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.getOperator()); + parameter018.setValue("0,2,3"); + condition01.getParameters().add(parameter018); + IotRuleSceneDO.TriggerConditionParameter parameter019 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter019.setIdentifier("width"); + parameter019.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.getOperator()); + parameter019.setValue("1,3"); + condition01.getParameters().add(parameter019); + IotRuleSceneDO.TriggerConditionParameter parameter020 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter020.setIdentifier("width"); + parameter020.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.getOperator()); + parameter020.setValue("2,3"); + condition01.getParameters().add(parameter020); + trigger01.getConditions().add(condition01); + // 状态 + IotRuleSceneDO.TriggerCondition condition02 = new IotRuleSceneDO.TriggerCondition(); + condition02.setType(IotDeviceMessageTypeEnum.STATE.getType()); + condition02.setIdentifier(IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier()); + condition02.setParameters(CollUtil.newArrayList()); + trigger01.getConditions().add(condition02); + // 事件 + IotRuleSceneDO.TriggerCondition condition03 = new IotRuleSceneDO.TriggerCondition(); + condition03.setType(IotDeviceMessageTypeEnum.EVENT.getType()); + condition03.setIdentifier("xxx"); + condition03.setParameters(CollUtil.newArrayList()); + IotRuleSceneDO.TriggerConditionParameter parameter030 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter030.setIdentifier("width"); + parameter030.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); + parameter030.setValue("1"); + trigger01.getConditions().add(condition03); + ruleScene01.getTriggers().add(trigger01); + // 动作 + ruleScene01.setActions(CollUtil.newArrayList()); + // 设备控制 + IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); + action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); + IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); + actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); + actionDeviceControl01.setDeviceNames(ListUtil.of("small")); + actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); + actionDeviceControl01.setData(MapUtil.builder() + .put("power", 1) + .put("color", "red") + .build()); + action01.setDeviceControl(actionDeviceControl01); +// ruleScene01.getActions().add(action01); // TODO 芋艿:先不测试了 + // 数据桥接(http) + IotRuleSceneDO.ActionConfig action02 = new IotRuleSceneDO.ActionConfig(); + action02.setType(IotRuleSceneActionTypeEnum.DATA_BRIDGE.getType()); + action02.setDataBridgeId(1L); + ruleScene01.getActions().add(action02); + return ListUtil.toList(ruleScene01); + } + + List list = ruleSceneMapper.selectList(); + // TODO @芋艿:需要考虑开启状态 + return filterList(list, ruleScene -> { + for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { + if (ObjUtil.notEqual(trigger.getProductKey(), productKey)) { + continue; + } + if (CollUtil.isEmpty(trigger.getDeviceNames()) // 无设备名称限制 + || trigger.getDeviceNames().contains(deviceName)) { // 包含设备名称 + return true; + } + } + return false; + }); + } + + @Override + public void executeRuleSceneByDevice(IotDeviceMessage message) { + TenantUtils.execute(message.getTenantId(), () -> { + // 1. 获得设备匹配的规则场景 + List ruleScenes = getMatchedRuleSceneListByMessage(message); + if (CollUtil.isEmpty(ruleScenes)) { + return; + } + + // 2. 执行规则场景 + executeRuleSceneAction(message, ruleScenes); + }); + } + + @Override + public void executeRuleSceneByTimer(Long id) { + // 1.1 获得规则场景 +// IotRuleSceneDO scene = TenantUtils.executeIgnore(() -> ruleSceneMapper.selectById(id)); + // TODO @芋艿:这里,临时测试,后续删除。 + IotRuleSceneDO scene = new IotRuleSceneDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + if (true) { + scene.setTenantId(1L); + IotRuleSceneDO.TriggerConfig triggerConfig = new IotRuleSceneDO.TriggerConfig(); + triggerConfig.setType(IotRuleSceneTriggerTypeEnum.TIMER.getType()); + scene.setTriggers(ListUtil.toList(triggerConfig)); + // 动作 + IotRuleSceneDO.ActionConfig action01 = new IotRuleSceneDO.ActionConfig(); + action01.setType(IotRuleSceneActionTypeEnum.DEVICE_CONTROL.getType()); + IotRuleSceneDO.ActionDeviceControl actionDeviceControl01 = new IotRuleSceneDO.ActionDeviceControl(); + actionDeviceControl01.setProductKey("4aymZgOTOOCrDKRT"); + actionDeviceControl01.setDeviceNames(ListUtil.of("small")); + actionDeviceControl01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); + actionDeviceControl01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()); + actionDeviceControl01.setData(MapUtil.builder() + .put("power", 1) + .put("color", "red") + .build()); + action01.setDeviceControl(actionDeviceControl01); + scene.setActions(ListUtil.toList(action01)); + } + if (scene == null) { + log.error("[executeRuleSceneByTimer][规则场景({}) 不存在]", id); + return; + } + if (CommonStatusEnum.isDisable(scene.getStatus())) { + log.info("[executeRuleSceneByTimer][规则场景({}) 已被禁用]", id); + return; + } + // 1.2 判断是否有定时触发器,避免脏数据 + IotRuleSceneDO.TriggerConfig config = CollUtil.findOne(scene.getTriggers(), + trigger -> ObjUtil.equals(trigger.getType(), IotRuleSceneTriggerTypeEnum.TIMER.getType())); + if (config == null) { + log.error("[executeRuleSceneByTimer][规则场景({}) 不存在定时触发器]", scene); + return; + } + + // 2. 执行规则场景 + TenantUtils.execute(scene.getTenantId(), + () -> executeRuleSceneAction(null, ListUtil.toList(scene))); + } + + /** + * 基于消息,获得匹配的规则场景列表 + * + * @param message 设备消息 + * @return 规则场景列表 + */ + private List getMatchedRuleSceneListByMessage(IotDeviceMessage message) { + // 1. 匹配设备 + // TODO @芋艿:可能需要 getSelf(); 缓存 + List ruleScenes = getRuleSceneListByProductKeyAndDeviceNameFromCache( + message.getProductKey(), message.getDeviceName()); + if (CollUtil.isEmpty(ruleScenes)) { + return ruleScenes; + } + + // 2. 匹配 trigger 触发器的条件 + return filterList(ruleScenes, ruleScene -> { + for (IotRuleSceneDO.TriggerConfig trigger : ruleScene.getTriggers()) { + // 2.1 非设备触发,不匹配 + if (ObjUtil.notEqual(trigger.getType(), IotRuleSceneTriggerTypeEnum.DEVICE.getType())) { + return false; + } + // TODO 芋艿:产品、设备的匹配,要不要这里在做一次???貌似和 1. 部分重复了 + // 2.2 条件为空,说明没有匹配的条件,因此不匹配 + if (CollUtil.isEmpty(trigger.getConditions())) { + return false; + } + // 2.3 多个条件,只需要满足一个即可 + IotRuleSceneDO.TriggerCondition matchedCondition = CollUtil.findOne(trigger.getConditions(), condition -> { + if (ObjUtil.notEqual(message.getType(), condition.getType()) + || ObjUtil.notEqual(message.getIdentifier(), condition.getIdentifier())) { + return false; + } + // 多个条件参数,必须全部满足。所以,下面的逻辑就是找到一个不满足的条件参数 + IotRuleSceneDO.TriggerConditionParameter notMatchedParameter = CollUtil.findOne(condition.getParameters(), + parameter -> !isTriggerConditionParameterMatched(message, parameter, ruleScene, trigger)); + return notMatchedParameter == null; + }); + if (matchedCondition == null) { + return false; + } + log.info("[getMatchedRuleSceneList][消息({}) 匹配到规则场景编号({}) 的触发器({})]", message, ruleScene.getId(), trigger); + return true; + } + return false; + }); + } + + // TODO @芋艿:【可优化】可以考虑增加下单测,边界太多了。 + /** + * 判断触发器的条件参数是否匹配 + * + * @param message 设备消息 + * @param parameter 触发器条件参数 + * @param ruleScene 规则场景(用于日志,无其它作用) + * @param trigger 触发器(用于日志,无其它作用) + * @return 是否匹配 + */ + @SuppressWarnings({"unchecked", "DataFlowIssue"}) + private boolean isTriggerConditionParameterMatched(IotDeviceMessage message, IotRuleSceneDO.TriggerConditionParameter parameter, + IotRuleSceneDO ruleScene, IotRuleSceneDO.TriggerConfig trigger) { + // 1.1 校验操作符是否合法 + IotRuleSceneTriggerConditionParameterOperatorEnum operator = + IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator()); + if (operator == null) { + log.error("[isTriggerConditionParameterMatched][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", + ruleScene.getId(), trigger, parameter.getOperator()); + return false; + } + // 1.2 校验消息是否包含对应的值 + String messageValue = MapUtil.getStr((Map) message.getData(), parameter.getIdentifier()); + if (messageValue == null) { + return false; + } + + // 2.1 构建 Spring 表达式的变量 + Map springExpressionVariables = new HashMap<>(); + try { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, messageValue); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, parameter.getValue()); + List parameterValues = StrUtil.splitTrim(parameter.getValue(), CharPool.COMMA); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, parameterValues); + // 特殊:解决数字的比较。因为 Spring 是基于它的 compareTo 方法,对数字的比较存在问题! + if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN, + IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN, + IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS) + && NumberUtil.isNumber(messageValue) + && NumberUtils.isAllNumber(parameterValues)) { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(messageValue)); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(parameter.getValue())); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, + convertList(parameterValues, NumberUtil::parseDouble)); + } + // 2.2 计算 Spring 表达式 + return (Boolean) SpringExpressionUtils.parseExpression(operator.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[isTriggerConditionParameterMatched][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}/{}) 计算异常]", + message, ruleScene.getId(), trigger, operator, springExpressionVariables, e); + return false; + } + } + + /** + * 执行规则场景的动作 + * + * @param message 设备消息 + * @param ruleScenes 规则场景列表 + */ + private void executeRuleSceneAction(IotDeviceMessage message, List ruleScenes) { + // 1. 遍历规则场景 + ruleScenes.forEach(ruleScene -> { + // 2. 遍历规则场景的动作 + ruleScene.getActions().forEach(actionConfig -> { + // 3.1 获取对应的动作 Action 数组 + List actions = filterList(ruleSceneActions, + action -> action.getType().getType().equals(actionConfig.getType())); + if (CollUtil.isEmpty(actions)) { + return; + } + // 3.2 执行动作 + actions.forEach(action -> { + try { + action.execute(message, actionConfig); + log.info("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 成功]", + message, ruleScene.getId(), actionConfig); + } catch (Exception e) { + log.error("[executeRuleSceneAction][消息({}) 规则场景编号({}) 的执行动作({}) 执行异常]", + message, ruleScene.getId(), actionConfig, e); + } + }); + }); + }); + } + + @Override + @SneakyThrows + public void test() { + // TODO @芋艿:测试思路代码,记得删除!!! + // 1. Job 类:IotRuleSceneJob DONE + // 2. 参数:id DONE + // 3. jobHandlerName:IotRuleSceneJob + id DONE + + // 新增:addJob + // 修改:不存在 addJob、存在 updateJob + // 有 + 禁用:1)存在、停止;2)不存在:不处理;TODO 测试:直接暂停,是否可以???(结论:可以)pauseJob + // 有 + 开启:1)存在,更新;2)不存在,新增;结论:使用 save(addOrUpdateJob) + // 无 + 禁用、开启:1)存在,删除;TODO 测试:直接删除???(结论:可以)deleteJob + + // + if (false) { + Long id = 1L; + Map jobDataMap = IotRuleSceneJob.buildJobDataMap(id); + schedulerManager.addOrUpdateJob(IotRuleSceneJob.class, + IotRuleSceneJob.buildJobName(id), + "0/10 * * * * ?", + jobDataMap); + } + if (false) { + Long id = 1L; + schedulerManager.pauseJob(IotRuleSceneJob.buildJobName(id)); + } + if (true) { + Long id = 1L; + schedulerManager.deleteJob(IotRuleSceneJob.buildJobName(id)); + } + } + + public static void main2(String[] args) throws SchedulerException { +// System.out.println(QuartzJobBean.class); + Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); + scheduler.start(); + + String jobHandlerName = "123"; + // 暂停 Trigger 对象 + scheduler.pauseTrigger(new TriggerKey(jobHandlerName)); + // 取消并删除 Job 调度 + scheduler.unscheduleJob(new TriggerKey(jobHandlerName)); + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java new file mode 100644 index 000000000..c7b921c04 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAction.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + +import javax.annotation.Nullable; + +/** + * IoT 规则场景的场景执行器接口 + * + * @author 芋道源码 + */ +public interface IotRuleSceneAction { + + // TODO @芋艿:groovy 或者 javascript 实现数据的转换;可以考虑基于 hutool 的 ScriptUtil 做 + /** + * 执行场景 + * + * @param message 消息,允许空 + * 1. 空的情况:定时触发 + * 2. 非空的情况:设备触发 + * @param config 配置 + */ + void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception; + + /** + * 获得类型 + * + * @return 类型 + */ + IotRuleSceneActionTypeEnum getType(); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java new file mode 100644 index 000000000..eadc17378 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneAlertAction.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; + +/** + * IoT 告警的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +public class IotRuleSceneAlertAction implements IotRuleSceneAction { + + @Override + public void execute(@Nullable IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { + // TODO @芋艿:待实现 + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.ALERT; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java new file mode 100644 index 000000000..b38e181f9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDataBridgeAction.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.rule.IotDataBridgeService; +import cn.iocoder.yudao.module.iot.service.rule.action.databridge.IotDataBridgeExecute; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * IoT 数据桥梁的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneDataBridgeAction implements IotRuleSceneAction { + + @Resource + private IotDataBridgeService dataBridgeService; + @Resource + private List> dataBridgeExecutes; + + @Override + public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) throws Exception { + // 1.1 如果消息为空,直接返回 + if (message == null) { + return; + } + // 1.2 获得数据桥梁 + Assert.notNull(config.getDataBridgeId(), "数据桥梁编号不能为空"); + IotDataBridgeDO dataBridge = dataBridgeService.getDataBridge(config.getDataBridgeId()); + if (dataBridge == null || dataBridge.getConfig() == null) { + log.error("[execute][message({}) config({}) 对应的数据桥梁不存在]", message, config); + return; + } + if (CommonStatusEnum.isDisable(dataBridge.getStatus())) { + log.info("[execute][message({}) config({}) 对应的数据桥梁({}) 状态为禁用]", message, config, dataBridge); + return; + } + + // 2. 执行数据桥接操作 + for (IotDataBridgeExecute execute : dataBridgeExecutes) { + execute.execute(message, dataBridge); + } + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.DATA_BRIDGE; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java new file mode 100644 index 000000000..d8fd76b5e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/IotRuleSceneDeviceControlAction.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.iot.service.rule.action; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceDownstreamReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneActionTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceDownstreamService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * IoT 设备控制的 {@link IotRuleSceneAction} 实现类 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class IotRuleSceneDeviceControlAction implements IotRuleSceneAction { + + @Resource + private IotDeviceDownstreamService deviceDownstreamService; + @Resource + private IotDeviceService deviceService; + + @Override + public void execute(IotDeviceMessage message, IotRuleSceneDO.ActionConfig config) { + IotRuleSceneDO.ActionDeviceControl control = config.getDeviceControl(); + Assert.notNull(control, "设备控制配置不能为空"); + // 遍历每个设备,下发消息 + control.getDeviceNames().forEach(deviceName -> { + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceNameFromCache(control.getProductKey(), deviceName); + if (device == null) { + log.error("[execute][message({}) config({}) 对应的设备不存在]", message, config); + return; + } + try { + IotDeviceMessage downstreamMessage = deviceDownstreamService.downstreamDevice(new IotDeviceDownstreamReqVO() + .setId(device.getId()).setType(control.getType()).setIdentifier(control.getIdentifier()) + .setData(control.getData())); + log.info("[execute][message({}) config({}) 下发消息({})成功]", message, config, downstreamMessage); + } catch (Exception e) { + log.error("[execute][message({}) config({}) 下发消息失败]", message, config, e); + } + }); + } + + @Override + public IotRuleSceneActionTypeEnum getType() { + return IotRuleSceneActionTypeEnum.DEVICE_CONTROL; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java new file mode 100644 index 000000000..e7f84dd6c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/AbstractCacheableDataBridgeExecute.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.cache.RemovalListener; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; + +// TODO @芋艿:数据库 +// TODO @芋艿:mqtt +// TODO @芋艿:tcp +// TODO @芋艿:websocket + +/** + * 带缓存功能的数据桥梁执行器抽象类 + * + * 该类提供了一个通用的缓存机制,用于管理各类数据桥接的生产者(Producer)实例。 + * + * 主要特点: + * - 基于Guava Cache实现高效的生产者实例缓存管理 + * - 自动处理生产者的生命周期(创建、获取、关闭) + * - 支持30分钟未访问自动过期清理机制 + * - 异常处理与日志记录,便于问题排查 + * + * 子类需要实现: + * - initProducer(Config) - 初始化特定类型的生产者实例 + * - closeProducer(Producer) - 关闭生产者实例并释放资源 + * + * @param 配置信息类型,用于初始化生产者 + * @param 生产者类型,负责将数据发送到目标系统 + * @author HUIHUI + */ +@Slf4j +public abstract class AbstractCacheableDataBridgeExecute implements IotDataBridgeExecute { + + /** + * Producer 缓存 + */ + private final LoadingCache PRODUCER_CACHE = CacheBuilder.newBuilder() + .expireAfterAccess(Duration.ofMinutes(30)) // 30 分钟未访问就提前过期 + .removalListener((RemovalListener) notification -> { + Producer producer = notification.getValue(); + if (producer == null) { + return; + } + + try { + closeProducer(producer); + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已关闭]", notification.getKey()); + } catch (Exception e) { + log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 关闭失败]", notification.getKey(), e); + } + }) + .build(new CacheLoader() { + + @Override + public Producer load(Config config) throws Exception { + try { + Producer producer = initProducer(config); + log.info("[PRODUCER_CACHE][配置({}) 对应的 producer 已创建并启动]", config); + return producer; + } catch (Exception e) { + log.error("[PRODUCER_CACHE][配置({}) 对应的 producer 创建启动失败]", config, e); + throw e; // 抛出异常,触发缓存加载失败机制 + } + } + + }); + + /** + * 获取生产者 + * + * @param config 配置信息 + * @return 生产者对象 + */ + protected Producer getProducer(Config config) throws Exception { + return PRODUCER_CACHE.get(config); + } + + /** + * 初始化生产者 + * + * @param config 配置信息 + * @return 生产者对象 + * @throws Exception 如果初始化失败 + */ + protected abstract Producer initProducer(Config config) throws Exception; + + /** + * 关闭生产者 + * + * @param producer 生产者对象 + */ + protected abstract void closeProducer(Producer producer) throws Exception; + + @Override + @SuppressWarnings({"unchecked"}) + public void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) { + if (ObjUtil.notEqual(message.getType(), getType())) { + return; + } + try { + execute0(message, (Config) dataBridge.getConfig()); + } catch (Exception e) { + log.error("[execute][桥梁配置 config({}) 对应的 message({}) 发送异常]", dataBridge.getConfig(), message, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java new file mode 100644 index 000000000..ce3d0f193 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecute.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; + + +/** + * IoT 数据桥梁的执行器 execute 接口 + * + * @author HUIHUI + */ +public interface IotDataBridgeExecute { + + /** + * 获取数据桥梁类型 + * + * @return 数据桥梁类型 + */ + Integer getType(); + + /** + * 执行数据桥梁操作 + * + * @param message 设备消息 + * @param dataBridge 数据桥梁 + */ + @SuppressWarnings({"unchecked"}) + default void execute(IotDeviceMessage message, IotDataBridgeDO dataBridge) throws Exception { + // 1.1 校验数据桥梁类型 + if (!getType().equals(dataBridge.getType())) { + return; + } + + // 1.2 执行对应的数据桥梁发送消息 + execute0(message, (Config) dataBridge.getConfig()); + } + + /** + * 【真正】执行数据桥梁操作 + * + * @param message 设备消息 + * @param config 桥梁配置 + */ + void execute0(IotDeviceMessage message, Config config) throws Exception; + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java new file mode 100644 index 000000000..22b72e055 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotHttpDataBridgeExecute.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeHttpConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Http 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotHttpDataBridgeExecute implements IotDataBridgeExecute { + + @Resource + private RestTemplate restTemplate; + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.HTTP.getType(); + } + + @Override + @SuppressWarnings({"unchecked", "deprecation"}) + public void execute0(IotDeviceMessage message, IotDataBridgeHttpConfig config) { + String url = null; + HttpMethod method = HttpMethod.valueOf(config.getMethod().toUpperCase()); + HttpEntity requestEntity = null; + ResponseEntity responseEntity = null; + try { + // 1.1 构建 Header + HttpHeaders headers = new HttpHeaders(); + if (CollUtil.isNotEmpty(config.getHeaders())) { + config.getHeaders().putAll(config.getHeaders()); + } + headers.add(HEADER_TENANT_ID, message.getTenantId().toString()); + // 1.2 构建 URL + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(config.getUrl()); + if (CollUtil.isNotEmpty(config.getQuery())) { + config.getQuery().forEach(uriBuilder::queryParam); + } + // 1.3 构建请求体 + if (method == HttpMethod.GET) { + uriBuilder.queryParam("message", HttpUtils.encodeUtf8(JsonUtils.toJsonString(message))); + url = uriBuilder.build().toUriString(); + requestEntity = new HttpEntity<>(headers); + } else { + url = uriBuilder.build().toUriString(); + Map requestBody = JsonUtils.parseObject(config.getBody(), Map.class); + if (requestBody == null) { + requestBody = new HashMap<>(); + } + requestBody.put("message", message); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE); + requestEntity = new HttpEntity<>(JsonUtils.toJsonString(requestBody), headers); + } + + // 2.1 发送请求 + responseEntity = restTemplate.exchange(url, method, requestEntity, String.class); + // 2.2 记录日志 + if (responseEntity.getStatusCode().is2xxSuccessful()) { + log.info("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求成功({})]", + message, config, url, method, requestEntity, responseEntity); + } else { + log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求失败({})]", + message, config, url, method, requestEntity, responseEntity); + } + } catch (Exception e) { + log.error("[executeHttp][message({}) config({}) url({}) method({}) requestEntity({}) 请求异常({})]", + message, config, url, method, requestEntity, responseEntity, e); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java new file mode 100644 index 000000000..5674c7d60 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotKafkaMQDataBridgeExecute.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeKafkaMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Kafka 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "org.springframework.kafka.core.KafkaTemplate") +@Component +@Slf4j +public class IotKafkaMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute> { + + private static final Duration SEND_TIMEOUT = Duration.ofMillis(10000); // 10 秒超时时间 + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.KAFKA.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeKafkaMQConfig config) throws Exception { + // 1. 获取或创建 KafkaTemplate + KafkaTemplate kafkaTemplate = getProducer(config); + + // 2. 发送消息并等待结果 + kafkaTemplate.send(config.getTopic(), message.toString()) + .get(SEND_TIMEOUT.getSeconds(), TimeUnit.SECONDS); // 添加超时等待 + log.info("[execute0][message({}) 发送成功]", message); + } + + @Override + protected KafkaTemplate initProducer(IotDataBridgeKafkaMQConfig config) { + // 1.1 构建生产者配置 + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + // 1.2 如果配置了认证信息 + if (config.getUsername() != null && config.getPassword() != null) { + props.put("security.protocol", "SASL_PLAINTEXT"); + props.put("sasl.mechanism", "PLAIN"); + props.put("sasl.jaas.config", + "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"" + + config.getUsername() + "\" password=\"" + config.getPassword() + "\";"); + } + // 1.3 如果启用 SSL + if (Boolean.TRUE.equals(config.getSsl())) { + props.put("security.protocol", "SSL"); + } + + // 2. 创建 KafkaTemplate + DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>(props); + return new KafkaTemplate<>(producerFactory); + } + + @Override + protected void closeProducer(KafkaTemplate producer) { + producer.destroy(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java new file mode 100644 index 000000000..efe08b1fc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRabbitMQDataBridgeExecute.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRabbitMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * RabbitMQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "com.rabbitmq.client.Channel") +@Component +@Slf4j +public class IotRabbitMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute { + + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.RABBITMQ.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRabbitMQConfig config) throws Exception { + // 1. 获取或创建 Channel + Channel channel = getProducer(config); + + // 2.1 声明交换机、队列和绑定关系 + channel.exchangeDeclare(config.getExchange(), "direct", true); + channel.queueDeclare(config.getQueue(), true, false, false, null); + channel.queueBind(config.getQueue(), config.getExchange(), config.getRoutingKey()); + + // 2.2 发送消息 + channel.basicPublish(config.getExchange(), config.getRoutingKey(), null, + message.toString().getBytes(StandardCharsets.UTF_8)); + log.info("[executeRabbitMQ][message({}) config({}) 发送成功]", message, config); + } + + @Override + @SuppressWarnings("resource") + protected Channel initProducer(IotDataBridgeRabbitMQConfig config) throws Exception { + // 1. 创建连接工厂 + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(config.getHost()); + factory.setPort(config.getPort()); + factory.setVirtualHost(config.getVirtualHost()); + factory.setUsername(config.getUsername()); + factory.setPassword(config.getPassword()); + + // 2. 创建连接 + Connection connection = factory.newConnection(); + + // 3. 创建信道 + return connection.createChannel(); + } + + @Override + protected void closeProducer(Channel channel) throws Exception { + if (channel.isOpen()) { + channel.close(); + } + Connection connection = channel.getConnection(); + if (connection.isOpen()) { + connection.close(); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java new file mode 100644 index 000000000..2aac76619 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRedisStreamMQDataBridgeExecute.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRedisStreamMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.redisson.spring.data.connection.RedissonConnectionFactory; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; + +/** + * Redis Stream MQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotRedisStreamMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute> { + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.REDIS_STREAM.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRedisStreamMQConfig config) throws Exception { + // 1. 获取 RedisTemplate + RedisTemplate redisTemplate = getProducer(config); + + // 2. 创建并发送 Stream 记录 + ObjectRecord record = StreamRecords.newRecord() + .ofObject(message).withStreamKey(config.getTopic()); + String recordId = String.valueOf(redisTemplate.opsForStream().add(record)); + log.info("[executeRedisStream][消息发送成功] messageId: {}, config: {}", recordId, config); + } + + @Override + protected RedisTemplate initProducer(IotDataBridgeRedisStreamMQConfig config) { + // 1.1 创建 Redisson 配置 + Config redissonConfig = new Config(); + SingleServerConfig serverConfig = redissonConfig.useSingleServer() + .setAddress("redis://" + config.getHost() + ":" + config.getPort()) + .setDatabase(config.getDatabase()); + // 1.2 设置密码(如果有) + if (StrUtil.isNotBlank(config.getPassword())) { + serverConfig.setPassword(config.getPassword()); + } + + // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 + // 2.1 创建 RedissonClient + RedissonClient redisson = Redisson.create(redissonConfig); + // 2.2 创建并配置 RedisTemplate + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(new RedissonConnectionFactory(redisson)); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(buildRedisSerializer()); + template.setHashValueSerializer(buildRedisSerializer()); + template.afterPropertiesSet();// 初始化 + return template; + } + + @Override + protected void closeProducer(RedisTemplate producer) throws Exception { + RedisConnectionFactory factory = producer.getConnectionFactory(); + if (factory != null) { + ((RedissonConnectionFactory) factory).destroy(); + } + } + + // TODO @huihui:看看能不能简化一些。按道理说,不用这么多的哈。 + public static RedisSerializer buildRedisSerializer() { + RedisSerializer json = RedisSerializer.json(); + // 解决 LocalDateTime 的序列化 + ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); + objectMapper.registerModules(new JavaTimeModule()); + return json; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java new file mode 100644 index 000000000..d3ac77227 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotRocketMQDataBridgeExecute.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.IotDataBridgeRocketMQConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataBridgeTypeEnum; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.SendStatus; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.stereotype.Component; + +/** + * RocketMQ 的 {@link IotDataBridgeExecute} 实现类 + * + * @author HUIHUI + */ +@ConditionalOnClass(name = "org.apache.rocketmq.client.producer.DefaultMQProducer") +@Component +@Slf4j +public class IotRocketMQDataBridgeExecute extends + AbstractCacheableDataBridgeExecute { + + @Override + public Integer getType() { + return IotDataBridgeTypeEnum.ROCKETMQ.getType(); + } + + @Override + public void execute0(IotDeviceMessage message, IotDataBridgeRocketMQConfig config) throws Exception { + // 1. 获取或创建 Producer + DefaultMQProducer producer = getProducer(config); + + // 2.1 创建消息对象,指定Topic、Tag和消息体 + Message msg = new Message( + config.getTopic(), + config.getTags(), + message.toString().getBytes(RemotingHelper.DEFAULT_CHARSET) + ); + // 2.2 发送同步消息并处理结果 + SendResult sendResult = producer.send(msg); + // 2.3 处理发送结果 + if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { + log.info("[executeRocketMQ][message({}) config({}) 发送成功,结果({})]", message, config, sendResult); + } else { + log.error("[executeRocketMQ][message({}) config({}) 发送失败,结果({})]", message, config, sendResult); + } + } + + @Override + protected DefaultMQProducer initProducer(IotDataBridgeRocketMQConfig config) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer(config.getGroup()); + producer.setNamesrvAddr(config.getNameServer()); + producer.start(); + return producer; + } + + @Override + protected void closeProducer(DefaultMQProducer producer) { + producer.shutdown(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java new file mode 100644 index 000000000..8834772d3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import jakarta.validation.Valid; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * IoT 产品物模型 Service 接口 + * + * @author 芋道源码 + */ +public interface IotThingModelService { + + /** + * 创建产品物模型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createThingModel(@Valid IotThingModelSaveReqVO createReqVO); + + /** + * 更新产品物模型 + * + * @param updateReqVO 更新信息 + */ + void updateThingModel(@Valid IotThingModelSaveReqVO updateReqVO); + + /** + * 删除产品物模型 + * + * @param id 编号 + */ + void deleteThingModel(Long id); + + /** + * 获得产品物模型 + * + * @param id 编号 + * @return 产品物模型 + */ + IotThingModelDO getThingModel(Long id); + + /** + * 获得产品物模型列表 + * + * @param productId 产品编号 + * @return 产品物模型列表 + */ + List getThingModelListByProductId(Long productId); + + /** + * 【缓存】获得产品物模型列表 + * + * 注意:该方法会忽略租户信息,所以调用时,需要确认会不会有跨租户访问的风险!!! + * + * @param productKey 产品标识 + * @return 产品物模型列表 + */ + List getThingModelListByProductKeyFromCache(String productKey); + + /** + * 获得产品物模型分页 + * + * @param pageReqVO 分页查询 + * @return 产品物模型分页 + */ + PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO); + + /** + * 获得产品物模型列表 + * + * @param reqVO 列表查询 + * @return 产品物模型列表 + */ + List getThingModelList(IotThingModelListReqVO reqVO); + + // TODO @super:用不到,删除下哈。 + /** + * 获得物模型数量 + * + * @param createTime 创建时间,如果为空,则统计所有物模型数量 + * @return 物模型数量 + */ + Long getThingModelCount(LocalDateTime createTime); + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java new file mode 100644 index 000000000..9487ff2de --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -0,0 +1,373 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelEvent; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelParam; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.ThingModelService; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; +import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelSaveReqVO; +import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; +import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.*; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; + +/** + * IoT 产品物模型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class IotThingModelServiceImpl implements IotThingModelService { + + @Resource + private IotThingModelMapper thingModelMapper; + + @Resource + private IotProductService productService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createThingModel(IotThingModelSaveReqVO createReqVO) { + // 1.1 校验功能标识符在同一产品下是否唯一 + validateIdentifierUnique(null, createReqVO.getProductId(), createReqVO.getIdentifier()); + // 1.2 功能名称在同一产品下是否唯一 + validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); + // 1.3 校验产品状态,发布状态下,不允许新增功能 + validateProductStatus(createReqVO.getProductId()); + + // 2. 插入数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(createReqVO); + thingModelMapper.insert(thingModel); + + // 3. 如果创建的是属性,需要更新默认的事件和服务 + if (Objects.equals(createReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(createReqVO.getProductKey()); + return thingModel.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateThingModel(IotThingModelSaveReqVO updateReqVO) { + // 1.1 校验功能是否存在 + validateProductThingModelMapperExists(updateReqVO.getId()); + // 1.2 校验功能标识符是否唯一 + validateIdentifierUnique(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); + // 1.3 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(updateReqVO.getProductId()); + + // 2. 更新数据库 + IotThingModelDO thingModel = IotThingModelConvert.INSTANCE.convert(updateReqVO); + thingModelMapper.updateById(thingModel); + + // 3. 如果更新的是属性,需要更新默认的事件和服务 + if (Objects.equals(updateReqVO.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(updateReqVO.getProductKey()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteThingModel(Long id) { + // 1.1 校验功能是否存在 + IotThingModelDO thingModel = thingModelMapper.selectById(id); + if (thingModel == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + // 1.2 校验产品状态,发布状态下,不允许操作功能 + validateProductStatus(thingModel.getProductId()); + + // 2. 删除功能 + thingModelMapper.deleteById(id); + + // 3. 如果删除的是属性,需要更新默认的事件和服务 + if (Objects.equals(thingModel.getType(), IotThingModelTypeEnum.PROPERTY.getType())) { + createDefaultEventsAndServices(thingModel.getProductId(), thingModel.getProductKey()); + } + + // 4. 删除缓存 + deleteThingModelListCache(thingModel.getProductKey()); + } + + @Override + public IotThingModelDO getThingModel(Long id) { + return thingModelMapper.selectById(id); + } + + @Override + public List getThingModelListByProductId(Long productId) { + return thingModelMapper.selectListByProductId(productId); + } + + @Override + @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") + @TenantIgnore // 忽略租户信息,跨租户 productKey 是唯一的 + public List getThingModelListByProductKeyFromCache(String productKey) { + return thingModelMapper.selectListByProductKey(productKey); + } + + @Override + public PageResult getProductThingModelPage(IotThingModelPageReqVO pageReqVO) { + return thingModelMapper.selectPage(pageReqVO); + } + + @Override + public List getThingModelList(IotThingModelListReqVO reqVO) { + return thingModelMapper.selectList(reqVO); + } + + /** + * 校验功能是否存在 + * + * @param id 功能编号 + */ + private void validateProductThingModelMapperExists(Long id) { + if (thingModelMapper.selectById(id) == null) { + throw exception(THING_MODEL_NOT_EXISTS); + } + } + + private void validateIdentifierUnique(Long id, Long productId, String identifier) { + // 1.0 情况一:创建时校验 + if (id == null) { + // 1.1 系统保留字段,不能用于标识符定义 + if (StrUtil.equalsAny(identifier, "set", "get", "post", "property", "event", "time", "value")) { + throw exception(THING_MODEL_IDENTIFIER_INVALID); + } + + // 1.2 校验唯一 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + return; + } + + // 2.0 情况二:更新时校验 + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndIdentifier(productId, identifier); + if (thingModel != null && ObjectUtil.notEqual(thingModel.getId(), id)) { + throw exception(THING_MODEL_IDENTIFIER_EXISTS); + } + } + + private void validateProductStatus(Long createReqVO) { + IotProductDO product = productService.validateProductExists(createReqVO); + if (Objects.equals(product.getStatus(), IotProductStatusEnum.PUBLISHED.getStatus())) { + throw exception(PRODUCT_STATUS_NOT_ALLOW_THING_MODEL); + } + } + + private void validateNameUnique(Long productId, String name) { + IotThingModelDO thingModel = thingModelMapper.selectByProductIdAndName(productId, name); + if (thingModel != null) { + throw exception(THING_MODEL_NAME_EXISTS); + } + } + + /** + * 创建默认的事件和服务 + * + * @param productId 产品编号 + * @param productKey 产品标识 + */ + public void createDefaultEventsAndServices(Long productId, String productKey) { + // 1. 获取当前属性列表 + List properties = thingModelMapper + .selectListByProductIdAndType(productId, IotThingModelTypeEnum.PROPERTY.getType()); + + // 2. 生成新的事件和服务列表 + List newThingModels = new ArrayList<>(); + // 2.1 生成属性上报事件 + ThingModelEvent propertyPostEvent = generatePropertyPostEvent(properties); + if (propertyPostEvent != null) { + newThingModels.add(buildEventThingModel(productId, productKey, propertyPostEvent, "属性上报事件")); + } + // 2.2 生成属性设置服务 + ThingModelService propertySetService = generatePropertySetService(properties); + if (propertySetService != null) { + newThingModels.add(buildServiceThingModel(productId, productKey, propertySetService, "属性设置服务")); + } + // 2.3 生成属性获取服务 + ThingModelService propertyGetService = generatePropertyGetService(properties); + if (propertyGetService != null) { + newThingModels.add(buildServiceThingModel(productId, productKey, propertyGetService, "属性获取服务")); + } + + // 3.1 获取数据库中的默认的旧事件和服务列表 + List oldThingModels = thingModelMapper.selectListByProductIdAndIdentifiersAndTypes( + productId, + Arrays.asList("post", "set", "get"), + Arrays.asList(IotThingModelTypeEnum.EVENT.getType(), IotThingModelTypeEnum.SERVICE.getType()) + ); + // 3.2 创建默认的事件和服务 + createDefaultEventsAndServices(oldThingModels, newThingModels); + } + + /** + * 创建默认的事件和服务 + */ + private void createDefaultEventsAndServices(List oldThingModels, + List newThingModels) { + // 使用 diffList 方法比较新旧列表 + List> diffResult = diffList(oldThingModels, newThingModels, + (oldVal, newVal) -> { + // 继续使用 identifier 和 type 进行比较:这样可以准确地匹配对应的功能对象。 + boolean same = Objects.equals(oldVal.getIdentifier(), newVal.getIdentifier()) + && Objects.equals(oldVal.getType(), newVal.getType()); + if (same) { + newVal.setId(oldVal.getId()); // 设置编号 + } + return same; + }); + // 批量添加、修改、删除 + if (CollUtil.isNotEmpty(diffResult.get(0))) { + thingModelMapper.insertBatch(diffResult.get(0)); + } + if (CollUtil.isNotEmpty(diffResult.get(1))) { + thingModelMapper.updateBatch(diffResult.get(1)); + } + if (CollUtil.isNotEmpty(diffResult.get(2))) { + thingModelMapper.deleteByIds(convertSet(diffResult.get(2), IotThingModelDO::getId)); + } + } + + /** + * 构建事件功能对象 + */ + private IotThingModelDO buildEventThingModel(Long productId, String productKey, + ThingModelEvent event, String description) { + return new IotThingModelDO().setProductId(productId).setProductKey(productKey) + .setIdentifier(event.getIdentifier()).setName(event.getName()).setDescription(description) + .setType(IotThingModelTypeEnum.EVENT.getType()).setEvent(event); + } + + /** + * 构建服务功能对象 + */ + private IotThingModelDO buildServiceThingModel(Long productId, String productKey, + ThingModelService service, String description) { + return new IotThingModelDO().setProductId(productId).setProductKey(productKey) + .setIdentifier(service.getIdentifier()).setName(service.getName()).setDescription(description) + .setType(IotThingModelTypeEnum.SERVICE.getType()).setService(service); + } + + // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 + + /** + * 生成属性上报事件 + */ + private ThingModelEvent generatePropertyPostEvent(List thingModels) { + // 没有属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 生成属性上报事件 + return new ThingModelEvent().setIdentifier("post").setName("属性上报").setMethod("thing.event.property.post") + .setType(IotThingModelServiceEventTypeEnum.INFO.getType()) + .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); + } + + // TODO @haohao:是不是不用生成这个?目前属性上报,是个批量接口 + + /** + * 生成属性设置服务 + */ + private ThingModelService generatePropertySetService(List thingModels) { + // 1.1 过滤出所有可写属性 + thingModels = filterList(thingModels, thingModel -> + IotThingModelAccessModeEnum.READ_WRITE.getMode().equals(thingModel.getProperty().getAccessMode())); + // 1.2 没有可写属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 2. 生成属性设置服务 + return new ThingModelService().setIdentifier("set").setName("属性设置").setMethod("thing.service.property.set") + .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) + .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) + .setOutputParams(Collections.emptyList()); // 属性设置服务一般不需要输出参数 + } + + /** + * 生成属性获取服务 + */ + private ThingModelService generatePropertyGetService(List thingModels) { + // 1.1 没有属性则不生成 + if (CollUtil.isEmpty(thingModels)) { + return null; + } + + // 1.2 生成属性获取服务 + return new ThingModelService().setIdentifier("get").setName("属性获取").setMethod("thing.service.property.get") + .setCallType(IotThingModelServiceCallTypeEnum.ASYNC.getType()) + .setInputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.INPUT)) + .setOutputParams(buildInputOutputParam(thingModels, IotThingModelParamDirectionEnum.OUTPUT)); + } + + /** + * 构建输入/输出参数列表 + * + * @param thingModels 属性列表 + * @return 输入/输出参数列表 + */ + private List buildInputOutputParam(List thingModels, + IotThingModelParamDirectionEnum direction) { + return convertList(thingModels, thingModel -> + BeanUtils.toBean(thingModel.getProperty(), ThingModelParam.class).setParaOrder(0) // TODO @puhui999: 先搞个默认值看看怎么个事 + .setDirection(direction.getDirection())); + } + + private void deleteThingModelListCache(String productKey) { + // 保证 Spring AOP 触发 + getSelf().deleteThingModelListCache0(productKey); + } + + @CacheEvict(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") + public void deleteThingModelListCache0(String productKey) { + } + + private IotThingModelServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + + // TODO @super:用不到,删除下; + @Override + public Long getThingModelCount(LocalDateTime createTime) { + return thingModelMapper.selectCountByCreateTime(createTime); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java deleted file mode 100644 index ce8e472f5..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionService.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.thinkmodelfunction; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import jakarta.validation.Valid; - -import java.util.List; - -/** - * IoT 产品物模型 Service 接口 - * - * @author 芋道源码 - */ -public interface IotThinkModelFunctionService { - - /** - * 创建产品物模型 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createThinkModelFunction(@Valid IotThinkModelFunctionSaveReqVO createReqVO); - - - /** - * 更新产品物模型 - * - * @param updateReqVO 更新信息 - */ - void updateThinkModelFunction(@Valid IotThinkModelFunctionSaveReqVO updateReqVO); - - /** - * 删除产品物模型 - * - * @param id 编号 - */ - void deleteThinkModelFunction(Long id); - - /** - * 获得产品物模型 - * - * @param id 编号 - * @return 产品物模型 - */ - IotThinkModelFunctionDO getThinkModelFunction(Long id); - - /** - * 获得产品物模型列表 - * - * @param productId 产品编号 - * @return 产品物模型列表 - */ - List getThinkModelFunctionListByProductId(Long productId); - - /** - * 获得产品物模型分页 - * - * @param pageReqVO 分页查询 - * @return 产品物模型分页 - */ - PageResult getThinkModelFunctionPage(IotThinkModelFunctionPageReqVO pageReqVO); - -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java deleted file mode 100644 index 793786944..000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thinkmodelfunction/IotThinkModelFunctionServiceImpl.java +++ /dev/null @@ -1,404 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.thinkmodelfunction; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelEvent; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelProperty; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.ThingModelService; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArgument; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArraySpecs; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelArrayType; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.thingModel.dataType.ThingModelTextType; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionPageReqVO; -import cn.iocoder.yudao.module.iot.controller.admin.thinkmodelfunction.vo.IotThinkModelFunctionSaveReqVO; -import cn.iocoder.yudao.module.iot.convert.thinkmodelfunction.IotThinkModelFunctionConvert; -import cn.iocoder.yudao.module.iot.dal.dataobject.thinkmodelfunction.IotThinkModelFunctionDO; -import cn.iocoder.yudao.module.iot.dal.mysql.thinkmodelfunction.IotThinkModelFunctionMapper; -import cn.iocoder.yudao.module.iot.enums.product.IotAccessModeEnum; -import cn.iocoder.yudao.module.iot.enums.product.IotProductFunctionTypeEnum; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; - -import java.util.*; -import java.util.stream.Collectors; - -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; - -/** - * IoT 产品物模型 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Validated -@Slf4j -public class IotThinkModelFunctionServiceImpl implements IotThinkModelFunctionService { - - @Resource - private IotThinkModelFunctionMapper thinkModelFunctionMapper; - - @Override - @Transactional(rollbackFor = Exception.class) - public Long createThinkModelFunction(IotThinkModelFunctionSaveReqVO createReqVO) { - // 1. 校验功能标识符在同一产品下是否唯一 - validateIdentifierUnique(createReqVO.getProductId(), createReqVO.getIdentifier()); - - // 2. 功能名称在同一产品下是否唯一 - validateNameUnique(createReqVO.getProductId(), createReqVO.getName()); - - // 3. 系统保留字段,不能用于标识符定义 - validateNotDefaultEventAndService(createReqVO.getIdentifier()); - - // 3. 插入数据库 - IotThinkModelFunctionDO function = IotThinkModelFunctionConvert.INSTANCE.convert(createReqVO); - thinkModelFunctionMapper.insert(function); - - // 4. 如果创建的是属性,需要更新默认的事件和服务 - if (Objects.equals(createReqVO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(createReqVO.getProductId(), createReqVO.getProductKey()); - } - return function.getId(); - } - - private void validateNotDefaultEventAndService(String identifier) { - // set, get, post, property, event, time, value 是系统保留字段,不能用于标识符定义 - if (CollUtil.containsAny(Arrays.asList("set", "get", "post", "property", "event", "time", "value"), Collections.singletonList(identifier))) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_INVALID); - } -// if (CollUtil.containsAny(Arrays.asList("post", "set", "get"), identifier)) { -// throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_INVALID); -// } - } - - private void validateNameUnique(Long productId, String name) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndName(productId, name); - if (function != null) { - throw exception(THINK_MODEL_FUNCTION_NAME_EXISTS); - } - } - - private void validateIdentifierUnique(Long productId, String identifier) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndIdentifier(productId, identifier); - if (function != null) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void updateThinkModelFunction(IotThinkModelFunctionSaveReqVO updateReqVO) { - // 1. 校验功能是否存在 - validateThinkModelFunctionExists(updateReqVO.getId()); - - // 2. 校验功能标识符是否唯一 - validateIdentifierUniqueForUpdate(updateReqVO.getId(), updateReqVO.getProductId(), updateReqVO.getIdentifier()); - - // 3. 更新数据库 - IotThinkModelFunctionDO thinkModelFunction = IotThinkModelFunctionConvert.INSTANCE.convert(updateReqVO); - thinkModelFunctionMapper.updateById(thinkModelFunction); - - // 4. 如果更新的是属性,需要更新默认的事件和服务 - if (Objects.equals(updateReqVO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(updateReqVO.getProductId(), updateReqVO.getProductKey()); - } - } - - private void validateIdentifierUniqueForUpdate(Long id, Long productId, String identifier) { - IotThinkModelFunctionDO function = thinkModelFunctionMapper.selectByProductIdAndIdentifier(productId, identifier); - if (function != null && ObjectUtil.notEqual(function.getId(), id)) { - throw exception(THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS); - } - } - - @Override - @Transactional(rollbackFor = Exception.class) - public void deleteThinkModelFunction(Long id) { - // 1. 校验功能是否存在 - IotThinkModelFunctionDO functionDO = thinkModelFunctionMapper.selectById(id); - if (functionDO == null) { - throw exception(THINK_MODEL_FUNCTION_NOT_EXISTS); - } - - // 2. 删除功能 - thinkModelFunctionMapper.deleteById(id); - - // 3. 如果删除的是属性,需要更新默认的事件和服务 - if (Objects.equals(functionDO.getType(), IotProductFunctionTypeEnum.PROPERTY.getType())) { - createDefaultEventsAndServices(functionDO.getProductId(), functionDO.getProductKey()); - } - } - - /** - * 校验功能是否存在 - * - * @param id 功能编号 - */ - private void validateThinkModelFunctionExists(Long id) { - if (thinkModelFunctionMapper.selectById(id) == null) { - throw exception(THINK_MODEL_FUNCTION_NOT_EXISTS); - } - } - - @Override - public IotThinkModelFunctionDO getThinkModelFunction(Long id) { - return thinkModelFunctionMapper.selectById(id); - } - - @Override - public List getThinkModelFunctionListByProductId(Long productId) { - return thinkModelFunctionMapper.selectListByProductId(productId); - } - - @Override - public PageResult getThinkModelFunctionPage(IotThinkModelFunctionPageReqVO pageReqVO) { - return thinkModelFunctionMapper.selectPage(pageReqVO); - } - - /** - * 创建默认的事件和服务 - */ - public void createDefaultEventsAndServices(Long productId, String productKey) { - // 1. 获取当前属性列表 - List propertyList = thinkModelFunctionMapper - .selectListByProductIdAndType(productId, IotProductFunctionTypeEnum.PROPERTY.getType()); - - // 2. 生成新的事件和服务列表 - List newFunctionList = new ArrayList<>(); - // 生成属性上报事件 - ThingModelEvent propertyPostEvent = generatePropertyPostEvent(propertyList); - if (propertyPostEvent != null) { - IotThinkModelFunctionDO eventFunction = buildEventFunctionDO(productId, productKey, propertyPostEvent); - newFunctionList.add(eventFunction); - } - // 生成属性设置服务 - ThingModelService propertySetService = generatePropertySetService(propertyList); - if (propertySetService != null) { - IotThinkModelFunctionDO setServiceFunction = buildServiceFunctionDO(productId, productKey, propertySetService); - newFunctionList.add(setServiceFunction); - } - // 生成属性获取服务 - ThingModelService propertyGetService = generatePropertyGetService(propertyList); - if (propertyGetService != null) { - IotThinkModelFunctionDO getServiceFunction = buildServiceFunctionDO(productId, productKey, propertyGetService); - newFunctionList.add(getServiceFunction); - } - - // 3. 获取数据库中的默认的旧事件和服务列表 - List oldFunctionList = thinkModelFunctionMapper.selectListByProductIdAndIdentifiersAndTypes( - productId, - Arrays.asList("post", "set", "get"), - Arrays.asList(IotProductFunctionTypeEnum.EVENT.getType(), IotProductFunctionTypeEnum.SERVICE.getType()) - ); - - // 3.1 使用 diffList 方法比较新旧列表 - List> diffResult = diffList(oldFunctionList, newFunctionList, - // 继续使用 identifier 和 type 进行比较:这样可以准确地匹配对应的功能对象。 - (oldFunc, newFunc) -> Objects.equals(oldFunc.getIdentifier(), newFunc.getIdentifier()) - && Objects.equals(oldFunc.getType(), newFunc.getType())); - List createList = diffResult.get(0); // 需要新增的 - List updateList = diffResult.get(1); // 需要更新的 - List deleteList = diffResult.get(2); // 需要删除的 - - // 3.2 批量执行数据库操作 - // 新增数据库中的新事件和服务列表 - if (CollUtil.isNotEmpty(createList)) { - thinkModelFunctionMapper.insertBatch(createList); - } - // 更新数据库中的事件和服务列表 - if (CollUtil.isNotEmpty(updateList)) { - // 首先,为每个需要更新的对象设置其对应的 ID - updateList.forEach(updateFunc -> { - IotThinkModelFunctionDO oldFunc = findFunctionByIdentifierAndType( - oldFunctionList, updateFunc.getIdentifier(), updateFunc.getType()); - if (oldFunc != null) { - updateFunc.setId(oldFunc.getId()); - } - }); - // 过滤掉没有设置 ID 的对象 - List validUpdateList = updateList.stream() - .filter(func -> func.getId() != null) - .collect(Collectors.toList()); - // 执行批量更新 - if (CollUtil.isNotEmpty(validUpdateList)) { - thinkModelFunctionMapper.updateBatch(validUpdateList); - } - } - - // 删除数据库中的旧事件和服务列表 - if (CollUtil.isNotEmpty(deleteList)) { - Set idsToDelete = CollectionUtils.convertSet(deleteList, IotThinkModelFunctionDO::getId); - thinkModelFunctionMapper.deleteByIds(idsToDelete); - } - } - - /** - * 根据标识符和类型查找功能对象 - */ - private IotThinkModelFunctionDO findFunctionByIdentifierAndType(List functionList, - String identifier, Integer type) { - return CollUtil.findOne(functionList, func -> - Objects.equals(func.getIdentifier(), identifier) && Objects.equals(func.getType(), type)); - } - - /** - * 构建事件功能对象 - */ - private IotThinkModelFunctionDO buildEventFunctionDO(Long productId, String productKey, ThingModelEvent event) { - return new IotThinkModelFunctionDO() - .setProductId(productId) - .setProductKey(productKey) - .setIdentifier(event.getIdentifier()) - .setName(event.getName()) - .setDescription(event.getDescription()) - .setType(IotProductFunctionTypeEnum.EVENT.getType()) - .setEvent(event); - } - - /** - * 构建服务功能对象 - */ - private IotThinkModelFunctionDO buildServiceFunctionDO(Long productId, String productKey, ThingModelService service) { - return new IotThinkModelFunctionDO() - .setProductId(productId) - .setProductKey(productKey) - .setIdentifier(service.getIdentifier()) - .setName(service.getName()) - .setDescription(service.getDescription()) - .setType(IotProductFunctionTypeEnum.SERVICE.getType()) - .setService(service); - } - - /** - * 生成属性上报事件 - */ - private ThingModelEvent generatePropertyPostEvent(List propertyList) { - if (CollUtil.isEmpty(propertyList)) { - return null; - } - - ThingModelEvent event = new ThingModelEvent() - .setIdentifier("post") - .setName("属性上报") - .setType("info") - .setDescription("属性上报事件") - .setMethod("thing.event.property.post"); - - // 将属性列表转换为事件的输出参数 - List outputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("output"); // 设置为输出参数 - outputData.add(arg); - } - event.setOutputData(outputData); - return event; - } - - /** - * 生成属性设置服务 - */ - private ThingModelService generatePropertySetService(List propertyList) { - if (propertyList == null || propertyList.isEmpty()) { - return null; - } - - List inputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - if (IotAccessModeEnum.WRITE.getMode().equals(property.getAccessMode()) || IotAccessModeEnum.READ_WRITE.getMode().equals(property.getAccessMode())) { - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("input"); // 设置为输入参数 - inputData.add(arg); - } - } - if (inputData.isEmpty()) { - // 如果没有可写属性,不生成属性设置服务 - return null; - } - - // 属性设置服务一般不需要输出参数 - return new ThingModelService() - .setIdentifier("set") - .setName("属性设置") - .setCallType("async") - .setDescription("属性设置服务") - .setMethod("thing.service.property.set") - .setInputData(inputData) - // 属性设置服务一般不需要输出参数 - .setOutputData(new ArrayList<>()); - } - - /** - * 生成属性获取服务 - */ - private ThingModelService generatePropertyGetService(List propertyList) { - if (propertyList == null || propertyList.isEmpty()) { - return null; - } - - List outputData = new ArrayList<>(); - for (IotThinkModelFunctionDO functionDO : propertyList) { - ThingModelProperty property = functionDO.getProperty(); - if (ObjectUtils.equalsAny(property.getAccessMode(), - IotAccessModeEnum.READ.getMode(), IotAccessModeEnum.READ_WRITE.getMode())) { - ThingModelArgument arg = new ThingModelArgument() - .setIdentifier(property.getIdentifier()) - .setName(property.getName()) - .setDataType(property.getDataType()) - .setDescription(property.getDescription()) - .setDirection("output"); // 设置为输出参数 - outputData.add(arg); - } - } - if (outputData.isEmpty()) { - // 如果没有可读属性,不生成属性获取服务 - return null; - } - - ThingModelService service = new ThingModelService() - .setIdentifier("get") - .setName("属性获取") - .setCallType("async") - .setDescription("属性获取服务") - .setMethod("thing.service.property.get"); - - // 定义输入参数:属性标识符列表 - ThingModelArgument inputArg = new ThingModelArgument() - .setIdentifier("properties") - .setName("属性标识符列表") - .setDescription("需要获取的属性标识符列表") - .setDirection("input"); // 设置为输入参数 - - // 创建数组类型,元素类型为文本类型(字符串) - ThingModelArrayType arrayType = new ThingModelArrayType(); - arrayType.setType("array"); - ThingModelArraySpecs arraySpecs = new ThingModelArraySpecs(); - ThingModelTextType textType = new ThingModelTextType(); - textType.setType("text"); - arraySpecs.setItem(textType); - arrayType.setSpecs(arraySpecs); - inputArg.setDataType(arrayType); - - service.setInputData(Collections.singletonList(inputArg)); - service.setOutputData(outputData); - return service; - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java new file mode 100644 index 000000000..01a6dba93 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/util/MqttSignUtils.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.util; + +import cn.hutool.crypto.digest.HMac; +import cn.hutool.crypto.digest.HmacAlgorithm; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.nio.charset.StandardCharsets; + +/** + * MQTT 签名工具类 + * + * 提供静态方法来计算 MQTT 连接参数 + */ +public class MqttSignUtils { + + /** + * 计算 MQTT 连接参数 + * + * @param productKey 产品密钥 + * @param deviceName 设备名称 + * @param deviceSecret 设备密钥 + * @return 包含 clientId, username, password 的结果对象 + */ + public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret) { + return calculate(productKey, deviceName, deviceSecret, productKey + "." + deviceName); + } + + /** + * 计算 MQTT 连接参数 + * + * @param productKey 产品密钥 + * @param deviceName 设备名称 + * @param deviceSecret 设备密钥 + * @param clientId 客户端 ID + * @return 包含 clientId, username, password 的结果对象 + */ + public static MqttSignResult calculate(String productKey, String deviceName, String deviceSecret, String clientId) { + String username = deviceName + "&" + productKey; + // 构建签名内容 + StringBuilder signContentBuilder = new StringBuilder() + .append("clientId").append(clientId) + .append("deviceName").append(deviceName) + .append("deviceSecret").append(deviceSecret) + .append("productKey").append(productKey); + + // 使用 HMac 计算签名 + byte[] key = deviceSecret.getBytes(StandardCharsets.UTF_8); + String signContent = signContentBuilder.toString(); + HMac mac = new HMac(HmacAlgorithm.HmacSHA256, key); + String password = mac.digestHex(signContent); + + return new MqttSignResult(clientId, username, password); + } + + /** + * MQTT 签名结果类 + */ + @Getter + @AllArgsConstructor + public static class MqttSignResult { + + private final String clientId; + private final String username; + private final String password; + + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml new file mode 100644 index 000000000..932a9a862 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogMapper.xml @@ -0,0 +1,122 @@ + + + + + + CREATE STABLE IF NOT EXISTS device_log ( + ts TIMESTAMP, + id NCHAR(50), + product_key NCHAR(50), + device_name NCHAR(50), + type NCHAR(50), + identifier NCHAR(255), + content NCHAR(1024), + code INT, + report_time TIMESTAMP + ) TAGS ( + device_key NCHAR(50) + ) + + + + + + INSERT INTO device_log_${deviceKey} (ts, id, product_key, device_name, type, identifier, content, code, report_time) + USING device_log + TAGS ('${deviceKey}') + VALUES ( + NOW, + #{id}, + #{productKey}, + #{deviceName}, + #{type}, + #{identifier}, + #{content}, + #{code}, + #{reportTime} + ) + + + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml new file mode 100644 index 000000000..8404729cc --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceMapper.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml new file mode 100644 index 000000000..bdc40e833 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + CREATE STABLE product_property_${productKey} ( + ts TIMESTAMP, + report_time TIMESTAMP, + + ${field.field} ${field.type} + + (${field.length}) + + + ) + TAGS ( + device_key NCHAR(50) + ) + + + + ALTER STABLE product_property_${productKey} + ADD COLUMN ${field.field} ${field.type} + + (${field.length}) + + + + + ALTER STABLE product_property_${productKey} + MODIFY COLUMN ${field.field} ${field.type} + + (${field.length}) + + + + + ALTER STABLE product_property_${productKey} + DROP COLUMN ${field.field} + + + + INSERT INTO device_property_${device.deviceKey} + USING product_property_${device.productKey} + TAGS ('${device.deviceKey}') + (ts, report_time, + + ${@cn.hutool.core.util.StrUtil@toUnderlineCase(key)} + + ) + VALUES + (NOW, #{reportTime}, + + #{value} + + ) + + + + + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java new file mode 100644 index 000000000..38586afdd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/action/databridge/IotDataBridgeExecuteTest.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.module.iot.service.rule.action.databridge; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.databridge.config.*; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataBridgeDO; +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * {@link IotDataBridgeExecute} 实现类的测试 + * + * @author HUIHUI + */ +@Disabled // 默认禁用,需要手动启用测试 +@Slf4j +public class IotDataBridgeExecuteTest extends BaseMockitoUnitTest { + + private IotDeviceMessage message; + + @Mock + private RestTemplate restTemplate; + + @InjectMocks + private IotHttpDataBridgeExecute httpDataBridgeExecute; + + @BeforeEach + public void setUp() { + // 创建共享的测试消息 + message = IotDeviceMessage.builder().requestId("TEST-001").reportTime(LocalDateTime.now()).tenantId(1L) + .productKey("testProduct").deviceName("testDevice").deviceKey("testDeviceKey") + .type("property").identifier("temperature").data("{\"value\": 60}") + .build(); + + // 配置 RestTemplate mock 返回成功响应 + // TODO @puhui999:这个应该放到 testHttpDataBridge 里 + when(restTemplate.exchange(anyString(), any(HttpMethod.class), any(), any(Class.class))) + .thenReturn(new ResponseEntity<>("Success", HttpStatus.OK)); + } + + @Test + public void testKafkaMQDataBridge() { + // 1. 创建执行器实例 + IotKafkaMQDataBridgeExecute action = new IotKafkaMQDataBridgeExecute(); + + // 2. 创建配置 + // TODO @puhui999:可以改成链式哈。 + IotDataBridgeKafkaMQConfig config = new IotDataBridgeKafkaMQConfig(); + config.setBootstrapServers("127.0.0.1:9092"); + config.setTopic("test-topic"); + config.setSsl(false); + config.setUsername(null); + config.setPassword(null); + + // 3. 执行两次测试,验证缓存 + log.info("[testKafkaMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testKafkaMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRabbitMQDataBridge() { + // 1. 创建执行器实例 + IotRabbitMQDataBridgeExecute action = new IotRabbitMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRabbitMQConfig config = new IotDataBridgeRabbitMQConfig(); + config.setHost("localhost"); + config.setPort(5672); + config.setVirtualHost("/"); + config.setUsername("admin"); + config.setPassword("123456"); + config.setExchange("test-exchange"); + config.setRoutingKey("test-key"); + config.setQueue("test-queue"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRabbitMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRabbitMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRedisStreamMQDataBridge() { + // 1. 创建执行器实例 + IotRedisStreamMQDataBridgeExecute action = new IotRedisStreamMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRedisStreamMQConfig config = new IotDataBridgeRedisStreamMQConfig(); + config.setHost("127.0.0.1"); + config.setPort(6379); + config.setDatabase(0); + config.setPassword("123456"); + config.setTopic("test-stream"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRedisStreamMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRedisStreamMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testRocketMQDataBridge() { + // 1. 创建执行器实例 + IotRocketMQDataBridgeExecute action = new IotRocketMQDataBridgeExecute(); + + // 2. 创建配置 + IotDataBridgeRocketMQConfig config = new IotDataBridgeRocketMQConfig(); + config.setNameServer("127.0.0.1:9876"); + config.setGroup("test-group"); + config.setTopic("test-topic"); + config.setTags("test-tag"); + + // 3. 执行两次测试,验证缓存 + log.info("[testRocketMQDataBridge][第一次执行,应该会创建新的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + + log.info("[testRocketMQDataBridge][第二次执行,应该会复用缓存的 producer]"); + action.execute(message, new IotDataBridgeDO().setType(action.getType()).setConfig(config)); + } + + @Test + public void testHttpDataBridge() throws Exception { + // 创建配置 + IotDataBridgeHttpConfig config = new IotDataBridgeHttpConfig(); + config.setUrl("https://doc.iocoder.cn/"); + config.setMethod(HttpMethod.GET.name()); + + // 执行测试 + log.info("[testHttpDataBridge][执行HTTP数据桥接测试]"); + httpDataBridgeExecute.execute(message, new IotDataBridgeDO().setType(httpDataBridgeExecute.getType()).setConfig(config)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/pom.xml new file mode 100644 index 000000000..d33292527 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/pom.xml @@ -0,0 +1,27 @@ + + + + yudao-module-iot + cn.iocoder.boot + ${revision} + + + yudao-module-iot-plugin-common + yudao-module-iot-plugin-http + yudao-module-iot-plugin-mqtt + yudao-module-iot-plugin-emqx + + + 4.0.0 + + yudao-module-iot-plugins + pom + + ${project.artifactId} + + 物联网 插件 模块 + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml new file mode 100644 index 000000000..1e5a69bfa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/pom.xml @@ -0,0 +1,52 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + yudao-module-iot-plugin-common + jar + + ${project.artifactId} + + + 物联网 插件 模块 - 通用功能 + + + + + org.springframework.boot + spring-boot-starter + + + + cn.iocoder.boot + yudao-module-iot-api + ${revision} + + + + + org.springframework + spring-web + + + + + io.vertx + vertx-web + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java new file mode 100644 index 000000000..ba7d56fe6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.plugin.common.config; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; +import cn.iocoder.yudao.module.iot.plugin.common.heartbeat.IotPluginInstanceHeartbeatJob; +import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.client.RestTemplate; + +/** + * IoT 插件的通用自动配置类 + * + * @author haohao + */ +@AutoConfiguration +@EnableConfigurationProperties(IotPluginCommonProperties.class) +@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务 +public class IotPluginCommonAutoConfiguration { + + @Bean + public RestTemplate restTemplate(IotPluginCommonProperties properties) { + return new RestTemplateBuilder() + .connectTimeout(properties.getUpstreamConnectTimeout()) + .readTimeout(properties.getUpstreamReadTimeout()) + .build(); + } + + @Bean + public IotDeviceUpstreamApi deviceUpstreamApi(IotPluginCommonProperties properties, + RestTemplate restTemplate) { + return new IotDeviceUpstreamClient(properties, restTemplate); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceDownstreamServer deviceDownstreamServer(IotPluginCommonProperties properties, + IotDeviceDownstreamHandler deviceDownstreamHandler) { + return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler); + } + + @Bean(initMethod = "init", destroyMethod = "stop") + public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(IotDeviceUpstreamApi deviceDataApi, + IotDeviceDownstreamServer deviceDownstreamServer, + IotPluginCommonProperties commonProperties) { + return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer, commonProperties); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java new file mode 100644 index 000000000..03d42c288 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonProperties.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.plugin.common.config; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.time.Duration; + +/** + * IoT 插件的通用配置类 + * + * @author haohao + */ +@ConfigurationProperties(prefix = "yudao.iot.plugin.common") +@Validated +@Data +public class IotPluginCommonProperties { + + /** + * 上行连接超时的默认值 + */ + public static final Duration UPSTREAM_CONNECT_TIMEOUT_DEFAULT = Duration.ofSeconds(30); + /** + * 上行读取超时的默认值 + */ + public static final Duration UPSTREAM_READ_TIMEOUT_DEFAULT = Duration.ofSeconds(30); + + /** + * 下行端口 - 随机 + */ + public static final Integer DOWNSTREAM_PORT_RANDOM = 0; + + /** + * 上行 URL + */ + @NotEmpty(message = "上行 URL 不能为空") + private String upstreamUrl; + /** + * 上行连接超时 + */ + private Duration upstreamConnectTimeout = UPSTREAM_CONNECT_TIMEOUT_DEFAULT; + /** + * 上行读取超时 + */ + private Duration upstreamReadTimeout = UPSTREAM_READ_TIMEOUT_DEFAULT; + + /** + * 下行端口 + */ + private Integer downstreamPort = DOWNSTREAM_PORT_RANDOM; + + /** + * 插件包标识符 + */ + @NotEmpty(message = "插件包标识符不能为空") + private String pluginKey; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java new file mode 100644 index 000000000..38aba3df6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamHandler.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; + +/** + * IoT 设备下行处理器 + * + * 目的:每个 plugin 需要实现,用于处理 server 下行的指令(请求),从而实现从 server => plugin => device 的下行流程 + * + * @author 芋道源码 + */ +public interface IotDeviceDownstreamHandler { + + /** + * 调用设备服务 + * + * @param invokeReqDTO 调用设备服务的请求 + * @return 是否成功 + */ + CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO); + + /** + * 获取设备属性 + * + * @param getReqDTO 获取设备属性的请求 + * @return 是否成功 + */ + CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO); + + /** + * 设置设备属性 + * + * @param setReqDTO 设置设备属性的请求 + * @return 是否成功 + */ + CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO); + + /** + * 设置设备配置 + * + * @param setReqDTO 设置设备配置的请求 + * @return 是否成功 + */ + CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO); + + /** + * 升级设备 OTA + * + * @param upgradeReqDTO 升级设备 OTA 的请求 + * @return 是否成功 + */ + CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO); + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java new file mode 100644 index 000000000..719fdb5c3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream; + +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.*; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 设备下行服务端,接收来自 server 服务器的请求,转发给 device 设备 + * + * @author 芋道源码 + */ +@Slf4j +public class IotDeviceDownstreamServer { + + private final Vertx vertx; + private final HttpServer server; + private final IotPluginCommonProperties properties; + + public IotDeviceDownstreamServer(IotPluginCommonProperties properties, + IotDeviceDownstreamHandler deviceDownstreamHandler) { + this.properties = properties; + // 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + router.post(IotDeviceServiceInvokeVertxHandler.PATH) + .handler(new IotDeviceServiceInvokeVertxHandler(deviceDownstreamHandler)); + router.post(IotDevicePropertySetVertxHandler.PATH) + .handler(new IotDevicePropertySetVertxHandler(deviceDownstreamHandler)); + router.post(IotDevicePropertyGetVertxHandler.PATH) + .handler(new IotDevicePropertyGetVertxHandler(deviceDownstreamHandler)); + router.post(IotDeviceConfigSetVertxHandler.PATH) + .handler(new IotDeviceConfigSetVertxHandler(deviceDownstreamHandler)); + router.post(IotDeviceOtaUpgradeVertxHandler.PATH) + .handler(new IotDeviceOtaUpgradeVertxHandler(deviceDownstreamHandler)); + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + } + + /** + * 启动 HTTP 服务器 + */ + public void start() { + log.info("[start][开始启动]"); + server.listen(properties.getDownstreamPort()) + .toCompletionStage() + .toCompletableFuture() + .join(); + log.info("[start][启动完成,端口({})]", this.server.actualPort()); + } + + /** + * 停止所有 + */ + public void stop() { + log.info("[stop][开始关闭]"); + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx != null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭异常]", e); + throw new RuntimeException(e); + } + } + + /** + * 获得端口 + * + * @return 端口 + */ + public int getPort() { + return this.server.actualPort(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java new file mode 100644 index 000000000..1693f128d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceConfigSetVertxHandler.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceConfigSetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备配置设置 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceConfigSetVertxHandler implements Handler { + + // TODO @haohao:是不是可以把 PATH、Method 所有的,抽到一个枚举类里?因为 topic、path、method 相当于不同的几个表达? + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/config/set"; + public static final String METHOD = "thing.service.config.set"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceConfigSetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map config = (Map) body.getMap().get("config"); + reqDTO = ((IotDeviceConfigSetReqDTO) new IotDeviceConfigSetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setConfig(config); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.setDeviceConfig(reqDTO); + + // 3. 响应结果 + IotStandardResponse response = result.isSuccess() ? + IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) + : IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 配置设置异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java new file mode 100644 index 000000000..b417229aa --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceOtaUpgradeVertxHandler.java @@ -0,0 +1,78 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceOtaUpgradeReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备 OTA 升级 Vertx Handler + *

+ * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceOtaUpgradeVertxHandler implements Handler { + + public static final String PATH = "/ota/:productKey/:deviceName/upgrade"; + public static final String METHOD = "ota.device.upgrade"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceOtaUpgradeReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Long firmwareId = body.getLong("firmwareId"); + String version = body.getString("version"); + String signMethod = body.getString("signMethod"); + String fileSign = body.getString("fileSign"); + Long fileSize = body.getLong("fileSize"); + String fileUrl = body.getString("fileUrl"); + String information = body.getString("information"); + reqDTO = ((IotDeviceOtaUpgradeReqDTO) new IotDeviceOtaUpgradeReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setFirmwareId(firmwareId).setVersion(version) + .setSignMethod(signMethod).setFileSign(fileSign).setFileSize(fileSize).setFileUrl(fileUrl) + .setInformation(information); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.upgradeDeviceOta(reqDTO); + + // 3. 响应结果 + // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, CommonResult) + IotStandardResponse response = result.isSuccess() ? + IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()) + :IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) OTA 升级异常]", reqDTO, e); + // TODO @haohao:可以考虑 IotStandardResponse.of(requestId, method, ErrorCode) + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java new file mode 100644 index 000000000..3cb4bc941 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertyGetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备服务获取 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDevicePropertyGetVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/get"; + public static final String METHOD = "thing.service.property.get"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDevicePropertyGetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + List identifiers = (List) body.getMap().get("identifiers"); + reqDTO = ((IotDevicePropertyGetReqDTO) new IotDevicePropertyGetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setIdentifiers(identifiers); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.getDeviceProperty(reqDTO); + + // 3. 响应结果 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 属性获取异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java new file mode 100644 index 000000000..251be1eb9 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertySetReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设置设备属性 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDevicePropertySetVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/set"; + public static final String METHOD = "thing.service.property.set"; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDevicePropertySetReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map properties = (Map) body.getMap().get("properties"); + reqDTO = ((IotDevicePropertySetReqDTO) new IotDevicePropertySetReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setProperties(properties); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + IotStandardResponse errorResponse = IotStandardResponse.error( + null, METHOD, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.setDeviceProperty(reqDTO); + + // 3. 响应结果 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), METHOD, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), METHOD, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 属性设置异常]", reqDTO, e); + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), METHOD, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java new file mode 100644 index 000000000..534823f75 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceServiceInvokeReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备服务调用 Vertx Handler + * + * 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class IotDeviceServiceInvokeVertxHandler implements Handler { + + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/:identifier"; + public static final String METHOD_PREFIX = "thing.service."; + public static final String METHOD_SUFFIX = ""; + + private final IotDeviceDownstreamHandler deviceDownstreamHandler; + + @Override + @SuppressWarnings("unchecked") + public void handle(RoutingContext routingContext) { + // 1. 解析参数 + IotDeviceServiceInvokeReqDTO reqDTO; + try { + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + String identifier = routingContext.pathParam("identifier"); + JsonObject body = routingContext.body().asJsonObject(); + String requestId = body.getString("requestId"); + Map params = (Map) body.getMap().get("params"); + reqDTO = ((IotDeviceServiceInvokeReqDTO) new IotDeviceServiceInvokeReqDTO() + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) + .setIdentifier(identifier).setParams(params); + } catch (Exception e) { + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); + String method = METHOD_PREFIX + routingContext.pathParam("identifier") + METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error( + null, method, BAD_REQUEST.getCode(), BAD_REQUEST.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 2. 调用处理器 + try { + CommonResult result = deviceDownstreamHandler.invokeDeviceService(reqDTO); + + // 3. 响应结果 + String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(reqDTO.getRequestId(), method, result.getData()); + } else { + response = IotStandardResponse.error(reqDTO.getRequestId(), method, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][请求参数({}) 服务调用异常]", reqDTO, e); + String method = METHOD_PREFIX + reqDTO.getIdentifier() + METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error( + reqDTO.getRequestId(), method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java new file mode 100644 index 000000000..f272468c5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeat/IotPluginInstanceHeartbeatJob.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.iot.plugin.common.heartbeat; + +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.concurrent.TimeUnit; + +/** + * IoT 插件实例心跳 Job + * + * 用于定时发送心跳给服务端 + */ +@RequiredArgsConstructor +@Slf4j +public class IotPluginInstanceHeartbeatJob { + + private final IotDeviceUpstreamApi deviceUpstreamApi; + private final IotDeviceDownstreamServer deviceDownstreamServer; + private final IotPluginCommonProperties commonProperties; + + public void init() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); + log.info("[init][上线结果:{})]", result); + } + + public void stop() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(false)); + log.info("[stop][下线结果:{})]", result); + } + + @Scheduled(initialDelay = 3, fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3 分钟执行一次 + public void execute() { + CommonResult result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true)); + log.info("[execute][心跳结果:{})]", result); + } + + private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) { + return new IotPluginInstanceHeartbeatReqDTO() + .setPluginKey(commonProperties.getPluginKey()).setProcessId(IotPluginCommonUtils.getProcessId()) + .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort()) + .setOnline(online); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java new file mode 100644 index 000000000..83b5bb58a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/package-info.java @@ -0,0 +1,2 @@ +// TODO @芋艿:注释 +package cn.iocoder.yudao.module.iot.plugin.common; \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java new file mode 100644 index 000000000..131eb1b9c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/pojo/IotStandardResponse.java @@ -0,0 +1,94 @@ +package cn.iocoder.yudao.module.iot.plugin.common.pojo; + +import lombok.Data; + +// TODO @芋艿:1)后续考虑,要不要叫 IoT 网关之类的 Response;2)包名 pojo +/** + * IoT 标准协议响应实体类 + *

+ * 用于统一 MQTT 和 HTTP 的响应格式 + * + * @author haohao + */ +@Data +public class IotStandardResponse { + + /** + * 消息ID + */ + private String id; + + /** + * 状态码 + */ + private Integer code; + + /** + * 响应数据 + */ + private Object data; + + /** + * 响应消息 + */ + private String message; + + /** + * 方法名 + */ + private String method; + + /** + * 协议版本 + */ + private String version; + + /** + * 创建成功响应 + * + * @param id 消息ID + * @param method 方法名 + * @return 成功响应 + */ + public static IotStandardResponse success(String id, String method) { + return success(id, method, null); + } + + /** + * 创建成功响应 + * + * @param id 消息ID + * @param method 方法名 + * @param data 响应数据 + * @return 成功响应 + */ + public static IotStandardResponse success(String id, String method, Object data) { + return new IotStandardResponse() + .setId(id) + .setCode(200) + .setData(data) + .setMessage("success") + .setMethod(method) + .setVersion("1.0"); + } + + /** + * 创建错误响应 + * + * @param id 消息ID + * @param method 方法名 + * @param code 错误码 + * @param message 错误消息 + * @return 错误响应 + */ + public static IotStandardResponse error(String id, String method, Integer code, String message) { + return new IotStandardResponse() + .setId(id) + .setCode(code) + .setData(null) + .setMessage(message) + .setMethod(method) + .setVersion("1.0"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java new file mode 100644 index 000000000..1bf4d676c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.iot.plugin.common.upstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.client.RestTemplate; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * 设备数据 Upstream 上行客户端 + * + * 通过 HTTP 调用远程的 IotDeviceUpstreamApi 接口 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { + + public static final String URL_PREFIX = "/rpc-api/iot/device/upstream"; + + private final IotPluginCommonProperties properties; + + private final RestTemplate restTemplate; + + @Override + public CommonResult updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/update-state"; + return doPost(url, updateReqDTO); + } + + @Override + public CommonResult reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-event"; + return doPost(url, reportReqDTO); + } + + // TODO @芋艿:待实现 + @Override + public CommonResult registerDevice(IotDeviceRegisterReqDTO registerReqDTO) { + return null; + } + + // TODO @芋艿:待实现 + @Override + public CommonResult registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) { + return null; + } + + // TODO @芋艿:待实现 + @Override + public CommonResult addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) { + return null; + } + + @Override + public CommonResult authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/authenticate-emqx-connection"; + return doPost(url, authReqDTO); + } + + @Override + public CommonResult reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/report-property"; + return doPost(url, reportReqDTO); + } + + @Override + public CommonResult heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) { + String url = properties.getUpstreamUrl() + URL_PREFIX + "/heartbeat-plugin-instance"; + return doPost(url, heartbeatReqDTO); + } + + @SuppressWarnings("unchecked") + private CommonResult doPost(String url, T requestBody) { + try { + CommonResult result = restTemplate.postForObject(url, requestBody, + (Class>) (Class) CommonResult.class); + log.info("[doPost][url({}) requestBody({}) result({})]", url, requestBody, result); + return result; + } catch (Exception e) { + log.error("[doPost][url({}) requestBody({}) 发生异常]", url, requestBody, e); + return CommonResult.error(INTERNAL_SERVER_ERROR); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java new file mode 100644 index 000000000..34c6c0fe2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/util/IotPluginCommonUtils.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.module.iot.plugin.common.util; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; +import org.springframework.http.MediaType; + +/** + * IoT 插件的通用工具类 + * + * @author 芋道源码 + */ +public class IotPluginCommonUtils { + + /** + * 流程实例的进程编号 + */ + private static String processId; + + public static String getProcessId() { + if (StrUtil.isEmpty(processId)) { + initProcessId(); + } + return processId; + } + + private synchronized static void initProcessId() { + processId = String.format("%s@%d@%s", // IP@PID@${uuid} + SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID()); + } + + /** + * 将对象转换为JSON字符串后写入HTTP响应 + * + * @param routingContext 路由上下文 + * @param data 数据对象 + */ + @SuppressWarnings("deprecation") + public static void writeJsonResponse(RoutingContext routingContext, Object data) { + routingContext.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(data)); + } + + /** + * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse) + *

+ * 推荐使用此方法,统一MQTT和HTTP的响应格式。使用方式: + * + *

+     * // 成功响应
+     * IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
+     * IotPluginCommonUtils.writeJsonResponse(routingContext, response);
+     *
+     * // 错误响应
+     * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
+     * IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
+     * 
+ * + * @param routingContext 路由上下文 + * @param response IotStandardResponse响应对象 + */ + @SuppressWarnings("deprecation") + public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) { + routingContext.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) + .end(JsonUtils.toJsonString(response)); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..eae9ad882 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.module.iot.plugin.common.config.IotPluginCommonAutoConfiguration \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties new file mode 100644 index 000000000..565e81eb0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/plugin.properties @@ -0,0 +1,6 @@ +plugin.id=yudao-module-iot-plugin-emqx +plugin.class=cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin +plugin.version=1.0.0 +plugin.provider=yudao +plugin.dependencies= +plugin.description=yudao-module-iot-plugin-emqx-1.0.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml new file mode 100644 index 000000000..8620ecaa6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/pom.xml @@ -0,0 +1,169 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-emqx + 1.0.0 + + ${project.artifactId} + + + 物联网 插件模块 - emqx 插件 + + + + + emqx-plugin + cn.iocoder.yudao.module.iot.plugin.emqx.config.IotEmqxPlugin + ${project.version} + yudao + ${project.artifactId}-${project.version} + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + -standalone + + + + + + + + + + + cn.iocoder.boot + yudao-module-iot-plugin-common + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.vertx + vertx-web + + + io.vertx + vertx-mqtt + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml new file mode 100644 index 000000000..daec9e431 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/assembly/assembly.xml @@ -0,0 +1,31 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java new file mode 100644 index 000000000..178038417 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/IotEmqxPluginApplication.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * IoT Emqx 插件的独立运行入口 + */ +@Slf4j +@SpringBootApplication +public class IotEmqxPluginApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(IotEmqxPluginApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.run(args); + log.info("[main][独立模式启动完成]"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java new file mode 100644 index 000000000..275c20eb1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotEmqxPlugin.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPlugin; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * EMQX 插件实现类 + * + * 基于 PF4J 插件框架,实现 EMQX 消息中间件的集成:负责插件的生命周期管理,包括启动、停止和应用上下文的创建 + * + * @author haohao + */ +@Slf4j +public class IotEmqxPlugin extends SpringPlugin { + + public IotEmqxPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("[EmqxPlugin][EmqxPlugin 插件启动开始...]"); + try { + log.info("[EmqxPlugin][EmqxPlugin 插件启动成功...]"); + } catch (Exception e) { + log.error("[EmqxPlugin][EmqxPlugin 插件开启动异常...]", e); + } + } + + @Override + public void stop() { + log.info("[EmqxPlugin][EmqxPlugin 插件停止开始...]"); + try { + log.info("[EmqxPlugin][EmqxPlugin 插件停止成功...]"); + } catch (Exception e) { + log.error("[EmqxPlugin][EmqxPlugin 插件停止异常...]", e); + } + } + + @Override + protected ApplicationContext createApplicationContext() { + // 创建插件自己的 ApplicationContext + AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); + // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) + pluginContext.setParent(SpringUtil.getApplicationContext()); + // 继续使用插件自己的 ClassLoader 以加载插件内部的类 + pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); + // 扫描当前插件的自动配置包 + // TODO @芋艿:是不是要配置下包 + pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.emqx.config"); + pluginContext.refresh(); + return pluginContext; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java new file mode 100644 index 000000000..e1d11504c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxAutoConfiguration.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import cn.hutool.core.util.IdUtil; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.downstream.IotDeviceDownstreamHandlerImpl; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.IotDeviceUpstreamServer; +import io.vertx.core.Vertx; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.MqttClientOptions; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * IoT 插件 EMQX 的专用自动配置类 + * + * @author haohao + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(IotPluginEmqxProperties.class) +public class IotPluginEmqxAutoConfiguration { + + @Bean + public Vertx vertx() { + return Vertx.vertx(); + } + + @Bean + public MqttClient mqttClient(Vertx vertx, IotPluginEmqxProperties emqxProperties) { + MqttClientOptions options = new MqttClientOptions() + .setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID()) + .setUsername(emqxProperties.getMqttUsername()) + .setPassword(emqxProperties.getMqttPassword()) + .setSsl(emqxProperties.getMqttSsl()); + return MqttClient.create(vertx, options); + } + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, + IotPluginEmqxProperties emqxProperties, + Vertx vertx, + MqttClient mqttClient) { + return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient); + } + + @Bean + public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) { + return new IotDeviceDownstreamHandlerImpl(mqttClient); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java new file mode 100644 index 000000000..219fe0360 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/config/IotPluginEmqxProperties.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * 物联网插件 - EMQX 配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yudao.iot.plugin.emqx") +@Validated +@Data +public class IotPluginEmqxProperties { + + // TODO @haohao:参数校验,加下,啊哈 + + /** + * 服务主机 + */ + private String mqttHost; + /** + * 服务端口 + */ + private Integer mqttPort; + /** + * 服务用户名 + */ + private String mqttUsername; + /** + * 服务密码 + */ + private String mqttPassword; + /** + * 是否启用 SSL + */ + private Boolean mqttSsl; + + /** + * 订阅的主题列表 + */ + private String[] mqttTopics; + + /** + * 认证端口 + */ + private Integer authPort; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java new file mode 100644 index 000000000..f5c19224a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/downstream/IotDeviceDownstreamHandlerImpl.java @@ -0,0 +1,176 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.downstream; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL; + +/** + * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { + + private static final String SYS_TOPIC_PREFIX = "/sys/"; + + // TODO @haohao:是不是可以类似 IotDeviceConfigSetVertxHandler 的建议,抽到统一的枚举类 + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈。;回复 都使用 Alink 格式,方便后续扩展。 + // 设备服务调用 标准 JSON + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier} + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply + private static final String SERVICE_TOPIC_PREFIX = "/thing/service/"; + + // 设置设备属性 标准 JSON + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply + private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set"; + + private final MqttClient mqttClient; + + /** + * 构造函数 + * + * @param mqttClient MQTT客户端 + */ + public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) { + this.mqttClient = mqttClient; + } + + @Override + public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) { + log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + + // 验证参数 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) { + log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + + try { + // 构建请求主题 + String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier()); + // 构建请求消息 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); + JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams()); + // 发送消息 + publishMessage(topic, request); + + log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic); + return CommonResult.success(true); + } catch (Exception e) { + log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + } + + @Override + public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { + return CommonResult.success(true); + } + + @Override + public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) { + // 验证参数 + log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) { + log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO)); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + + try { + // 构建请求主题 + String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName()); + // 构建请求消息 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId(); + JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties()); + // 发送消息 + publishMessage(topic, request); + + log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic); + return CommonResult.success(true); + } catch (Exception e) { + log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e); + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg()); + } + } + + @Override + public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { + return CommonResult.success(true); + } + + @Override + public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { + return CommonResult.success(true); + } + + /** + * 构建服务调用主题 + */ + private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) { + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier; + } + + /** + * 构建属性设置主题 + */ + private String buildPropertySetTopic(String productKey, String deviceName) { + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC; + } + + // TODO @haohao:这个,后面搞个对象,会不会好点哈? + /** + * 构建服务调用请求 + */ + private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map params) { + return new JSONObject() + .set("id", requestId) + .set("version", "1.0") + .set("method", "thing.service." + serviceIdentifier) + .set("params", params != null ? params : new JSONObject()); + } + + /** + * 构建属性设置请求 + */ + private JSONObject buildPropertySetRequest(String requestId, Map properties) { + return new JSONObject() + .set("id", requestId) + .set("version", "1.0") + .set("method", "thing.service.property.set") + .set("params", properties); + } + + /** + * 发布 MQTT 消息 + */ + private void publishMessage(String topic, JSONObject payload) { + mqttClient.publish( + topic, + Buffer.buffer(payload.toString()), + MqttQoS.AT_LEAST_ONCE, + false, + false); + log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload); + } + + /** + * 生成请求 ID + */ + private String generateRequestId() { + return IdUtil.fastSimpleUUID(); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java new file mode 100644 index 000000000..00792ebcf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/IotDeviceUpstreamServer.java @@ -0,0 +1,236 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.emqx.config.IotPluginEmqxProperties; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceAuthVertxHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceMqttMessageHandler; +import cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router.IotDeviceWebhookVertxHandler; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.mqtt.MqttClient; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 + *

+ * 协议:HTTP、MQTT + * + * @author haohao + */ +@Slf4j +public class IotDeviceUpstreamServer { + + /** + * 重连延迟时间(毫秒) + */ + private static final int RECONNECT_DELAY_MS = 5000; + /** + * 连接超时时间(毫秒) + */ + private static final int CONNECTION_TIMEOUT_MS = 10000; + /** + * 默认 QoS 级别 + */ + private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; + + private final Vertx vertx; + private final HttpServer server; + private final MqttClient client; + private final IotPluginEmqxProperties emqxProperties; + private final IotDeviceMqttMessageHandler mqttMessageHandler; + + /** + * 服务运行状态标志 + */ + private volatile boolean isRunning = false; + + public IotDeviceUpstreamServer(IotPluginEmqxProperties emqxProperties, + IotDeviceUpstreamApi deviceUpstreamApi, + Vertx vertx, + MqttClient client) { + this.vertx = vertx; + this.emqxProperties = emqxProperties; + this.client = client; + + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + router.post(IotDeviceAuthVertxHandler.PATH) + // TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀? + // 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式 + .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi)); + // 添加 Webhook 处理器,用于处理设备连接和断开连接事件 + router.post(IotDeviceWebhookVertxHandler.PATH) + .handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi)); + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client); + } + + /** + * 启动 HTTP 服务器、MQTT 客户端 + */ + public void start() { + if (isRunning) { + log.warn("[start][服务已经在运行中,请勿重复启动]"); + return; + } + log.info("[start][开始启动服务]"); + + // TODO @haohao:建议先启动 MQTT Broker,再启动 HTTP Server。类似 jdbc 先连接了,在启动 tomcat 的味道 + // 1. 启动 HTTP 服务器 + CompletableFuture httpFuture = server.listen(emqxProperties.getAuthPort()) + .toCompletionStage() + .toCompletableFuture() + .thenAccept(v -> log.info("[start][HTTP 服务器启动完成,端口: {}]", server.actualPort())); + + // 2. 连接 MQTT Broker + CompletableFuture mqttFuture = connectMqtt() + .toCompletionStage() + .toCompletableFuture() + .thenAccept(v -> { + // 2.1 添加 MQTT 断开重连监听器 + client.closeHandler(closeEvent -> { + log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); + reconnectWithDelay(); + }); + // 2.2 设置 MQTT 消息处理器 + setupMessageHandler(); + }); + + // 3. 等待所有服务启动完成 + CompletableFuture.allOf(httpFuture, mqttFuture) + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS) // TODO @芋艿:JDK8 不兼容 + .whenComplete((result, error) -> { + if (error != null) { + log.error("[start][服务启动失败]", error); + } else { + isRunning = true; + log.info("[start][所有服务启动完成]"); + } + }); + } + + /** + * 设置 MQTT 消息处理器 + */ + private void setupMessageHandler() { + client.publishHandler(mqttMessageHandler::handle); + log.debug("[setupMessageHandler][MQTT 消息处理器设置完成]"); + } + + /** + * 重连 MQTT 客户端 + */ + private void reconnectWithDelay() { + if (!isRunning) { + log.info("[reconnectWithDelay][服务已停止,不再尝试重连]"); + return; + } + + vertx.setTimer(RECONNECT_DELAY_MS, id -> { + log.info("[reconnectWithDelay][开始重新连接 MQTT]"); + connectMqtt(); + }); + } + + /** + * 连接 MQTT Broker 并订阅主题 + * + * @return 连接结果的Future + */ + private Future connectMqtt() { + return client.connect(emqxProperties.getMqttPort(), emqxProperties.getMqttHost()) + .compose(connAck -> { + log.info("[connectMqtt][MQTT客户端连接成功]"); + return subscribeToTopics(); + }) + .recover(error -> { + log.error("[connectMqtt][连接MQTT Broker失败:]", error); + reconnectWithDelay(); + return Future.failedFuture(error); + }); + } + + /** + * 订阅设备上行消息主题 + * + * @return 订阅结果的 Future + */ + private Future subscribeToTopics() { + String[] topics = emqxProperties.getMqttTopics(); + if (ArrayUtil.isEmpty(topics)) { + log.warn("[subscribeToTopics][未配置MQTT主题,跳过订阅]"); + return Future.succeededFuture(); + } + log.info("[subscribeToTopics][开始订阅设备上行消息主题]"); + + Future compositeFuture = Future.succeededFuture(); + for (String topic : topics) { + String trimmedTopic = topic.trim(); + if (trimmedTopic.isEmpty()) { + continue; + } + compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value()) + .map(ack -> { + log.info("[subscribeToTopics][成功订阅主题: {}]", trimmedTopic); + return null; + }) + .recover(error -> { + log.error("[subscribeToTopics][订阅主题失败: {}]", trimmedTopic, error); + return Future.succeededFuture(); // 继续订阅其他主题 + })); + } + return compositeFuture; + } + + /** + * 停止所有服务 + */ + public void stop() { + if (!isRunning) { + log.warn("[stop][服务未运行,无需停止]"); + return; + } + log.info("[stop][开始关闭服务]"); + isRunning = false; + + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 MQTT 客户端 + if (client != null) { + client.disconnect() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx!= null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭服务异常]", e); + throw new RuntimeException("关闭 IoT 设备上行服务失败", e); + } + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java new file mode 100644 index 000000000..e9206d5b6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceAuthVertxHandler.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; + +/** + * IoT EMQX 连接认证的 Vert.x Handler + * + * 参考:EMQX HTTP + * + * 注意:该处理器需要返回特定格式:{"result": "allow"} 或 {"result": "deny"}, + * 以符合 EMQX 认证插件的要求,因此不使用 IotStandardResponse 实体类 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceAuthVertxHandler implements Handler { + + public static final String PATH = "/mqtt/auth"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + @Override + public void handle(RoutingContext routingContext) { + try { + // 构建认证请求 DTO + JsonObject json = routingContext.body().asJsonObject(); + String clientId = json.getString("clientid"); + String username = json.getString("username"); + String password = json.getString("password"); + IotDeviceEmqxAuthReqDTO authReqDTO = new IotDeviceEmqxAuthReqDTO() + .setClientId(clientId) + .setUsername(username) + .setPassword(password); + + // 调用认证 API + CommonResult authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO); + if (authResult.getCode() != 0 || !authResult.getData()) { + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); + return; + } + + // 响应结果 + // 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow")); + } catch (Exception e) { + log.error("[handle][EMQX 认证异常]", e); + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny")); + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java new file mode 100644 index 000000000..00fa1b96d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceMqttMessageHandler.java @@ -0,0 +1,296 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttClient; +import io.vertx.mqtt.messages.MqttPublishMessage; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * IoT 设备 MQTT 消息处理器 + * + * 参考:设备属性、事件、服务 + */ +@Slf4j +public class IotDeviceMqttMessageHandler { + + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈;回复 都使用 Alink 格式,方便后续扩展。 + // 设备上报属性 标准 JSON + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply + + // 设备上报事件 标准 JSON + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply + + private static final String SYS_TOPIC_PREFIX = "/sys/"; + private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post"; + private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/"; + private static final String EVENT_POST_TOPIC_SUFFIX = "/post"; + private static final String REPLY_SUFFIX = "_reply"; + private static final String PROPERTY_METHOD = "thing.event.property.post"; + private static final String EVENT_METHOD_PREFIX = "thing.event."; + private static final String EVENT_METHOD_SUFFIX = ".post"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + private final MqttClient mqttClient; + + public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) { + this.deviceUpstreamApi = deviceUpstreamApi; + this.mqttClient = mqttClient; + } + + /** + * 处理MQTT消息 + * + * @param message MQTT发布消息 + */ + public void handle(MqttPublishMessage message) { + String topic = message.topicName(); + String payload = message.payload().toString(); + log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload); + + try { + if (StrUtil.isEmpty(payload)) { + log.warn("[messageHandler][消息内容为空][topic: {}]", topic); + return; + } + handleMessage(topic, payload); + } catch (Exception e) { + log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 根据主题类型处理消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handleMessage(String topic, String payload) { + // 校验前缀 + if (!topic.startsWith(SYS_TOPIC_PREFIX)) { + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); + return; + } + + // 处理设备属性上报消息 + if (topic.endsWith(PROPERTY_POST_TOPIC)) { + log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic); + handlePropertyPost(topic, payload); + return; + } + + // 处理设备事件上报消息 + if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) { + log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic); + handleEventPost(topic, payload); + return; + } + + // 未知消息类型 + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic); + } + + /** + * 处理设备属性上报消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handlePropertyPost(String topic, String payload) { + try { + // 解析消息内容 + JSONObject jsonObject = JSONUtil.parseObj(payload); + String[] topicParts = parseTopic(topic); + if (topicParts == null) { + return; + } + + // 构建设备属性上报请求对象 + IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts); + + // 调用上游 API 处理设备上报数据 + deviceUpstreamApi.reportDeviceProperty(reportReqDTO); + log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic); + + // 发送响应消息 + sendResponse(topic, jsonObject, PROPERTY_METHOD, null); + } catch (Exception e) { + log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 处理设备事件上报消息 + * + * @param topic 主题 + * @param payload 消息内容 + */ + private void handleEventPost(String topic, String payload) { + try { + // 解析消息内容 + JSONObject jsonObject = JSONUtil.parseObj(payload); + String[] topicParts = parseTopic(topic); + if (topicParts == null) { + return; + } + + // 构建设备事件上报请求对象 + IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts); + + // 调用上游 API 处理设备上报数据 + deviceUpstreamApi.reportDeviceEvent(reportReqDTO); + log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic); + + // 从 topic 中获取事件标识符 + String eventIdentifier = getEventIdentifier(topicParts, topic); + if (eventIdentifier == null) { + return; + } + + // 发送响应消息 + String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX; + sendResponse(topic, jsonObject, method, null); + } catch (Exception e) { + log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e); + } + } + + /** + * 解析主题,获取主题各部分 + * + * @param topic 主题 + * @return 主题各部分数组,如果解析失败返回null + */ + private String[] parseTopic(String topic) { + String[] topicParts = topic.split("/"); + if (topicParts.length < 7) { + log.warn("[parseTopic][主题格式不正确][topic: {}]", topic); + return null; + } + return topicParts; + } + + /** + * 从主题部分中获取事件标识符 + * + * @param topicParts 主题各部分 + * @param topic 原始主题,用于日志 + * @return 事件标识符,如果获取失败返回null + */ + private String getEventIdentifier(String[] topicParts, String topic) { + try { + return topicParts[6]; + } catch (ArrayIndexOutOfBoundsException e) { + log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]", + topic, Arrays.toString(topicParts)); + return null; + } + } + + /** + * 发送响应消息 + * + * @param topic 原始主题 + * @param jsonObject 原始消息JSON对象 + * @param method 响应方法 + * @param customData 自定义数据,可为 null + */ + private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) { + String replyTopic = topic + REPLY_SUFFIX; + + // 响应结果 + IotStandardResponse response = IotStandardResponse.success( + jsonObject.getStr("id"), method, customData); + try { + mqttClient.publish(replyTopic, Buffer.buffer(JsonUtils.toJsonString(response)), + MqttQoS.AT_LEAST_ONCE, false, false); + log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic); + } catch (Exception e) { + log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]", replyTopic, response, e); + } + } + + /** + * 构建设备属性上报请求对象 + * + * @param jsonObject 消息内容 + * @param topicParts 主题部分 + * @return 设备属性上报请求对象 + */ + private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) { + IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO(); + reportReqDTO.setRequestId(jsonObject.getStr("id")); + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + reportReqDTO.setReportTime(LocalDateTime.now()); + reportReqDTO.setProductKey(topicParts[2]); + reportReqDTO.setDeviceName(topicParts[3]); + + // 只使用标准JSON格式处理属性数据 + JSONObject params = jsonObject.getJSONObject("params"); + if (params == null) { + log.warn("[buildPropertyReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); + params = new JSONObject(); + } + + // 将标准格式的params转换为平台需要的properties格式 + Map properties = new HashMap<>(); + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object valueObj = entry.getValue(); + + // 如果是复杂结构(包含value和time) + if (valueObj instanceof JSONObject valueJson) { + properties.put(key, valueJson.getOrDefault("value", valueObj)); + } else { + properties.put(key, valueObj); + } + } + reportReqDTO.setProperties(properties); + + return reportReqDTO; + } + + /** + * 构建设备事件上报请求对象 + * + * @param jsonObject 消息内容 + * @param topicParts 主题部分 + * @return 设备事件上报请求对象 + */ + private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) { + IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO(); + reportReqDTO.setRequestId(jsonObject.getStr("id")); + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + reportReqDTO.setReportTime(LocalDateTime.now()); + reportReqDTO.setProductKey(topicParts[2]); + reportReqDTO.setDeviceName(topicParts[3]); + reportReqDTO.setIdentifier(topicParts[6]); + + // 只使用标准JSON格式处理事件参数 + JSONObject params = jsonObject.getJSONObject("params"); + if (params == null) { + log.warn("[buildEventReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject); + params = new JSONObject(); + } + reportReqDTO.setParams(params); + + return reportReqDTO; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java new file mode 100644 index 000000000..21b49e097 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/java/cn/iocoder/yudao/module/iot/plugin/emqx/upstream/router/IotDeviceWebhookVertxHandler.java @@ -0,0 +1,152 @@ +package cn.iocoder.yudao.module.iot.plugin.emqx.upstream.router; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Collections; + +/** + * IoT EMQX Webhook 事件处理的 Vert.x Handler + * + * 参考:EMQX Webhook + * + * 注意:该处理器需要返回特定格式:{"result": "success"} 或 {"result": "error"}, + * 以符合 EMQX Webhook 插件的要求,因此不使用 IotStandardResponse 实体类。 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceWebhookVertxHandler implements Handler { + + public static final String PATH = "/mqtt/webhook"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + @Override + public void handle(RoutingContext routingContext) { + try { + // 解析请求体 + JsonObject json = routingContext.body().asJsonObject(); + String event = json.getString("event"); + String clientId = json.getString("clientid"); + String username = json.getString("username"); + + // 处理不同的事件类型 + switch (event) { + case "client.connected": + handleClientConnected(clientId, username); + break; + case "client.disconnected": + handleClientDisconnected(clientId, username); + break; + default: + log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, username); + break; + } + + // 返回成功响应 + // 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success")); + } catch (Exception e) { + log.error("[handle][处理 Webhook 事件异常]", e); + // 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error")); + } + } + + /** + * 处理客户端连接事件 + * + * @param clientId 客户端ID + * @param username 用户名 + */ + private void handleClientConnected(String clientId, String username) { + // 解析产品标识和设备名称 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { + log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId); + return; + } + String[] parts = parseUsername(username); + if (parts == null) { + return; + } + + // 更新设备状态为在线 + IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO(); + updateReqDTO.setProductKey(parts[1]); + updateReqDTO.setDeviceName(parts[0]); + updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState()); + updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + updateReqDTO.setReportTime(LocalDateTime.now()); + CommonResult result = deviceUpstreamApi.updateDeviceState(updateReqDTO); + if (result.getCode() != 0 || !result.getData()) { + log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}", + clientId, username, result.getCode(), result.getMsg()); + } else { + log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username); + } + } + + /** + * 处理客户端断开连接事件 + * + * @param clientId 客户端ID + * @param username 用户名 + */ + private void handleClientDisconnected(String clientId, String username) { + // 解析产品标识和设备名称 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) { + log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId); + return; + } + String[] parts = parseUsername(username); + if (parts == null) { + return; + } + + // 更新设备状态为离线 + IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO(); + offlineReqDTO.setProductKey(parts[1]); + offlineReqDTO.setDeviceName(parts[0]); + offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState()); + offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId()); + offlineReqDTO.setReportTime(LocalDateTime.now()); + CommonResult offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO); + if (offlineResult.getCode() != 0 || !offlineResult.getData()) { + log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}", + clientId, username, offlineResult.getCode(), offlineResult.getMsg()); + } else { + log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username); + } + } + + /** + * 解析用户名,格式为 deviceName&productKey + * + * @param username 用户名 + * @return 解析结果,[0] 为 deviceName,[1] 为 productKey,解析失败返回 null + */ + private String[] parseUsername(String username) { + if (StrUtil.isEmpty(username)) { + return null; + } + String[] parts = username.split("&"); + if (parts.length != 2) { + log.warn("[parseUsername][用户名格式({})不正确,无法解析产品标识和设备名称]", username); + return null; + } + return parts; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml new file mode 100644 index 000000000..c00621c82 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-emqx/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + application: + name: yudao-module-iot-plugin-emqx + +yudao: + iot: + plugin: + common: + upstream-url: http://127.0.0.1:48080 + downstream-port: 8100 + plugin-key: yudao-module-iot-plugin-emqx + emqx: + mqtt-host: 127.0.0.1 + mqtt-port: 1883 + mqtt-ssl: false + mqtt-username: yudao + mqtt-password: 123456 + mqtt-topics: + - "/sys/#" + auth-port: 8101 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties new file mode 100644 index 000000000..647d55155 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/plugin.properties @@ -0,0 +1,6 @@ +plugin.id=yudao-module-iot-plugin-http +plugin.class=cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin +plugin.version=1.0.0 +plugin.provider=yudao +plugin.dependencies= +plugin.description=yudao-module-iot-plugin-http-1.0.0 \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml new file mode 100644 index 000000000..88a413ca6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/pom.xml @@ -0,0 +1,165 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-http + 1.0.0 + + ${project.artifactId} + + + 物联网 插件模块 - http 插件 + + + + + ${project.artifactId} + cn.iocoder.yudao.module.iot.plugin.http.config.IotHttpVertxPlugin + ${project.version} + yudao + ${project.artifactId}-${project.version} + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + + repackage + + + -standalone + + + + + + + + + + + cn.iocoder.boot + yudao-module-iot-plugin-common + ${revision} + + + + + org.springframework.boot + spring-boot-starter-web + + + + + io.vertx + vertx-web + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml new file mode 100644 index 000000000..9b79e6152 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/assembly/assembly.xml @@ -0,0 +1,24 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java new file mode 100644 index 000000000..a88b34eb3 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/IotHttpPluginApplication.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.iot.plugin.http; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 独立运行入口 + */ +@Slf4j +@SpringBootApplication +public class IotHttpPluginApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(IotHttpPluginApplication.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.run(args); + log.info("[main][独立模式启动完成]"); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java new file mode 100644 index 000000000..f704c1844 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotHttpVertxPlugin.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import cn.hutool.core.lang.Assert; +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.PluginWrapper; +import org.pf4j.spring.SpringPlugin; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +// TODO @芋艿:完善注释 +/** + * 负责插件的启动和停止,与 Vert.x 的生命周期管理 + */ +@Slf4j +public class IotHttpVertxPlugin extends SpringPlugin { + + public IotHttpVertxPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动开始...]"); + try { + ApplicationContext pluginContext = getApplicationContext(); + Assert.notNull(pluginContext, "pluginContext 不能为空"); + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件启动成功...]"); + } catch (Exception e) { + log.error("[HttpVertxPlugin][HttpVertxPlugin 插件开启动异常...]", e); + } + } + + @Override + public void stop() { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止开始...]"); + try { + log.info("[HttpVertxPlugin][HttpVertxPlugin 插件停止成功...]"); + } catch (Exception e) { + log.error("[HttpVertxPlugin][HttpVertxPlugin 插件停止异常...]", e); + } + } + + // TODO @芋艿:思考下,未来要不要。。。 + @Override + protected ApplicationContext createApplicationContext() { + // 创建插件自己的 ApplicationContext + AnnotationConfigApplicationContext pluginContext = new AnnotationConfigApplicationContext(); + // 设置父容器为主应用的 ApplicationContext (确保主应用中提供的类可用) + pluginContext.setParent(SpringUtil.getApplicationContext()); + // 继续使用插件自己的 ClassLoader 以加载插件内部的类 + pluginContext.setClassLoader(getWrapper().getPluginClassLoader()); + // 扫描当前插件的自动配置包 + // TODO @芋艿:后续看看,怎么配置类包 + pluginContext.scan("cn.iocoder.yudao.module.iot.plugin.http.config"); + pluginContext.refresh(); + return pluginContext; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java new file mode 100644 index 000000000..63e55f58f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpAutoConfiguration.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; +import cn.iocoder.yudao.module.iot.plugin.http.downstream.IotDeviceDownstreamHandlerImpl; +import cn.iocoder.yudao.module.iot.plugin.http.upstream.IotDeviceUpstreamServer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * IoT 插件 HTTP 的专用自动配置类 + * + * @author haohao + */ +@Configuration +@EnableConfigurationProperties(IotPluginHttpProperties.class) +public class IotPluginHttpAutoConfiguration { + + @Bean(initMethod = "start", destroyMethod = "stop") + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi, + IotPluginHttpProperties properties) { + return new IotDeviceUpstreamServer(properties, deviceUpstreamApi); + } + + @Bean + public IotDeviceDownstreamHandler deviceDownstreamHandler() { + return new IotDeviceDownstreamHandlerImpl(); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java new file mode 100644 index 000000000..49dca8126 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/config/IotPluginHttpProperties.java @@ -0,0 +1,17 @@ +package cn.iocoder.yudao.module.iot.plugin.http.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties(prefix = "yudao.iot.plugin.http") +@Validated +@Data +public class IotPluginHttpProperties { + + /** + * HTTP 服务端口 + */ + private Integer serverPort; + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java new file mode 100644 index 000000000..869fe7234 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/downstream/IotDeviceDownstreamHandlerImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.iot.plugin.http.downstream; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*; +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; + +/** + * HTTP 插件的 {@link IotDeviceDownstreamHandler} 实现类 + * + * 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!! + * 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。 + * + * @author 芋道源码 + */ +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler { + + @Override + public CommonResult invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持调用设备服务"); + } + + @Override + public CommonResult getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持获取设备属性"); + } + + @Override + public CommonResult setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + + @Override + public CommonResult setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + + @Override + public CommonResult upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性"); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java new file mode 100644 index 000000000..67129a4d1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/IotDeviceUpstreamServer.java @@ -0,0 +1,83 @@ +package cn.iocoder.yudao.module.iot.plugin.http.upstream; + +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.plugin.http.config.IotPluginHttpProperties; +import cn.iocoder.yudao.module.iot.plugin.http.upstream.router.IotDeviceUpstreamVertxHandler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.handler.BodyHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器 + * + * 协议:HTTP + * + * @author haohao + */ +@Slf4j +public class IotDeviceUpstreamServer { + + private final Vertx vertx; + private final HttpServer server; + private final IotPluginHttpProperties properties; + + public IotDeviceUpstreamServer(IotPluginHttpProperties properties, + IotDeviceUpstreamApi deviceUpstreamApi) { + this.properties = properties; + // 创建 Vertx 实例 + this.vertx = Vertx.vertx(); + // 创建 Router 实例 + Router router = Router.router(vertx); + router.route().handler(BodyHandler.create()); // 处理 Body + + // 使用统一的 Handler 处理所有上行请求 + IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(deviceUpstreamApi); + router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler); + router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler); + + // 创建 HttpServer 实例 + this.server = vertx.createHttpServer().requestHandler(router); + } + + /** + * 启动 HTTP 服务器 + */ + public void start() { + log.info("[start][开始启动]"); + server.listen(properties.getServerPort()) + .toCompletionStage() + .toCompletableFuture() + .join(); + log.info("[start][启动完成,端口({})]", this.server.actualPort()); + } + + /** + * 停止所有 + */ + public void stop() { + log.info("[stop][开始关闭]"); + try { + // 关闭 HTTP 服务器 + if (server != null) { + server.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + + // 关闭 Vertx 实例 + if (vertx != null) { + vertx.close() + .toCompletionStage() + .toCompletableFuture() + .join(); + } + log.info("[stop][关闭完成]"); + } catch (Exception e) { + log.error("[stop][关闭异常]", e); + throw new RuntimeException(e); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java new file mode 100644 index 000000000..79d465ea0 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/java/cn/iocoder/yudao/module/iot/plugin/http/upstream/router/IotDeviceUpstreamVertxHandler.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.plugin.http.upstream.router; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.plugin.common.pojo.IotStandardResponse; +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; + +/** + * IoT 设备上行统一处理的 Vert.x Handler + *

+ * 统一处理设备属性上报和事件上报的请求 + * + * @author haohao + */ +@RequiredArgsConstructor +@Slf4j +public class IotDeviceUpstreamVertxHandler implements Handler { + + // TODO @haohao:要不要类似 IotDeviceConfigSetVertxHandler 写的,把这些 PATH、METHOD 之类的抽走 + /** + * 属性上报路径 + */ + public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName/thing/event/property/post"; + /** + * 事件上报路径 + */ + public static final String EVENT_PATH = "/sys/:productKey/:deviceName/thing/event/:identifier/post"; + + private static final String PROPERTY_METHOD = "thing.event.property.post"; + private static final String EVENT_METHOD_PREFIX = "thing.event."; + private static final String EVENT_METHOD_SUFFIX = ".post"; + + private final IotDeviceUpstreamApi deviceUpstreamApi; + + // TODO @haohao:要不要分成多个 Handler?每个只解决一个问题哈。 + @Override + public void handle(RoutingContext routingContext) { + String path = routingContext.request().path(); + String requestId = IdUtil.fastSimpleUUID(); + + try { + // 1. 解析通用参数 + String productKey = routingContext.pathParam("productKey"); + String deviceName = routingContext.pathParam("deviceName"); + JsonObject body = routingContext.body().asJsonObject(); + requestId = ObjUtil.defaultIfBlank(body.getString("id"), requestId); + + // 2. 根据路径模式处理不同类型的请求 + CommonResult result; + String method; + if (path.matches(".*/thing/event/property/post")) { + // 处理属性上报 + IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName, requestId, body); + + // 设备上线 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + + // 属性上报 + result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO); + method = PROPERTY_METHOD; + } else if (path.matches(".*/thing/event/.+/post")) { + // 处理事件上报 + String identifier = routingContext.pathParam("identifier"); + IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier, requestId, body); + + // 设备上线 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName()); + + // 事件上报 + result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO); + method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX; + } else { + // 不支持的请求路径 + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, "unknown", BAD_REQUEST.getCode(), "不支持的请求路径"); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + return; + } + + // 3. 返回标准响应 + IotStandardResponse response; + if (result.isSuccess()) { + response = IotStandardResponse.success(requestId, method, result.getData()); + } else { + response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg()); + } + IotPluginCommonUtils.writeJsonResponse(routingContext, response); + } catch (Exception e) { + log.error("[handle][处理上行请求异常] path={}", path, e); + String method = path.contains("/property/") ? PROPERTY_METHOD + : EVENT_METHOD_PREFIX + (routingContext.pathParams().containsKey("identifier") + ? routingContext.pathParam("identifier") + : "unknown") + EVENT_METHOD_SUFFIX; + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse); + } + } + + /** + * 更新设备状态 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + */ + private void updateDeviceState(String productKey, String deviceName) { + deviceUpstreamApi.updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO() + .setRequestId(IdUtil.fastSimpleUUID()).setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState())); + } + + /** + * 解析属性上报请求 + * + * @param productKey 产品 Key + * @param deviceName 设备名称 + * @param requestId 请求 ID + * @param body 请求体 + * @return 属性上报请求 DTO + */ + @SuppressWarnings("unchecked") + private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName, String requestId, JsonObject body) { + // 按照标准 JSON 格式处理属性数据 + Map properties = new HashMap<>(); + Map params = body.getJsonObject("params") != null ? body.getJsonObject("params").getMap() : null; + if (params != null) { + // 将标准格式的 params 转换为平台需要的 properties 格式 + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object valueObj = entry.getValue(); + // 如果是复杂结构(包含 value 和 time) + if (valueObj instanceof Map) { + Map valueMap = (Map) valueObj; + properties.put(key, valueMap.getOrDefault("value", valueObj)); + } else { + properties.put(key, valueObj); + } + } + } + + // 构建属性上报请求 DTO + return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO().setRequestId(requestId) + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setProperties(properties); + } + + /** + * 解析事件上报请求 + * + * @param productKey 产品K ey + * @param deviceName 设备名称 + * @param identifier 事件标识符 + * @param requestId 请求 ID + * @param body 请求体 + * @return 事件上报请求 DTO + */ + private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier, String requestId, JsonObject body) { + // 按照标准 JSON 格式处理事件参数 + Map params; + if (body.containsKey("params")) { + params = body.getJsonObject("params").getMap(); + } else { + // 兼容旧格式 + params = new HashMap<>(); + } + + // 构建事件上报请求 DTO + return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId) + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now()) + .setProductKey(productKey).setDeviceName(deviceName)).setIdentifier(identifier).setParams(params); + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml new file mode 100644 index 000000000..f195628a6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-http/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + application: + name: yudao-module-iot-plugin-http + +yudao: + iot: + plugin: + common: + upstream-url: http://127.0.0.1:48080 + downstream-port: 8093 + plugin-key: yudao-module-iot-plugin-http + http: + server-port: 8092 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties new file mode 100644 index 000000000..939e0f692 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/plugin.properties @@ -0,0 +1,7 @@ +plugin.id=mqtt-plugin +plugin.description=Vert.x MQTT plugin +plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin +plugin.version=1.0.0 +plugin.requires= +plugin.provider=ahh +plugin.license=Apache-2.0 diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml new file mode 100644 index 000000000..f1fba5059 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/pom.xml @@ -0,0 +1,156 @@ + + + + yudao-module-iot-plugins + cn.iocoder.boot + ${revision} + + 4.0.0 + jar + + yudao-module-iot-plugin-mqtt + + ${project.artifactId} + + + 物联网 插件模块 - mqtt 插件 + + + + + mqtt-plugin + cn.iocoder.yudao.module.iot.plugin.MqttPlugin + 0.0.1 + ahh + mqtt-plugin-0.0.1 + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + unzip jar file + package + + + + + + + run + + + + + + + maven-assembly-plugin + 2.3 + + + + src/main/assembly/assembly.xml + + + false + + + + make-assembly + package + + attached + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + ${plugin.id} + ${plugin.class} + ${plugin.version} + ${plugin.provider} + ${plugin.description} + ${plugin.dependencies} + + + + + + + maven-deploy-plugin + + true + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.pf4j + pf4j-spring + provided + + + + cn.iocoder.boot + yudao-module-iot-api + ${revision} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + io.vertx + vertx-mqtt + 4.5.11 + + + \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml new file mode 100644 index 000000000..daec9e431 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/assembly/assembly.xml @@ -0,0 +1,31 @@ + + plugin + + zip + + false + + + false + runtime + lib + + *:jar:* + + + + + + + target/plugin-classes + classes + + + diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java new file mode 100644 index 000000000..7883fa8b1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.iot.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; + +// TODO @芋艿:暂未实现 +@Slf4j +public class MqttPlugin extends Plugin { + + private MqttServerExtension mqttServerExtension; + + public MqttPlugin(PluginWrapper wrapper) { + super(wrapper); + } + + @Override + public void start() { + log.info("MQTT Plugin started."); + mqttServerExtension = new MqttServerExtension(); + mqttServerExtension.startMqttServer(); + } + + @Override + public void stop() { + log.info("MQTT Plugin stopped."); + if (mqttServerExtension != null) { + mqttServerExtension.stopMqttServer().onComplete(ar -> { + if (ar.succeeded()) { + log.info("Stopped MQTT Server successfully"); + } else { + log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage()); + } + }); + } + } +} diff --git a/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java new file mode 100644 index 000000000..dd0c5da37 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-mqtt/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java @@ -0,0 +1,232 @@ +package cn.iocoder.yudao.module.iot.plugin; + +import io.netty.handler.codec.mqtt.MqttProperties; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.mqtt.MqttEndpoint; +import io.vertx.mqtt.MqttServer; +import io.vertx.mqtt.MqttServerOptions; +import io.vertx.mqtt.MqttTopicSubscription; +import io.vertx.mqtt.messages.MqttDisconnectMessage; +import io.vertx.mqtt.messages.MqttPublishMessage; +import io.vertx.mqtt.messages.MqttSubscribeMessage; +import io.vertx.mqtt.messages.MqttUnsubscribeMessage; +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode; +import lombok.extern.slf4j.Slf4j; +import org.pf4j.Extension; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +// TODO @芋艿:暂未实现 +/** + * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中 + */ +@Slf4j +@Extension +public class MqttServerExtension { + + private Vertx vertx; + private MqttServer mqttServer; + + /** + * 启动 MQTT 服务端 + * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等 + */ + public void startMqttServer() { + // 初始化 Vert.x + vertx = Vertx.vertx(); + + // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ========== + // MqttServerOptions options = new MqttServerOptions() + // .setPort(8883) + // .setKeyCertOptions(new PemKeyCertOptions() + // .setKeyPath("./src/test/resources/tls/server-key.pem") + // .setCertPath("./src/test/resources/tls/server-cert.pem")) + // .setSsl(true); + + // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ========== + // options.setUseWebSocket(true); + + // ========== 默认不启用 SSL 的示例 ========== + MqttServerOptions options = new MqttServerOptions() + .setPort(1883) + .setHost("0.0.0.0") + .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true + + mqttServer = MqttServer.create(vertx, options); + + // 指定 endpointHandler,处理客户端连接等 + mqttServer.endpointHandler(endpoint -> { + handleClientConnect(endpoint); + handleDisconnect(endpoint); + handleSubscribe(endpoint); + handleUnsubscribe(endpoint); + handlePublish(endpoint); + handlePing(endpoint); + }); + + // 启动监听 + mqttServer.listen(ar -> { + if (ar.succeeded()) { + log.info("MQTT server is listening on port {}", mqttServer.actualPort()); + } else { + log.error("Error on starting the server", ar.cause()); + } + }); + } + + /** + * 优雅关闭 MQTT 服务端 + */ + public Future stopMqttServer() { + if (mqttServer != null) { + return mqttServer.close().onComplete(ar -> { + if (ar.succeeded()) { + log.info("MQTT server closed."); + if (vertx != null) { + vertx.close(); + log.info("Vert.x instance closed."); + } + } else { + log.error("Failed to close MQTT server: {}", ar.cause().getMessage()); + } + }); + } + return Future.succeededFuture(); + } + + // ==================== 以下为官方示例中常见事件的处理封装 ==================== + + /** + * 处理客户端连接 (CONNECT) + */ + private void handleClientConnect(MqttEndpoint endpoint) { + // 打印 CONNECT 的主要信息 + log.info("MQTT client [{}] request to connect, clean session = {}", + endpoint.clientIdentifier(), endpoint.isCleanSession()); + + if (endpoint.auth() != null) { + log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword()); + } + log.info("[properties = {}]", endpoint.connectProperties()); + + if (endpoint.will() != null) { + log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]", + endpoint.will().getWillTopic(), + new String(endpoint.will().getWillMessageBytes()), + endpoint.will().getWillQos(), + endpoint.will().isWillRetain()); + } + + log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds()); + + // 接受远程客户端的连接 + endpoint.accept(false); + } + + /** + * 处理客户端主动断开 (DISCONNECT) + */ + private void handleDisconnect(MqttEndpoint endpoint) { + endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> { + log.info("Received disconnect from client [{}], reason code = {}", + endpoint.clientIdentifier(), disconnectMessage.code()); + }); + } + + /** + * 处理客户端订阅 (SUBSCRIBE) + */ + private void handleSubscribe(MqttEndpoint endpoint) { + endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> { + List reasonCodes = new ArrayList<>(); + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) { + log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService()); + // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS) + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService())); + } + // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES); + }); + } + + /** + * 处理客户端取消订阅 (UNSUBSCRIBE) + */ + private void handleUnsubscribe(MqttEndpoint endpoint) { + endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> { + for (String topic : unsubscribe.topics()) { + log.info("Unsubscription for {}", topic); + } + // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties + endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); + }); + } + + /** + * 处理客户端发布的消息 (PUBLISH) + */ + private void handlePublish(MqttEndpoint endpoint) { + // 接收 PUBLISH 消息 + endpoint.publishHandler((MqttPublishMessage message) -> { + String payload = message.payload().toString(Charset.defaultCharset()); + log.info("Received message [{}] on topic [{}] with QoS [{}]", + payload, message.topicName(), message.qosLevel()); + + // 根据不同 QoS,回复客户端 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { + endpoint.publishAcknowledge(message.messageId()); + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { + endpoint.publishReceived(message.messageId()); + } + }); + + // 如果 QoS = 2,需要处理 PUBREL + endpoint.publishReleaseHandler(messageId -> { + endpoint.publishComplete(messageId); + }); + } + + /** + * 处理客户端 PINGREQ + */ + private void handlePing(MqttEndpoint endpoint) { + endpoint.pingHandler(v -> { + // 这里仅做日志, PINGRESP 已自动发送 + log.info("Ping received from client [{}]", endpoint.clientIdentifier()); + }); + } + + // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ==================== + + /** + * 服务端主动向已连接的某个 endpoint 发布消息的示例 + * 如果使用 MQTT 5.0,可以传递更多消息属性 + */ + public void publishToClient(MqttEndpoint endpoint, String topic, String content) { + endpoint.publish(topic, + Buffer.buffer(content), + MqttQoS.AT_LEAST_ONCE, // QoS 自行选择 + false, + false); + + // 处理 QoS 1 和 QoS 2 的 ACK + endpoint.publishAcknowledgeHandler(messageId -> { + log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); + }).publishReceivedHandler(messageId -> { + endpoint.publishRelease(messageId); + }).publishCompletionHandler(messageId -> { + log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId); + }); + } + + // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ==================== + // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle: + // DeploymentOptions options = new DeploymentOptions().setInstances(10); + // vertx.deployVerticle(() -> new MyMqttVerticle(), options); + +} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http index 4ab7b4f71..8b2067399 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/ProductSpuController.http @@ -1,4 +1,4 @@ ### 获得商品 SPU 明细 GET {{baseUrl}}/product/spu/get-detail?id=4 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.http b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.http index c391b5873..784c46460 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.http +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.http @@ -1,18 +1,18 @@ ### 获得订单交易的分页(默认) GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 获得订单交易的分页(价格) GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10&sortField=price&sortAsc=true Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 获得订单交易的分页(销售) GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10&sortField=salesCount&sortAsc=true Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 获得商品 SPU 明细 GET {{appApi}}/product/spu/get-detail?id=102 -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java index 124357cac..8ea2ca510 100644 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/history/ProductBrowseHistoryMapper.java @@ -21,9 +21,8 @@ import java.util.Collection; public interface ProductBrowseHistoryMapper extends BaseMapperX { default ProductBrowseHistoryDO selectByUserIdAndSpuId(Long userId, Long spuId) { - return selectOne(new LambdaQueryWrapperX() - .eq(ProductBrowseHistoryDO::getUserId, userId) - .eq(ProductBrowseHistoryDO::getSpuId, spuId)); + return selectFirstOne(ProductBrowseHistoryDO::getUserId, userId, + ProductBrowseHistoryDO::getSpuId, spuId); } default PageResult selectPage(ProductBrowseHistoryPageReqVO reqVO) { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http index 0dda88c7d..e74a46a11 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/activity/AppActivityController.http @@ -2,4 +2,4 @@ GET {{appApi}}/promotion/activity/list-by-spu-ids?spuIds=222&spuIds=633 Authorization: Bearer {{appToken}} Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainHelpController.http b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainHelpController.http index 2e401e9d6..a3b3d225f 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainHelpController.http +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainHelpController.http @@ -2,7 +2,7 @@ POST {{appApi}}/promotion/bargain-help/create Authorization: Bearer test248 Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "recordId": 26 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.http b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.http index 46cbe3c8e..dde40f807 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.http +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/bargain/AppBargainRecordController.http @@ -2,7 +2,7 @@ POST {{appApi}}/promotion/bargain-record/create Authorization: Bearer {{appToken}} Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "activityId": 1 diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java index 29b771126..3096a49f3 100755 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -47,7 +47,7 @@ public interface CouponTemplateMapper extends BaseMapperX { } default List selectListByTakeType(Integer takeType) { - return selectList(CouponTemplateDO::getTakeType, takeType); + return selectList(CouponTemplateDO::getTakeType, takeType, CouponTemplateDO::getStatus, CommonStatusEnum.ENABLE.getStatus()); } default List selectList(List canTakeTypes, Integer productScope, Long productScopeValue, Integer count) { diff --git a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index 060306f11..1c5db8288 100644 --- a/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -121,6 +121,10 @@ public class CouponServiceImpl implements CouponService { @Transactional(rollbackFor = Exception.class) public Map> takeCoupon(Long templateId, Set userIds, CouponTakeTypeEnum takeType) { CouponTemplateDO template = couponTemplateService.getCouponTemplate(templateId); + return takeCoupon(template, userIds, takeType); + } + + private Map> takeCoupon(CouponTemplateDO template, Set userIds, CouponTakeTypeEnum takeType) { // 1. 过滤掉达到领取限制的用户 removeTakeLimitUser(userIds, template); // 2. 校验优惠劵是否可以领取 @@ -131,7 +135,7 @@ public class CouponServiceImpl implements CouponService { couponMapper.insertBatch(couponList); // 4. 增加优惠劵模板的领取数量 - couponTemplateService.updateCouponTemplateTakeCount(templateId, userIds.size()); + couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @@ -208,7 +212,7 @@ public class CouponServiceImpl implements CouponService { public void takeCouponByRegister(Long userId) { List templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER); for (CouponTemplateDO template : templates) { - takeCoupon(template.getId(), CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); + takeCoupon(template, CollUtil.newHashSet(userId), CouponTakeTypeEnum.REGISTER); } } diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.http b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.http index 81cb35cbf..f342e948a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.http +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/TradeAfterSaleController.http @@ -1,18 +1,18 @@ ### 获得交易售后分页 => 成功 GET {{baseUrl}}/trade/after-sale/page?pageNo=1&pageSize=10 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 同意售后 => 成功 PUT {{baseUrl}}/trade/after-sale/agree?id=7 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Content-Type: application/json ### 拒绝售后 => 成功 PUT {{baseUrl}}/trade/after-sale/disagree Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Content-Type: application/json { @@ -23,11 +23,11 @@ Content-Type: application/json ### 确认退款 => 成功 PUT {{baseUrl}}/trade/after-sale/refund?id=6 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Content-Type: application/json ### 确认收货 => 成功 PUT {{baseUrl}}/trade/after-sale/receive?id=7 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} Content-Type: application/json diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.http b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.http index 7877faade..5255d1b8f 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.http +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/order/TradeOrderController.http @@ -1,14 +1,14 @@ ### 获得交易订单分页 => 成功 GET {{baseUrl}}/trade/order/page?pageNo=1&pageSize=10 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 获得交易订单分页 => 成功 GET {{baseUrl}}/trade/order/get-detail?id=21 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 获得交易订单的物流轨迹 => 成功 GET {{baseUrl}}/trade/order/get-express-track-list?id=21 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} \ No newline at end of file +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/AppCartController.http b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/AppCartController.http index b341a4886..2eda3c2c3 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/AppCartController.http +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/cart/AppCartController.http @@ -1,6 +1,6 @@ ### 请求 /trade/cart/add 接口 => 成功 POST {{appApi}}/trade/cart/add -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} Content-Type: application/json @@ -12,7 +12,7 @@ Content-Type: application/json ### 请求 /trade/cart/update 接口 => 成功 PUT {{appApi}}/trade/cart/update -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} Content-Type: application/json @@ -23,20 +23,20 @@ Content-Type: application/json ### 请求 /trade/cart/delete 接口 => 成功 DELETE {{appApi}}/trade/cart/delete?ids=1 -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /trade/cart/get-count 接口 => 成功 GET {{appApi}}/trade/cart/get-count -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /trade/cart/get-count-map 接口 => 成功 GET {{appApi}}/trade/cart/get-count-map -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /trade/cart/list 接口 => 成功 GET {{appApi}}/trade/cart/list -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.http b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.http index 59490a773..f51ddff00 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.http +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/app/order/AppTradeOrderController.http @@ -1,18 +1,18 @@ ### /trade-order/settlement 获得订单结算信息(基于商品) GET {{appApi}}/trade/order/settlement?type=0&items[0].skuId=1&items[0].count=2&items[1].skuId=2&items[1].count=3&couponId=1 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### /trade-order/settlement 获得订单结算信息(基于购物车) GET {{appApi}}/trade/order/settlement?type=0&items[0].cartId=50&couponId=1 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### /trade-order/create 创建订单(基于商品)【快递】 POST {{appApi}}/trade/order/create Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "pointStatus": true, @@ -31,7 +31,7 @@ tenant-id: {{appTenentId}} POST {{appApi}}/trade/order/create Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "pointStatus": true, @@ -51,19 +51,19 @@ tenant-id: {{appTenentId}} ### 获得订单交易的分页 GET {{appApi}}/trade/order/page?pageNo=1&pageSize=10 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 获得订单交易的详细 GET {{appApi}}/trade/order/get-detail?id=21 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 获得交易订单的物流轨迹 GET {{appApi}}/trade/order/get-express-track-list?id=70 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### /trade-order/settlement-product 获得商品结算信息 GET {{appApi}}/trade/order/settlement-product?spuIds=633 Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} \ No newline at end of file +tenant-id: {{appTenantId}} \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java index 3d836bb17..9cd6ff793 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/config/TradeExpressProperties.java @@ -56,6 +56,15 @@ public class TradeExpressProperties { @NotEmpty(message = "快递鸟 Api Key 配置项不能为空") private String apiKey; + /** + * 接口指令 + * + * 1. 1002:免费版(只能查询申通、圆通快递) + * 2. 8001:付费版 + */ + @NotEmpty(message = "RequestType 配置项不能为空") + private String requestType = "1002"; + } /** diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java index 24cf8e6ed..5ecd1c3f0 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java @@ -39,11 +39,6 @@ public class KdNiaoExpressClient implements ExpressClient { private static final String REAL_TIME_QUERY_URL = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; - /** - * 快递鸟即时查询免费版 RequestType - */ - private static final String REAL_TIME_FREE_REQ_TYPE = "1002"; - private final RestTemplate restTemplate; private final TradeExpressProperties.KdNiaoConfig config; @@ -67,7 +62,7 @@ public class KdNiaoExpressClient implements ExpressClient { && StrUtil.length(reqDTO.getPhone()) >= 4) { requestDTO.setCustomerName(StrUtil.subSufByLength(reqDTO.getPhone(), 4)); } - KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, REAL_TIME_FREE_REQ_TYPE, + KdNiaoExpressQueryRespDTO respDTO = httpRequest(REAL_TIME_QUERY_URL, config.getRequestType(), requestDTO, KdNiaoExpressQueryRespDTO.class); // 处理结果 diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java index 4522f956a..41224f45a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderQueryServiceImpl.java @@ -94,6 +94,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { if (userIds == null) { // 没查询到用户,说明肯定也没他的订单 return PageResult.empty(); } + // 分页查询 return tradeOrderMapper.selectPage(reqVO, userIds); } @@ -126,7 +127,7 @@ public class TradeOrderQueryServiceImpl implements TradeOrderQueryService { return new TradeOrderSummaryRespVO(); } // 查询每个售后状态对应的数量、金额 - List> list = tradeOrderMapper.selectOrderSummaryGroupByRefundStatus(reqVO, null); + List> list = tradeOrderMapper.selectOrderSummaryGroupByRefundStatus(reqVO, userIds); TradeOrderSummaryRespVO vo = new TradeOrderSummaryRespVO().setAfterSaleCount(0L).setAfterSalePrice(0L); for (Map map : list) { diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java index 801c7c018..bf8651af5 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.price.calculator; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.module.member.api.level.MemberLevelApi; import cn.iocoder.yudao.module.member.api.level.dto.MemberLevelRespDTO; import cn.iocoder.yudao.module.member.api.user.MemberUserApi; @@ -141,7 +142,9 @@ public class TradeDiscountActivityPriceCalculator implements TradePriceCalculato */ public Integer calculateVipPrice(MemberLevelRespDTO level, TradePriceCalculateRespBO.OrderItem orderItem) { - if (level == null || level.getDiscountPercent() == null) { + if (level == null + || CommonStatusEnum.isDisable(level.getStatus()) + || level.getDiscountPercent() == null) { return 0; } Integer newPrice = calculateRatePrice(orderItem.getPayPrice(), level.getDiscountPercent().doubleValue()); diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.http b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.http index 6bae7c7eb..a0582e6d3 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.http +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/address/AppAddressController.http @@ -1,7 +1,7 @@ ### 请求 /create 接口 => 成功 POST {{appApi}}//member/address/create Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} { @@ -16,7 +16,7 @@ Authorization: Bearer {{appToken}} ### 请求 /update 接口 => 成功 PUT {{appApi}}//member/address/update Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} { @@ -32,23 +32,23 @@ Authorization: Bearer {{appToken}} ### 请求 /delete 接口 => 成功 DELETE {{appApi}}//member/address/delete?id=2 Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /get 接口 => 成功 GET {{appApi}}//member/address/get?id=1 Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /get-default 接口 => 成功 GET {{appApi}}//member/address/get-default Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} ### 请求 /list 接口 => 成功 GET {{appApi}}//member/address/list Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} Authorization: Bearer {{appToken}} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http index 5b68d6984..391aa922c 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.http @@ -1,7 +1,7 @@ ### 请求 /login 接口 => 成功 POST {{appApi}}/member/auth/login Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "mobile": "15601691388", @@ -11,7 +11,7 @@ tenant-id: {{appTenentId}} ### 请求 /send-sms-code 接口 => 成功 POST {{appApi}}/member/auth/send-sms-code Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "mobile": "15601691388", @@ -21,7 +21,7 @@ tenant-id: {{appTenentId}} ### 请求 /sms-login 接口 => 成功 POST {{appApi}}/member/auth/sms-login Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} terminal: 30 { @@ -32,7 +32,7 @@ terminal: 30 ### 请求 /social-login 接口 => 成功 POST {{appApi}}/member/auth/social-login Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "type": 34, @@ -43,7 +43,7 @@ tenant-id: {{appTenentId}} ### 请求 /weixin-mini-app-login 接口 => 成功 POST {{appApi}}/member/auth/weixin-mini-app-login Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "phoneCode": "618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5", @@ -54,14 +54,14 @@ tenant-id: {{appTenentId}} POST {{appApi}}/member/auth/logout Content-Type: application/json Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66 -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 请求 /auth/refresh-token 接口 => 成功 POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70 Content-Type: application/json -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} ### 请求 /auth/create-weixin-jsapi-signature 接口 => 成功 POST {{appApi}}/member/auth/create-weixin-jsapi-signature?url=http://www.iocoder.cn Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} diff --git a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/AppMemberUserController.http b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/AppMemberUserController.http index 745556f75..8ffb70cac 100644 --- a/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/AppMemberUserController.http +++ b/yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/user/AppMemberUserController.http @@ -1,4 +1,4 @@ ### 请求 /member/user/profile/get 接口 => 没有权限 GET {{appApi}}/member/user/get Authorization: Bearer test245 -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http index 74b8f40b6..c093adf71 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/material/MpMaterialController.http @@ -2,4 +2,4 @@ GET {{baseUrl}}/mp/material/page?permanent=true&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http index 2276b3b4f..defd7ecb5 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/menu/MpMenuController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/mp/menu/save Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "accountId": "1", @@ -37,7 +37,7 @@ tenant-id: {{adminTenentId}} POST {{baseUrl}}/mp/menu/save Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "accountId": "1", @@ -47,4 +47,4 @@ tenant-id: {{adminTenentId}} ### 请求 /mp/menu/list 接口 => 成功 GET {{baseUrl}}/mp/menu/list?accountId=1 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http index dbb3a7b22..8d17c6bd5 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpAutoReplyController.http @@ -2,4 +2,4 @@ GET {{baseUrl}}/mp/auto-reply/page?accountId=1&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http index b9f9721c9..16677d430 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/message/MpMessageController.http @@ -2,13 +2,13 @@ GET {{baseUrl}}/mp/message/page?accountId=1&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/message/send 接口 => 成功(文本) POST {{baseUrl}}/mp/message/send Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "userId": 3, @@ -20,7 +20,7 @@ tenant-id: {{adminTenentId}} POST {{baseUrl}}/mp/message/send Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "userId": 3, diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http index 87f9d432a..ea754292c 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpDraftController.http @@ -2,13 +2,13 @@ GET {{baseUrl}}/mp/draft/page?accountId=1&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/draft/create 接口 => 成功 POST {{baseUrl}}/mp/draft/create?accountId=1 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "articles": [ @@ -35,7 +35,7 @@ tenant-id: {{adminTenentId}} PUT {{baseUrl}}/mp/draft/update?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} [{ "title": "我是标题(OOO)", diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http index 122413200..246a95c4a 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/news/MpFreePublishController.http @@ -2,12 +2,12 @@ GET {{baseUrl}}/mp/free-publish/page?accountId=1&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/free-publish/submit 接口 => 成功 POST {{baseUrl}}/mp/free-publish/submit?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-vilmd7iS51D8IPddxflWrau0hIQ2ovY8YanO5jlgUcM Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} {} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http index fe79105ba..a888145e1 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/mp/tag/create Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "accountId": "1", @@ -13,7 +13,7 @@ tenant-id: {{adminTenentId}} PUT {{baseUrl}}/mp/tag/update Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "id": "3", @@ -24,16 +24,16 @@ tenant-id: {{adminTenentId}} DELETE {{baseUrl}}/mp/tag/delete?id=3 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/tag/page 接口 => 成功 GET {{baseUrl}}/mp/tag/page?accountId=1&pageNo=1&pageSize=10 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/tag/sync 接口 => 成功 POST {{baseUrl}}/mp/tag/sync?accountId=1 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http index 7c615810f..c78356aad 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http @@ -2,13 +2,13 @@ POST {{baseUrl}}/mp/user/sync?accountId=1 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /mp/user/update 接口 => 成功 PUT {{baseUrl}}/mp/user/update Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "id": "3", diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http index 14ce54ef9..4fe1898b4 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.http @@ -2,7 +2,7 @@ POST {{appApi}}/pay/order/submit Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "id": 174, @@ -13,7 +13,7 @@ tenant-id: {{appTenentId}} POST {{appApi}}/pay/order/submit Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "id": 202, @@ -27,7 +27,7 @@ tenant-id: {{appTenentId}} POST {{appApi}}/pay/order/submit Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "id": 202, @@ -41,7 +41,7 @@ tenant-id: {{appTenentId}} POST {{appApi}}/pay/order/submit Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "id": 202, @@ -55,7 +55,7 @@ tenant-id: {{appTenentId}} POST {{appApi}}/pay/order/submit Content-Type: application/json Authorization: Bearer {{appToken}} -tenant-id: {{appTenentId}} +tenant-id: {{appTenantId}} { "id": 202, diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java index a482605d5..384628370 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -44,7 +44,7 @@ public class PayNotifyLogDO extends BaseDO { /** * 支付通知状态 * - * 外键 {@link PayNotifyStatusEnum} + * 枚举 {@link PayNotifyStatusEnum} */ private Integer status; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java index 7bfabad3f..9f9ee7fef 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -52,7 +52,7 @@ public class PayNotifyTaskDO extends TenantBaseDO { /** * 通知类型 * - * 外键 {@link PayNotifyTypeEnum} + * 枚举 {@link PayNotifyTypeEnum} */ private Integer type; /** @@ -73,7 +73,7 @@ public class PayNotifyTaskDO extends TenantBaseDO { /** * 通知状态 * - * 外键 {@link PayNotifyStatusEnum} + * 枚举 {@link PayNotifyStatusEnum} */ private Integer status; /** diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java index 396694a75..ec88aa73a 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; @@ -41,7 +41,7 @@ public class WxAppPayClient extends AbstractWxPayClient { // 构建 WxPayUnifiedOrderRequest 对象 WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); // 执行请求 - WxPayMpOrderResult response = client.createOrder(request); + WxPayAppOrderResult response = client.createOrder(request); // 转换结果 return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index a3cc1cefa..6db0e567c 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -24,6 +24,7 @@ public interface ErrorCodeConstants { ErrorCode MENU_NOT_EXISTS = new ErrorCode(1_002_001_003, "菜单不存在"); ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1_002_001_004, "存在子菜单,无法删除"); ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1_002_001_005, "父菜单的类型必须是目录或者菜单"); + ErrorCode MENU_COMPONENT_NAME_DUPLICATE = new ErrorCode(1_002_001_006, "已经存在该组件名的菜单"); // ========== 角色模块 1-002-002-000 ========== ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1_002_002_000, "角色不存在"); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http index 00ae2ba2b..f42dfcd03 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.http @@ -1,7 +1,7 @@ ### 请求 /login 接口 => 成功 POST {{baseUrl}}/system/auth/login Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} tag: Yunai.local { @@ -14,7 +14,7 @@ tag: Yunai.local ### 请求 /login 接口 => 成功(无验证码) POST {{baseUrl}}/system/auth/login Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "username": "admin", @@ -24,10 +24,10 @@ tenant-id: {{adminTenentId}} ### 请求 /get-permission-info 接口 => 成功 GET {{baseUrl}}/system/auth/get-permission-info Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /list-menus 接口 => 成功 GET {{baseUrl}}/system/list-menus Authorization: Bearer {{token}} #Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index 1e7a99a32..d9269470d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -7,14 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.security.config.SecurityProperties; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthLoginRespVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthRegisterReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthResetPasswordReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSmsSendReqVO; -import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO; +import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; @@ -36,12 +29,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.Collections; import java.util.List; diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http index f52431502..5a7ce8ee1 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.http @@ -1,4 +1,4 @@ ### 请求 /menu/list 接口 => 成功 GET {{baseUrl}}/system/dict-data/list-all-simple Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http index f1b893d01..14165619e 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/ip/AreaController.http @@ -1,5 +1,5 @@ ### 获得地区树 GET {{baseUrl}}/system/area/tree Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http index 485358165..be3102eb6 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.http @@ -1,4 +1,4 @@ ### 请求 /system/operate-log/page 接口 => 成功 GET {{baseUrl}}/system/operate-log/page Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.http index f3c47f514..9ad2ed208 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/system/mail-template/send-mail Authorization: Bearer {{token}} Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "templateCode": "test_01", diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http index dcf60a6cf..f8d7b8931 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2ClientController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/system/oauth2-client/create Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "id": "1", diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http index 725a5d4f5..f770ed91d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.http @@ -1,13 +1,13 @@ ### 请求 /system/oauth2/authorize 接口 => 成功 GET {{baseUrl}}/system/oauth2/authorize?clientId=default Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /system/oauth2/authorize + token 接口 => 成功 POST {{baseUrl}}/system/oauth2/authorize Content-Type: application/x-www-form-urlencoded Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true @@ -15,7 +15,7 @@ response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=htt POST {{baseUrl}}/system/oauth2/authorize Content-Type: application/x-www-form-urlencoded Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false @@ -23,7 +23,7 @@ response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=http POST {{baseUrl}}/system/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a @@ -31,7 +31,7 @@ grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07 POST {{baseUrl}}/system/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} grant_type=password&username=admin&password=admin123&scope=user.read @@ -39,16 +39,16 @@ grant_type=password&username=admin&password=admin123&scope=user.read POST {{baseUrl}}/system/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 ### 请求 /system/oauth2/token + DELETE 接口 => 成功 DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /system/oauth2/check-token 接口 => 成功 POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106 Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http index 13c8545bd..9e76c7cdc 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2UserController.http @@ -1,13 +1,13 @@ ### 请求 /system/oauth2/user/get 接口 => 成功 GET {{baseUrl}}/system/oauth2/user/get Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### 请求 /system/oauth2/user/update 接口 => 成功 PUT {{baseUrl}}/system/oauth2/user/update Content-Type: application/json Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "nickname": "芋道源码" diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http index a90d8b8ab..fbaff1e50 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.http @@ -1,4 +1,4 @@ ### 请求 /menu/list 接口 => 成功 GET {{baseUrl}}/system/menu/list Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java index b6d067f1f..824fe4104 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/MenuController.java @@ -67,8 +67,8 @@ public class MenuController { } @GetMapping({"/list-all-simple", "simple-list"}) - @Operation(summary = "获取菜单精简信息列表", description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + - "在多租户的场景下,会只返回租户所在套餐有的菜单") + @Operation(summary = "获取菜单精简信息列表", + description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。在多租户的场景下,会只返回租户所在套餐有的菜单") public CommonResult> getSimpleMenuList() { List list = menuService.getMenuListByTenant( new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus())); diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http index c68b86b75..375180a34 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/system/role/create Authorization: Bearer {{token}} Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "name": "测试角色", @@ -14,7 +14,7 @@ tenant-id: {{adminTenentId}} POST {{baseUrl}}/system/role/update Authorization: Bearer {{token}} Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "id": 100, @@ -26,7 +26,7 @@ tenant-id: {{adminTenentId}} POST {{baseUrl}}/system/role/delete Content-Type: application/x-www-form-urlencoded Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} roleId=14 @@ -34,9 +34,9 @@ roleId=14 GET {{baseUrl}}/system/role/get?id=100 Content-Type: application/x-www-form-urlencoded Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} ### /role/page 成功 GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10 Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http index ee24e928b..3b975c39d 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/SmsTemplateController.http @@ -2,7 +2,7 @@ POST {{baseUrl}}/system/sms-template/send-sms Authorization: Bearer {{token}} Content-Type: application/json -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "templateCode": "test_01", diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http index 5ab64392a..9909b519a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http @@ -3,7 +3,7 @@ POST {{baseUrl}}/system/social-client/send-subscribe-message Authorization: Bearer {{token}} Content-Type: application/json #Authorization: Bearer test100 -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "userId": 247, diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http index a4d517385..38aa594ea 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.http @@ -5,7 +5,7 @@ GET {{baseUrl}}/system/tenant/get-id-by-name?name=芋道源码 POST {{baseUrl}}/system/tenant/create Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} { "name": "芋道", diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java index 090c6a80f..f698b92d0 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/TenantController.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -9,7 +10,6 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO; -import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSimpleRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; import cn.iocoder.yudao.module.system.service.tenant.TenantService; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - 租户") @RestController @@ -45,13 +46,25 @@ public class TenantController { return success(tenant != null ? tenant.getId() : null); } + @GetMapping({ "simple-list" }) + @PermitAll + @Operation(summary = "获取租户精简信息列表", description = "只包含被开启的租户,用于【首页】功能的选择租户选项") + public CommonResult> getTenantSimpleList() { + List list = tenantService.getTenantListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, tenantDO -> + new TenantRespVO().setId(tenantDO.getId()).setName(tenantDO.getName()))); + } + @GetMapping("/get-by-website") @PermitAll @Operation(summary = "使用域名,获得租户信息", description = "登录界面,根据用户的域名,获得租户信息") @Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn") - public CommonResult getTenantByWebsite(@RequestParam("website") String website) { + public CommonResult getTenantByWebsite(@RequestParam("website") String website) { TenantDO tenant = tenantService.getTenantByWebsite(website); - return success(BeanUtils.toBean(tenant, TenantSimpleRespVO.class)); + if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) { + return success(null); + } + return success(new TenantRespVO().setId(tenant.getId()).setName(tenant.getName())); } @PostMapping("/create") @@ -99,8 +112,7 @@ public class TenantController { @Operation(summary = "导出租户 Excel") @PreAuthorize("@ss.hasPermission('system:tenant:export')") @ApiAccessLog(operateType = EXPORT) - public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, - HttpServletResponse response) throws IOException { + public void exportTenantExcel(@Valid TenantPageReqVO exportReqVO, HttpServletResponse response) throws IOException { exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); List list = tenantService.getTenantPage(exportReqVO).getList(); // 导出 Excel diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java deleted file mode 100755 index 49752278d..000000000 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantSimpleRespVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Schema(description = "管理后台 - 租户精简 Response VO") -@Data -public class TenantSimpleRespVO { - - @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") - private Long id; - - @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") - private String name; - -} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http index 7d7f6227e..90f0c9832 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http @@ -2,4 +2,4 @@ GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 Authorization: Bearer {{token}} #Authorization: Bearer test100 -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http index f06037b37..c94c2ad23 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.http @@ -1,4 +1,4 @@ ### 请求 /system/user/profile/get 接口 => 没有权限 GET {{baseUrl}}/system/user/profile/get Authorization: Bearer {{token}} -tenant-id: {{adminTenentId}} +tenant-id: {{adminTenantId}} diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java index 8458faa67..31eb117d2 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/MenuMapper.java @@ -28,4 +28,9 @@ public interface MenuMapper extends BaseMapperX { default List selectListByPermission(String permission) { return selectList(MenuDO::getPermission, permission); } + + default MenuDO selectByComponentName(String componentName) { + return selectOne(MenuDO::getComponentName, componentName); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java index af30ecee2..a90e6ac2a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/social/SocialUserMapper.java @@ -5,23 +5,20 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.system.controller.admin.socail.vo.user.SocialUserPageReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialUserDO; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SocialUserMapper extends BaseMapperX { default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { - return selectOne(new LambdaQueryWrapper() - .eq(SocialUserDO::getType, type) - .eq(SocialUserDO::getCode, code) - .eq(SocialUserDO::getState, state)); + return selectOne(SocialUserDO::getType, type, + SocialUserDO::getCode, code, + SocialUserDO::getState, state); } default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { - return selectOne(new LambdaQueryWrapper() - .eq(SocialUserDO::getType, type) - .eq(SocialUserDO::getOpenid, openid)); + return selectFirstOne(SocialUserDO::getType, type, + SocialUserDO::getOpenid, openid); } default PageResult selectPage(SocialUserPageReqVO reqVO) { diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java index aaca0160a..8ddf06065 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/tenant/TenantMapper.java @@ -43,4 +43,8 @@ public interface TenantMapper extends BaseMapperX { return selectList(TenantDO::getPackageId, packageId); } + default List selectListByStatus(Integer status) { + return selectList(TenantDO::getStatus, status); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java index 558dbdef2..3dd12491a 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java @@ -184,7 +184,7 @@ public class AliyunSmsClient extends AbstractSmsClient { @SneakyThrows private static String percentCode(String str) { Assert.notNull(str, "str 不能为空"); - return URLEncoder.encode(str, StandardCharsets.UTF_8.name()) + return HttpUtils.encodeUtf8(str) .replace("+", "%20") // 加号 "+" 被替换为 "%20" .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java index 82f55395e..622f8ac1b 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.format.FastDateFormat; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.http.HttpUtil; @@ -19,8 +18,6 @@ import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditS import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; import lombok.extern.slf4j.Slf4j; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDateTime; @@ -156,10 +153,9 @@ public class HuaweiSmsClient extends AbstractSmsClient { .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(null); } - @SuppressWarnings("CharsetObjectCanBeUsed") - private static void appendToBody(StringBuilder body, String key, String value) throws UnsupportedEncodingException { + private static void appendToBody(StringBuilder body, String key, String value) { if (StrUtil.isNotEmpty(value)) { - body.append(key).append(URLEncoder.encode(value, CharsetUtil.CHARSET_UTF_8.name())); + body.append(key).append(HttpUtils.encodeUtf8(value)); } } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java index e81c75724..4b0998023 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java @@ -30,6 +30,7 @@ import com.xingyuv.captcha.model.vo.CaptchaVO; import com.xingyuv.captcha.service.CaptchaService; import jakarta.annotation.Resource; import jakarta.validation.Validator; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -71,6 +72,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { * 验证码的开关,默认为 true */ @Value("${yudao.captcha.enable:true}") + @Setter // 为了单测:开启或者关闭验证码 private Boolean captchaEnable; @Override diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java index 98052eb65..5378108dd 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.controller.admin.permission.vo.menu.MenuListReqVO; @@ -53,7 +54,8 @@ public class MenuServiceImpl implements MenuService { // 校验父菜单存在 validateParentMenu(createReqVO.getParentId(), null); // 校验菜单(自己) - validateMenu(createReqVO.getParentId(), createReqVO.getName(), null); + validateMenuName(createReqVO.getParentId(), createReqVO.getName(), null); + validateMenuComponentName(createReqVO.getComponentName(), null); // 插入数据库 MenuDO menu = BeanUtils.toBean(createReqVO, MenuDO.class); @@ -74,7 +76,8 @@ public class MenuServiceImpl implements MenuService { // 校验父菜单存在 validateParentMenu(updateReqVO.getParentId(), updateReqVO.getId()); // 校验菜单(自己) - validateMenu(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); + validateMenuName(updateReqVO.getParentId(), updateReqVO.getName(), updateReqVO.getId()); + validateMenuComponentName(updateReqVO.getComponentName(), updateReqVO.getId()); // 更新到数据库 MenuDO updateObj = BeanUtils.toBean(updateReqVO, MenuDO.class); @@ -228,7 +231,7 @@ public class MenuServiceImpl implements MenuService { * @param id 菜单编号 */ @VisibleForTesting - void validateMenu(Long parentId, String name, Long id) { + void validateMenuName(Long parentId, String name, Long id) { MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); if (menu == null) { return; @@ -242,6 +245,30 @@ public class MenuServiceImpl implements MenuService { } } + /** + * 校验菜单组件名是否合法 + * + * @param componentName 组件名 + * @param id 菜单编号 + */ + @VisibleForTesting + void validateMenuComponentName(String componentName, Long id) { + if (StrUtil.isBlank(componentName)) { + return; + } + MenuDO menu = menuMapper.selectByComponentName(componentName); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + return; + } + if (!menu.getId().equals(id)) { + throw exception(MENU_COMPONENT_NAME_DUPLICATE); + } + } + /** * 初始化菜单的通用属性。 *

diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java index e0bd9d291..425d18d60 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantService.java @@ -38,7 +38,7 @@ public interface TenantService { * 更新租户的角色菜单 * * @param tenantId 租户编号 - * @param menuIds 菜单编号数组 + * @param menuIds 菜单编号数组 */ void updateTenantRoleMenu(Long tenantId, Set menuIds); @@ -97,6 +97,14 @@ public interface TenantService { */ List getTenantListByPackageId(Long packageId); + /** + * 获得指定状态的租户列表 + * + * @param status 状态 + * @return 租户列表 + */ + List getTenantListByStatus(Integer status); + /** * 进行租户的信息处理逻辑 * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java index b5b3c358e..c3b09ec5a 100755 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/tenant/TenantServiceImpl.java @@ -265,6 +265,11 @@ public class TenantServiceImpl implements TenantService { return tenantMapper.selectListByPackageId(packageId); } + @Override + public List getTenantListByStatus(Integer status) { + return tenantMapper.selectListByStatus(status); + } + @Override public void handleTenantInfo(TenantInfoHandler handler) { // 如果禁用,则不执行逻辑 diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java index 093060e84..b04f426dc 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java @@ -12,6 +12,7 @@ import com.google.common.collect.Lists; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; import java.time.LocalDateTime; import java.util.List; @@ -50,6 +51,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"Message\":\"OK\",\"RequestId\":\"30067CE9-3710-5984-8881-909B21D8DB28\",\"Code\":\"OK\",\"BizId\":\"800025323183427988\"}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, @@ -75,6 +78,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"Message\":\"手机号码格式错误\",\"RequestId\":\"B7700B8E-227E-5886-9564-26036172F01F\",\"Code\":\"isv.MOBILE_NUMBER_ILLEGAL\"}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams); @@ -127,6 +132,8 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest { // mock 方法 httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString())) .thenReturn("{\"TemplateCode\":\"SMS_207945135\",\"RequestId\":\"6F4CC077-29C8-5BA5-AB62-5FF95068A5AC\",\"Message\":\"OK\",\"TemplateContent\":\"您的验证码${code},该验证码5分钟内有效,请勿泄漏于他人!\",\"TemplateName\":\"公告通知\",\"TemplateType\":0,\"Code\":\"OK\",\"CreateDate\":\"2020-12-23 17:34:42\",\"Reason\":\"无审批备注\",\"TemplateStatus\":1}"); + httpUtilsMockedStatic.when(() -> HttpUtils.encodeUtf8(anyString())) + .then((Answer) invocationOnMock -> (String) invocationOnMock.getArguments()[0]); // 调用 SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java index 2f5f717e2..151150bc5 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java @@ -22,7 +22,6 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService; import com.xingyuv.captcha.model.common.ResponseModel; import com.xingyuv.captcha.service.CaptchaService; import jakarta.annotation.Resource; -import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validation; import jakarta.validation.Validator; import org.junit.jupiter.api.BeforeEach; @@ -37,7 +36,6 @@ 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.module.system.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -66,7 +64,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { @BeforeEach public void setUp() { - ReflectUtil.setFieldValue(authService, "captchaEnable", true); + authService.setCaptchaEnable(true); // 注入一个 Validator 对象 ReflectUtil.setFieldValue(authService, "validator", Validation.buildDefaultValidatorFactory().getValidator()); @@ -156,7 +154,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { .setSocialType(randomEle(SocialTypeEnum.values()).getType())); // mock 验证码正确 - ReflectUtil.setFieldValue(authService, "captchaEnable", false); + authService.setCaptchaEnable(false); // mock user 数据 AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); @@ -269,8 +267,6 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - // mock 验证码打开 - ReflectUtil.setFieldValue(authService, "captchaEnable", true); // mock 验证通过 when(captchaService.verification(argThat(captchaVO -> { assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); @@ -287,34 +283,17 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); // mock 验证码关闭 - ReflectUtil.setFieldValue(authService, "captchaEnable", false); + authService.setCaptchaEnable(false); // 调用,无需断言 authService.validateCaptcha(reqVO); } - @Test - public void testValidateCaptcha_constraintViolationException() { - // 准备参数 - AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - reqVO.setCaptchaVerification(null); - - // mock 验证码打开 - ReflectUtil.setFieldValue(authService, "captchaEnable", true); - - // 调用,并断言异常 - assertThrows(ConstraintViolationException.class, () -> authService.validateCaptcha(reqVO), - "验证码不能为空"); - } - - @Test public void testCaptcha_fail() { // 准备参数 AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); - // mock 验证码打开 - ReflectUtil.setFieldValue(authService, "captchaEnable", true); // mock 验证通过 when(captchaService.verification(argThat(captchaVO -> { assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); diff --git a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java index 27f1efb36..cc393796c 100644 --- a/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/permission/MenuServiceImplTest.java @@ -275,7 +275,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest { } @Test - public void testValidateMenu_success() { + public void testValidateMenu_Name_success() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 @@ -284,11 +284,11 @@ public class MenuServiceImplTest extends BaseDbUnitTest { String otherSonMenuName = randomString(); // 调用,无需断言 - menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId); + menuService.validateMenuName(parentId, otherSonMenuName, otherSonMenuId); } @Test - public void testValidateMenu_sonMenuNameDuplicate() { + public void testValidateMenu_sonMenuNameNameDuplicate() { // mock 父子菜单 MenuDO sonMenu = createParentAndSonMenu(); // 准备参数 @@ -297,7 +297,7 @@ public class MenuServiceImplTest extends BaseDbUnitTest { String otherSonMenuName = sonMenu.getName(); //相同名称 // 调用,并断言异常 - assertServiceException(() -> menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId), + assertServiceException(() -> menuService.validateMenuName(parentId, otherSonMenuName, otherSonMenuId), MENU_NAME_DUPLICATE); } diff --git a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java index 2bf6e5277..a9ed8fbf4 100644 --- a/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java +++ b/yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java @@ -1,6 +1,10 @@ package cn.iocoder.yudao.server.controller; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import jakarta.annotation.security.PermitAll; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -13,6 +17,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC * @author 芋道源码 */ @RestController +@Slf4j public class DefaultController { @RequestMapping("/admin-api/bpm/**") @@ -27,9 +32,9 @@ public class DefaultController { "[微信公众号 yudao-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/product/**", // 商品中心 + @RequestMapping(value = { "/admin-api/product/**", // 商品中心 "/admin-api/trade/**", // 交易中心 - "/admin-api/promotion/**"}) // 营销中心 + "/admin-api/promotion/**" }) // 营销中心 public CommonResult mall404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[商城系统 yudao-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); @@ -47,28 +52,43 @@ public class DefaultController { "[CRM 模块 yudao-module-crm - 已禁用][参考 https://doc.iocoder.cn/crm/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/report/**"}) + @RequestMapping(value = { "/admin-api/report/**"}) public CommonResult report404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[报表模块 yudao-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); } - @RequestMapping(value = {"/admin-api/pay/**"}) + @RequestMapping(value = { "/admin-api/pay/**"}) public CommonResult pay404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/ai/**"}) + @RequestMapping(value = { "/admin-api/ai/**"}) public CommonResult ai404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); } - @RequestMapping(value = {"/admin-api/iot/**"}) + @RequestMapping(value = { "/admin-api/iot/**"}) public CommonResult iot404() { return CommonResult.error(NOT_IMPLEMENTED.getCode(), - "[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + "[IoT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]"); + } + + /** + * 测试接口:打印 query、header、body + */ + @RequestMapping(value = { "/test" }) + @PermitAll + public CommonResult test(HttpServletRequest request) { + // 打印查询参数 + log.info("Query: {}", ServletUtils.getParamMap(request)); + // 打印请求头 + log.info("Header: {}", ServletUtils.getHeaderMap(request)); + // 打印请求体 + log.info("Body: {}", ServletUtils.getBody(request)); + return CommonResult.success(true); } } diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index cd8652c79..7ca9ec6cc 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -4,6 +4,10 @@ server: --- #################### 数据库相关配置 #################### spring: + autoconfigure: + exclude: + - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 @@ -213,4 +217,9 @@ iot: # 保持连接 keepalive: 60 # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) - clearSession: true \ No newline at end of file + clearSession: true + + +# 插件配置 +pf4j: + pluginsDir: ${user.home}/plugins # 插件目录 \ No newline at end of file diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 885f73db5..77438ef2a 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -3,13 +3,15 @@ server: --- #################### 数据库相关配置 #################### spring: - # 数据源配置项 autoconfigure: exclude: - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 + - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 web-stat-filter: @@ -66,6 +68,13 @@ spring: url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true username: root password: 123456 +# tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!) +# url: jdbc:TAOS-RS://127.0.0.1:6041/ruoyi_vue_pro +# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver +# username: root +# password: taosdata +# druid: +# validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 data: @@ -73,7 +82,7 @@ spring: host: 127.0.0.1 # 地址 port: 6379 # 端口 database: 0 # 数据库索引 -# password: dev # 密码,建议生产环境开启 + # password: dev # 密码,建议生产环境开启 --- #################### 定时任务相关配置 #################### @@ -175,6 +184,7 @@ logging: cn.iocoder.yudao.module.crm.dal.mysql: debug cn.iocoder.yudao.module.erp.dal.mysql: debug cn.iocoder.yudao.module.iot.dal.mysql: debug + cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG cn.iocoder.yudao.module.ai.dal.mysql: debug org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示 @@ -256,20 +266,7 @@ justauth: prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 ---- #################### iot相关配置 TODO 芋艿:再瞅瞅 #################### -iot: - emq: - # 账号 - username: anhaohao - # 密码 - password: ahh@123456 - # 主机地址 - hostUrl: tcp://chaojiniu.top:1883 - # 客户端Id,不能相同,采用随机数 ${random.value} - client-id: ${random.int} - # 默认主题 - default-topic: test - # 保持连接 - keepalive: 60 - # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息) - clearSession: true \ No newline at end of file +--- #################### iot相关配置 TODO 芋艿【IOT】:再瞅瞅 #################### +pf4j: +# pluginsDir: /tmp/ + pluginsDir: ../plugins \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index e0e135c7b..13683df54 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -48,7 +48,7 @@ springdoc: default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 knife4j: - enable: true + enable: false # TODO 芋艿:需要关闭增强,具体原因见:https://github.com/xiaoymin/knife4j/issues/874 setting: language: zh_cn @@ -149,21 +149,28 @@ spring: ai: vectorstore: # 向量存储 redis: - index: default-index - prefix: "default:" - embedding: - transformer: - onnx: - model-uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/model.onnx - tokenizer: - uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/tokenizer.json + initialize-schema: true + index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 + prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 + qdrant: + initialize-schema: true + collection-name: knowledge_segment # Qdrant 中向量集合的名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行 + host: 127.0.0.1 + port: 6334 + milvus: + initialize-schema: true + database-name: default # Milvus 中数据库的名称 + collection-name: knowledge_segment # Milvus 中集合的名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行 + client: + host: 127.0.0.1 + port: 19530 qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK zhipuai: # 智谱 AI api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs openai: # OpenAI 官方 - api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z + api-key: sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17 base-url: https://api.gptsapi.net azure: # OpenAI 微软 openai: @@ -175,11 +182,12 @@ spring: model: llama3 stabilityai: api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx - cloud: - ai: - tongyi: # 通义千问 - tongyi: - api-key: sk-Zsd81gZYg7 + dashscope: # 通义千问 + api-key: sk-71800982914041848008480000000000 + minimax: # Minimax:https://www.minimaxi.com/ + api-key: xxxx + moonshot: # 月之暗灭(KIMI) + api-key: sk-abc yudao: ai: @@ -187,11 +195,22 @@ yudao: enable: true api-key: sk-e94db327cc7d457d99a8de8810fc6b12 model: deepseek-chat + doubao: # 字节豆包 + enable: true + api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 + model: doubao-1-5-lite-32k-250115 + hunyuan: # 腾讯混元 + enable: true + api-key: sk-abc + model: hunyuan-turbo + siliconflow: # 硅基流动 + enable: true + api-key: sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz + model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B xinghuo: # 讯飞星火 enable: true - appId: 13c8cca6 - appKey: cb6415c19d6162cda07b47316fcb0416 - secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh + appKey: 75b161ed2aef4719b275d6e7f2a4d4cd + secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz model: generalv3.5 midjourney: enable: true @@ -252,6 +271,7 @@ yudao: ignore-urls: - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 - /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号 + - /admin-api/system/tenant/simple-list # 获取租户列表,不许带租户编号 - /admin-api/system/captcha/get # 获取图片验证码,和租户无关 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 @@ -284,6 +304,8 @@ yudao: - infra_job - infra_job_log - infra_job_log + - iot_plugin_info + - iot_plugin_instance - infra_data_source_config - jimu_dict - jimu_dict_item @@ -309,6 +331,8 @@ yudao: - mail_account - mail_template - sms_template + - iot:device + - iot:thing_model_list sms-code: # 短信验证码相关的配置项 expire-times: 10m send-frequency: 1m @@ -321,12 +345,16 @@ yudao: receive-expire-time: 14d # 收货的过期时间 comment-expire-time: 7d # 评论的过期时间 express: - client: kd_niao + client: KD_NIAO kd-niao: api-key: cb022f1e-48f1-4c4a-a723-9001ac9676b8 business-id: 1809751 + request-type: 1002 # 免费版 1002;付费版 8001 kd100: key: pLXUGAwK5305 customer: E77DF18BE109F454A5CD319E44BF5177 -debug: false \ No newline at end of file +debug: false +# 插件配置 TODO 芋艿:【IOT】需要处理下 +pf4j: + pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录 \ No newline at end of file