# Conflicts:
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/messagebus/config/IotMessageBusProperties.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
#	yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/AbstractIotProtocolDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxAuthEventProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/IotEmqxUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/IotUdpUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketDownstreamSubscriber.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/websocket/IotWebSocketUpstreamProtocol.java
#	yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotSceneRuleMessageSubscriber.java
#	yudao-module-iot/yudao-module-iot-server/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
pull/244/head
YunaiV 2026-02-14 16:36:52 +08:00
commit 71393eed21
249 changed files with 15095 additions and 7778 deletions

View File

@ -11,7 +11,7 @@
Target Server Version : 80200 (8.2.0)
File Encoding : 65001
Date: 26/11/2025 22:43:12
Date: 14/02/2026 16:02:08
*/
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 = 23210 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
) ENGINE = InnoDB AUTO_INCREMENT = 23367 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 = 2659 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
) ENGINE = InnoDB AUTO_INCREMENT = 2880 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 = 197 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
) ENGINE = InnoDB AUTO_INCREMENT = 210 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
-- ----------------------------
-- Records of infra_codegen_table
@ -204,7 +204,6 @@ INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `val
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 'url', 2, 'SkyWalking 监控的地址', 'url.skywalking', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:57:03', b'0');
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 'url', 2, 'Spring Boot Admin 监控的地址', 'url.spring-boot-admin', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:52:07', b'0');
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 'url', 2, 'Swagger 接口文档的地址', 'url.swagger', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:59:00', b'0');
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 'ui', 2, '腾讯地图 key', 'tencent.lbs.key', 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E', b'1', '腾讯地图 key', '1', '2023-06-03 19:16:27', '1', '2023-06-03 19:16:27', b'0');
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 'test2', 2, 'test3', 'test4', 'test5', b'1', 'test6', '1', '2023-12-03 09:55:16', '1', '2025-04-06 21:00:09', b'0');
INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (13, '用户管理-账号初始密码', 2, '用户管理-注册开关', 'system.user.register-enabled', 'true', b'0', '', '1', '2025-04-26 17:23:41', '1', '2025-04-26 17:23:41', b'0');
COMMIT;
@ -225,7 +224,7 @@ CREATE TABLE `infra_data_source_config` (
`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 = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表';
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表';
-- ----------------------------
-- Records of infra_data_source_config
@ -251,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 = 2142 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
) ENGINE = InnoDB AUTO_INCREMENT = 2163 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
-- ----------------------------
-- Records of infra_file
@ -292,7 +291,7 @@ INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `c
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (29, '本地存储(示例)', 10, 'mac/linux 使用 /windows 使用 \\', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileClientConfig\",\"basePath\":\"/Users/yunai/tmp/file\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2025-05-02 11:25:45', '1', '2025-11-24 20:57:14', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (30, 'SFTP 存储(示例)', 12, '', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileClientConfig\",\"basePath\":\"/upload\",\"domain\":\"http://127.0.0.1:48080\",\"host\":\"127.0.0.1\",\"port\":2222,\"username\":\"foo\",\"password\":\"pass\"}', '1', '2025-05-02 16:34:10', '1', '2025-11-24 20:57:14', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (34, '七牛云存储【私有】(示例)', 20, '请换成你自己的密钥!!!', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3.cn-south-1.qiniucs.com\",\"domain\":\"http://t151glocd.hn-bkt.clouddn.com\",\"bucket\":\"ruoyi-vue-pro-private\",\"accessKey\":\"3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS\",\"accessSecret\":\"wd0tbVBYlp0S-ihA8Qg2hPLncoP83wyrIq24OZuY\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-08-17 21:22:00', '1', '2025-11-24 20:57:14', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false}', '1', '2025-10-02 14:32:12', '1', '2025-11-24 20:57:14', b'0');
INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, '1', 20, '1', b'0', '{\"@class\":\"cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"http://www.baidu.com\",\"domain\":\"http://www.xxx.com\",\"bucket\":\"1\",\"accessKey\":\"2\",\"accessSecret\":\"3\",\"enablePathStyleAccess\":false,\"enablePublicAccess\":false,\"region\":\"1\"}', '1', '2025-10-02 14:32:12', '1', '2025-11-29 15:59:39', b'0');
COMMIT;
-- ----------------------------
@ -413,16 +412,16 @@ CREATE TABLE `system_dept` (
`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 = 116 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表';
) ENGINE = InnoDB AUTO_INCREMENT = 118 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表';
-- ----------------------------
-- Records of system_dept
-- ----------------------------
BEGIN;
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2025-03-29 15:47:53', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-01-04 18:01:12', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2025-03-29 15:49:55', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (102, '长沙分公司', 100, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:40', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2024-10-02 10:22:03', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2026-01-04 18:01:24', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, '市场部门', 101, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:38', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (105, '测试部门', 101, 3, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:15', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (106, '财务部门', 101, 4, 103, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-15 21:32:22', b'0', 1);
@ -433,6 +432,8 @@ INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`,
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '顶级部门', 0, 1, NULL, NULL, NULL, 0, '113', '2022-03-07 21:44:50', '113', '2022-03-07 21:44:50', b'0', 122);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, '产品部门', 101, 100, 1, NULL, NULL, 1, '1', '2023-12-02 09:45:13', '1', '2023-12-02 09:45:31', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, '支持部门', 102, 3, 104, NULL, NULL, 1, '1', '2023-12-02 09:47:38', '1', '2025-03-29 15:00:56', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '某个子部门', 0, 1, NULL, NULL, NULL, 0, '1', '2025-12-08 14:51:12', '1', '2025-12-08 14:51:12', b'0', 1);
INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, '某个子部门 2', 0, 2, NULL, NULL, NULL, 0, '1', '2025-12-08 14:51:25', '1', '2025-12-08 14:51:25', b'0', 1);
COMMIT;
-- ----------------------------
@ -455,13 +456,13 @@ 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 = 3035 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
) ENGINE = InnoDB AUTO_INCREMENT = 3054 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
-- ----------------------------
-- Records of system_dict_data
-- ----------------------------
BEGIN;
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 1, '', '1', 'system_user_sex', 0, 'default', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2022-03-29 00:14: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 (1, 1, '', '1', 'system_user_sex', 0, 'primary', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2025-12-10 13:19: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 (2, 2, '', '2', 'system_user_sex', 0, 'success', '', '性别女', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 23:30: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 (8, 1, '正常', '1', 'infra_job_status', 0, 'success', '', '正常状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33: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 (9, 2, '暂停', '2', 'infra_job_status', 0, 'danger', '', '停用状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:45', b'0');
@ -1060,7 +1061,6 @@ 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 (3000, 16, '百川智能', 'BaiChuan', 'ai_platform', 0, '', '', '', '1', '2025-03-23 12:15:46', '1', '2025-03-23 12:15: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 (3001, 40, 'Vben5.0 Ant Design Schema 模版', '40', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-04-23 21:47:47', '1', '2025-09-04 23:25: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 (3002, 6, '支付宝余额', '6', 'brokerage_withdraw_type', 0, '', '', 'API 打款', '1', '2025-05-10 08:24:49', '1', '2025-05-10 08:24: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 (3003, 1, 'Alink', 'Alink', 'iot_codec_type', 0, '', '', '阿里云 Alink', '1', '2025-06-12 22:56:06', '1', '2025-06-12 23: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 (3004, 3, 'WARN', '3', 'iot_alert_level', 0, 'warning', '', '', '1', '2025-06-27 20:32:22', '1', '2025-06-27 20: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 (3005, 1, 'INFO', '1', 'iot_alert_level', 0, 'primary', '', '', '1', '2025-06-27 20:33:28', '1', '2025-06-27 20:34: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 (3006, 5, 'ERROR', '5', 'iot_alert_level', 0, 'danger', '', '', '1', '2025-06-27 20:33:50', '1', '2025-06-27 20:33:50', b'0');
@ -1078,9 +1078,6 @@ 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 (3018, 30, '升级成功', '30', 'iot_ota_task_record_status', 0, 'success', '', '', '1', '2025-07-02 09:45:47', '1', '2025-07-02 09:45: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 (3019, 40, '升级失败', '40', 'iot_ota_task_record_status', 0, 'danger', '', '', '1', '2025-07-02 09:46:02', '1', '2025-07-02 09:46: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 (3020, 50, '升级取消', '50', 'iot_ota_task_record_status', 0, 'warning', '', '', '1', '2025-07-02 09:46:09', '\"1\"', '2025-07-02 09:46: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 (3021, 1, 'IP 定位', '1', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:56:46', '1', '2025-07-05 09: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 (3022, 2, '设备上报', '2', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:56:57', '1', '2025-07-05 09:56:57', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3023, 3, '手动定位', '3', 'iot_location_type', 0, '', '', '', '1', '2025-07-05 09:57:05', '1', '2025-07-05 09:57:05', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3024, 3, '设备事件上报', '3', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:29', '1', '2025-07-06 10:28: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 (3025, 4, '设备服务调用', '4', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:35', '1', '2025-07-06 10:28: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 (3026, 100, '定时触发', '100', 'iot_rule_scene_trigger_type_enum', 0, '', '', '', '1', '2025-07-06 10:28:48', '1', '2025-07-06 10:28:48', b'0');
@ -1093,6 +1090,21 @@ 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 (3033, 51, 'Vben5.0 Element Plus 标准模版', '51', 'infra_codegen_front_type', 0, '', '', '', '1', '2025-09-04 23:26:49', '1', '2025-09-04 23:26: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 (3034, 1, 'ttt', 'tt', 'iot_ota_task_record_status', 0, 'success', '', NULL, '1', '2025-09-06 00:02:21', '1', '2025-09-06 00:02:31', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3035, 40, '支付宝小程序', '40', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3036, 60, 'Admin Uniapp 移动端', '60', 'infra_codegen_front_type', 0, '', '', NULL, '1', '2025-12-16 19:25:51', '1', '2025-12-17 09:46:15', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3040, 1, 'UDP', 'udp', 'iot_protocol_type', 0, '', '', 'UDP 协议', '1', '2026-02-04 00:32:47', '1', '2026-02-04 00:32:47', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3041, 2, 'WebSocket', 'websocket', 'iot_protocol_type', 0, '', '', 'WebSocket 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3042, 3, 'HTTP', 'http', 'iot_protocol_type', 0, '', '', 'HTTP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3043, 4, 'MQTT', 'mqtt', 'iot_protocol_type', 0, 'success', '', 'MQTT 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3044, 5, 'EMQX', 'emqx', 'iot_protocol_type', 0, 'success', '', 'EMQX 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3045, 6, 'CoAP', 'coap', 'iot_protocol_type', 0, '', '', 'CoAP 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-04 00:32:55', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3046, 7, 'Modbus TCP Server', 'modbus_tcp_server', 'iot_protocol_type', 0, '', '', 'Modbus TCP Server 协议', '1', '2026-02-04 00:32:55', '1', '2026-02-12 15:16:45', b'0');
INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3047, 0, 'JSON', 'json', 'iot_serialize_type', 0, 'success', '', 'JSON 格式', '1', '2026-02-04 00:33:19', '1', '2026-02-04 00:33: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 (3048, 1, '二进制', 'binary', 'iot_serialize_type', 0, 'warning', '', '二进制格式', '1', '2026-02-04 00:33:19', '1', '2026-02-04 00:33: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 (3049, 8, 'Modbus TCP Client', 'modbus_tcp_client', 'iot_protocol_type', 0, '', '', 'Modbus TCP Client 协议', '1', '2026-02-08 18:29:46', '1', '2026-02-12 15:16: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 (3050, 2, '边缘采集', '2', 'iot_modbus_mode', 0, 'success', '', '设备主动上报数据,无需轮询', '1', '2025-06-12 22:56:06', '1', '2026-02-09 13:03: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 (3051, 1, 'Modbus TCP', '1', 'iot_modbus_frame_format', 0, 'default', '', 'MBAP 头部格式', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56: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 (3052, 2, 'Modbus RTU', '2', 'iot_modbus_frame_format', 0, 'warning', '', 'CRC16 校验格式', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56: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 (3053, 1, '云端轮询', '1', 'iot_modbus_mode', 0, 'primary', '', '网关主动轮询读取设备寄存器', '1', '2025-06-12 22:56:06', '1', '2025-06-12 22:56:06', b'0');
COMMIT;
-- ----------------------------
@ -1112,7 +1124,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 = 2008 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
) ENGINE = InnoDB AUTO_INCREMENT = 2012 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
-- ----------------------------
-- Records of system_dict_type
@ -1221,14 +1233,16 @@ 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 (1014, 'IoT 场景流转的触发类型枚举', 'iot_rule_scene_trigger_type_enum', 0, '', '1', '2025-03-20 14:59:44', '1', '2025-03-20 14:59:44', 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 (1015, 'IoT 设备消息类型枚举', 'iot_device_message_type_enum', 0, '', '1', '2025-03-20 15:01:15', '1', '2025-03-20 15:01:15', 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 (1016, 'IoT 规则场景的触发类型枚举', 'iot_rule_scene_action_type_enum', 0, '', '1', '2025-03-28 15:26:54', '1', '2025-03-28 15:29: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 (2000, 'IoT 数据格式', 'iot_codec_type', 0, 'IoT 编解码器类型', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', 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 (2001, 'IoT 告警级别', 'iot_alert_level', 0, '', '1', '2025-06-27 20:30:57', '1', '2025-06-27 20:30:57', 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 (2002, 'IoT 告警', 'iot_alert_receive_type', 0, '', '1', '2025-06-27 22:49:19', '1', '2025-06-27 22:49: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 (2003, 'IoT 固件设备范围', 'iot_ota_task_device_scope', 0, '', '1', '2025-07-02 09:42:49', '1', '2025-07-02 09:42:49', 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 (2004, 'IoT 固件升级任务状态', 'iot_ota_task_status', 0, '', '1', '2025-07-02 09:43:43', '1', '2025-07-02 09:43: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 (2005, 'IoT 固件升级记录状态', 'iot_ota_task_record_status', 0, '', '1', '2025-07-02 09:45:02', '1', '2025-07-02 09:45:02', 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 (2006, 'IoT 定位类型', 'iot_location_type', 0, '', '1', '2025-07-05 09:56:25', '1', '2025-07-05 09:56:25', 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 (2007, 'AI MCP 客户端名字', 'ai_mcp_client_name', 0, '', '1', '2025-08-28 13:57:40', '1', '2025-08-28 13:57:40', 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 (2008, 'IoT 协议类型', 'iot_protocol_type', 0, 'IoT 设备接入协议类型', '1', '2026-02-04 00:31:33', '1', '2026-02-04 00:31:33', 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 (2009, 'IoT 序列化类型', 'iot_serialize_type', 0, 'IoT 设备消息序列化类型', '1', '2026-02-04 00:33:16', '1', '2026-02-04 00:33: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 (2010, 'IoT Modbus 工作模式', 'iot_modbus_mode', 0, 'Modbus 设备数据采集模式', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', 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 (2011, 'IoT Modbus 帧格式', 'iot_modbus_frame_format', 0, 'Modbus 数据帧协议格式', '1', '2025-06-12 22:55:46', '1', '2025-06-12 22:55:46', b'0', '1970-01-01 00:00:00');
COMMIT;
-- ----------------------------
@ -1252,7 +1266,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 = 4066 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
) ENGINE = InnoDB AUTO_INCREMENT = 4449 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
-- ----------------------------
-- Records of system_login_log
@ -1286,7 +1300,7 @@ CREATE TABLE `system_mail_account` (
-- ----------------------------
BEGIN;
INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '7684413@qq.com', '7684413@qq.com', '1234576', '127.0.0.1', 8080, b'0', b'0', '1', '2023-01-25 17:39:52', '1', '2025-04-04 16:34:40', b'0');
INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'ydym_test@163.com', 'ydym_test@163.com', 'WBZTEINMIFVRYSOE', 'smtp.163.com', 465, b'1', b'0', '1', '2023-01-26 01:26:03', '1', '2025-07-26 21:57:55', b'0');
INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'ydym_test@163.com', 'ydym_test@163.com', 'WBZTEINMIFVRYSOE', 'smtp.163.com', 465, b'1', b'0', '1', '2023-01-26 01:26:03', '1', '2025-12-20 18:09:32', b'0');
INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, '76854114@qq.com', '3335', '11234', 'yunai1.cn', 466, b'0', b'0', '1', '2023-01-27 15:06:38', '1', '2023-01-27 07:08:36', b'1');
INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `starttls_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '7685413x@qq.com', '2', '3', '4', 5, b'1', b'0', '1', '2023-04-12 23:05:06', '1', '2023-04-12 15:05:11', b'1');
COMMIT;
@ -1385,7 +1399,7 @@ 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 = 5047 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
) ENGINE = InnoDB AUTO_INCREMENT = 5049 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
-- ----------------------------
-- Records of system_menu
@ -1394,8 +1408,8 @@ 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', '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', '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 (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', '2026-01-01 18:43:01', 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', '2026-01-05 19:30: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 (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');
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 (104, '岗位管理', '', 2, 5, 1, 'post', 'fa:address-book-o', 'system/post/index', 'SystemPost', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2024-02-29 01:06:39', b'0');
@ -1730,7 +1744,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 (2156, '查询项目', 'report:go-view-project:query', 3, 0, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:53', '1', '2023-02-07 19:25: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 (2157, '使用 SQL 查询数据', 'report:go-view-data:get-by-sql', 3, 3, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:15', '1', '2023-02-07 19:26:15', 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 (2158, '使用 HTTP 查询数据', 'report:go-view-data:get-by-http', 3, 4, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:35', '1', '2023-02-07 19:26: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 (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2024-07-28 11: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 (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2026-01-05 19:31:07', 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 (2160, 'Cloud 开发文档', '', 1, 2, 0, 'https://cloud.iocoder.cn', 'ep:document-copy', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:47:07', '1', '2023-12-02 21:32: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 (2161, '接入示例', '', 1, 99, 1117, 'demo', 'fa-solid:dragon', 'pay/demo/index', NULL, 0, b'1', b'1', b'1', '', '2023-02-11 14:21:42', '1', '2024-01-18 23:50: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 (2162, '商品导出', 'product:spu:export', 3, 5, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0');
@ -2141,7 +2155,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 (2722, '流程实例的查询(管理员)', 'bpm:process-instance:manager-query', 3, 1, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:18:27', '1', '2024-03-22 08:19: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 (2723, '流程实例的取消(管理员)', 'bpm:process-instance:cancel-by-admin', 3, 2, 2721, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:19:25', '1', '2024-03-22 08: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 (2724, '流程任务', '', 2, 11, 1186, 'process-tasnk', 'ep:collection-tag', 'bpm/task/manager/index', 'BpmManagerTask', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:22', '1', '2024-03-22 08:43: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 (2725, '流程任务的查询(管理员)', 'bpm:task:mananger-query', 3, 1, 2724, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:49', '1', '2024-03-22 08:43: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 (2725, '流程任务的查询(管理员)', 'bpm:task:manager-query', 3, 1, 2724, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-03-22 08:43:49', '1', '2025-12-23 23:04: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 (2726, '流程监听器', '', 2, 5, 1186, 'process-listener', 'fa:assistive-listening-systems', 'bpm/processListener/index', 'BpmProcessListener', 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '1', '2024-03-23 13:13:38', 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 (2727, '流程监听器查询', 'bpm:process-listener:query', 3, 1, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05: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 (2728, '流程监听器创建', 'bpm:process-listener:create', 3, 2, 2726, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-03-09 16:05:34', '', '2024-03-09 16:05:34', b'0');
@ -2386,13 +2400,13 @@ CREATE TABLE `system_notify_message` (
-- Records of system_notify_message
-- ----------------------------
BEGIN;
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 1, 2, 1, 'test', '123', '我是 1我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 11:44:08', '1', '2025-04-21 14:59:37', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 1, 2, 1, 'test', '123', '我是 1我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 11:45:04', '1', '2025-04-21 14:59:37', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 1, 2, 1, 'test', '123', '我是 1我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-12-15 21:24:36', '1', '2023-01-28 11:44:08', '1', '2025-12-15 21:24:36', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 1, 2, 1, 'test', '123', '我是 1我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2025-12-15 21:24:36', '1', '2023-01-28 11:45:04', '1', '2025-12-15 21:24:36', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 103, 2, 2, 'register', '系统消息', '你好,欢迎 哈哈 加入大家庭!', 2, '{\"name\":\"哈哈\"}', b'0', NULL, '1', '2023-01-28 21:02:20', '1', '2023-01-28 21:02:20', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-04-21 14:59:37', '1', '2023-01-28 22:21:42', '1', '2025-04-21 14:59:37', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-04-21 14:59:36', '1', '2023-01-28 22:22:07', '1', '2025-04-21 14:59:36', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 1, 2, 1, 'test', '123', '我是 2我开始 3 了', 1, '{\"name\":\"2\",\"what\":\"3\"}', b'1', '2025-04-21 14:59:35', '1', '2023-01-28 23:45:21', '1', '2025-04-21 14:59:35', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 1, 2, 2, 'register', '系统消息', '你好,欢迎 123 加入大家庭!', 2, '{\"name\":\"123\"}', b'1', '2025-04-21 14:59:35', '1', '2023-01-28 23:50:21', '1', '2025-04-21 14:59:35', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-12-08 17:25:28', '1', '2023-01-28 22:21:42', '1', '2025-12-08 17:25:28', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2025-12-08 17:25:30', '1', '2023-01-28 22:22:07', '1', '2025-12-08 17:25:30', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 1, 2, 1, 'test', '123', '我是 2我开始 3 了', 1, '{\"name\":\"2\",\"what\":\"3\"}', b'1', '2025-12-08 17:25:22', '1', '2023-01-28 23:45:21', '1', '2025-12-08 17:25:22', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 1, 2, 2, 'register', '系统消息', '你好,欢迎 123 加入大家庭!', 2, '{\"name\":\"123\"}', b'1', '2025-12-08 16:46:01', '1', '2023-01-28 23:50:21', '1', '2025-12-08 16:46:01', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (9, 247, 1, 4, 'brokerage_withdraw_audit_approve', 'system', '您在2023-09-28 08:35:46提现¥0.09元的申请已通过审核', 2, '{\"reason\":null,\"createTime\":\"2023-09-28 08:35:46\",\"price\":\"0.09\"}', b'0', NULL, '1', '2023-09-28 16:36:22', '1', '2023-09-28 16:36:22', b'0', 1);
INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (10, 247, 1, 4, 'brokerage_withdraw_audit_approve', 'system', '您在2023-09-30 20:59:40提现¥1.00元的申请已通过审核', 2, '{\"reason\":null,\"createTime\":\"2023-09-30 20:59:40\",\"price\":\"1.00\"}', b'0', NULL, '1', '2023-10-03 12:11:34', '1', '2023-10-03 12:11:34', b'0', 1);
COMMIT;
@ -2448,7 +2462,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 = 39737 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
) ENGINE = InnoDB AUTO_INCREMENT = 47630 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
-- ----------------------------
-- Records of system_oauth2_access_token
@ -2516,8 +2530,8 @@ CREATE TABLE `system_oauth2_client` (
-- Records of system_oauth2_client
-- ----------------------------
BEGIN;
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/20250502/sort2_1746189740718.png', '我是描述', 0, 1800, 2592000, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2025-08-21 10:04:50', b'0');
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/xx/20250502/ed07110a37464b5299f8bd7c67ad65c7_1746187077009.jpg', '啦啦啦啦', 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2025-05-02 19:58:08', b'0');
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/20250502/sort2_1746189740718.png', '我是描述', 0, 1800, 2592000, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2025-12-07 20:07:09', b'0');
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/20251227/javayuanma_1766829882970.jpg', '啦啦啦啦', 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2025-12-27 18:04:44', b'0');
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (41, 'yudao-sso-demo-by-code', 'test', '基于授权码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/it/20250502/sign_1746181948685.png', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"authorization_code\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-09-29 13:28:31', '1', '2025-05-02 18:32:30', b'0');
INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 'yudao-sso-demo-by-password', 'test', '基于密码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/20251025/images (3)_1761360515810.jpeg', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"password\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-10-04 17:40:16', '1', '2025-10-25 10:49:40', b'0');
COMMIT;
@ -2570,7 +2584,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 = 2243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
) ENGINE = InnoDB AUTO_INCREMENT = 2501 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
-- ----------------------------
-- Records of system_oauth2_refresh_token
@ -2604,7 +2618,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 = 9178 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
) ENGINE = InnoDB AUTO_INCREMENT = 9193 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
-- ----------------------------
-- Records of system_operate_log
@ -2636,7 +2650,7 @@ CREATE TABLE `system_post` (
-- Records of system_post
-- ----------------------------
BEGIN;
INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 09:18:20', b'0', 1);
INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2025-12-15 22:38:43', b'0', 1);
INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 'user', '普通员工', 4, 0, '111222', 'admin', '2021-01-05 17:03:48', '1', '2025-03-24 21:32:40', b'0', 1);
INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 'HR', '人力资源', 5, 0, '`', '1', '2024-03-24 20:45:40', '1', '2025-03-29 19:08:10', b'0', 1);
INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 'test', '测试', 10, 0, NULL, '1', '2025-09-02 08:45:57', '1', '2025-09-02 08:45:57', b'0', 1);
@ -2663,7 +2677,7 @@ CREATE TABLE `system_role` (
`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 = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
) ENGINE = InnoDB AUTO_INCREMENT = 160 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表';
-- ----------------------------
-- Records of system_role
@ -2674,7 +2688,7 @@ INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_sco
INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1);
INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121);
INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限', 'test-dp', 3, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '', '1', '2025-03-31 14:58:06', '1', '2025-09-06 20:15:13', b'0', 1);
INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限1', 'test-dp', 4, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '1111', '1', '2025-03-31 14:58:06', '1', '2025-12-04 23:29:40', b'0', 1);
COMMIT;
-- ----------------------------
@ -2692,7 +2706,7 @@ CREATE TABLE `system_role_menu` (
`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 = 6293 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
) ENGINE = InnoDB AUTO_INCREMENT = 6352 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
-- ----------------------------
-- Records of system_role_menu
@ -3512,6 +3526,65 @@ 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 (6290, 111, 2335, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6291, 111, 2363, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6292, 111, 2364, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6293, 2, 5, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6294, 2, 1118, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6295, 2, 1119, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6296, 2, 1120, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6297, 2, 2713, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6298, 2, 2714, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6299, 2, 2715, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6300, 2, 2716, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6301, 2, 2717, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6302, 2, 2718, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6303, 2, 2720, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6304, 2, 1185, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6305, 2, 2721, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6306, 2, 1186, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6307, 2, 2722, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6308, 2, 1187, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6309, 2, 2723, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6310, 2, 1188, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6311, 2, 2724, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6312, 2, 1189, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6313, 2, 2725, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6314, 2, 1190, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6315, 2, 2726, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6316, 2, 1191, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6317, 2, 2727, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6318, 2, 1192, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6319, 2, 2728, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6320, 2, 1193, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6321, 2, 2729, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6322, 2, 1194, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6323, 2, 2730, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6324, 2, 1195, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6325, 2, 2731, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6326, 2, 2732, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6327, 2, 1197, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6328, 2, 2733, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6329, 2, 1198, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6330, 2, 2734, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6331, 2, 1199, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6332, 2, 2735, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6333, 2, 1200, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6334, 2, 1201, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6335, 2, 1202, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6336, 2, 1207, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6337, 2, 1208, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6338, 2, 1209, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6339, 2, 1210, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6340, 2, 1211, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6341, 2, 1212, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6342, 2, 1213, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6343, 2, 1215, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6344, 2, 1216, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6345, 2, 1217, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6346, 2, 1218, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6347, 2, 1219, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6348, 2, 1220, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6349, 2, 1221, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6350, 2, 1222, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6351, 2, 2913, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
COMMIT;
-- ----------------------------
@ -3541,7 +3614,7 @@ CREATE TABLE `system_sms_channel` (
BEGIN;
INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '你要改哦,只有我可以用!!!!', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2024-08-04 08:53:26', b'0');
INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0');
INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2024-09-30 08:55:01', b'0');
INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'mock腾讯云', 'TENCENT', 0, '123', '1 2', '2 3', '', '1', '2024-09-30 08:53:45', '1', '2025-12-20 11:30:18', b'0');
COMMIT;
-- ----------------------------
@ -3566,7 +3639,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 = 682 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
) ENGINE = InnoDB AUTO_INCREMENT = 690 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
-- ----------------------------
-- Records of system_sms_code
@ -3607,7 +3680,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 = 1528 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
) ENGINE = InnoDB AUTO_INCREMENT = 1549 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
-- ----------------------------
-- Records of system_sms_log
@ -3670,9 +3743,9 @@ CREATE TABLE `system_social_client` (
`social_type` tinyint NOT NULL COMMENT '社交平台的类型',
`user_type` tinyint NOT NULL COMMENT '用户类型',
`client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号',
`client_secret` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥',
`public_key` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT 'publicKey公钥',
`client_secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥',
`agent_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '代理编号',
`public_key` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'publicKey 公钥',
`status` tinyint NOT NULL COMMENT '状态',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@ -3681,18 +3754,20 @@ CREATE TABLE `system_social_client` (
`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 = 46 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交客户端表';
) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交客户端表';
-- ----------------------------
-- Records of system_social_client
-- ----------------------------
BEGIN;
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '钉钉', 20, 2, 'dingvrnreaje3yqvzhxg', 'i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI', NULL, 0, '', '2023-10-18 11:21:18', '1', '2023-12-20 21:28:26', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '钉钉(王土豆)', 20, 2, 'dingtsu9hpepjkbmthhw', 'FP_bnSq_HAHKCSncmJjw5hxhnzs6vaVDSZZn3egj6rdqTQ_hu5tQVJyLMpgCakdP', NULL, 0, '', '2023-10-18 11:21:18', '', '2023-12-20 21:28:26', b'1', 121);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '微信公众号', 31, 1, 'wx5b23ba7a5589ecbb', '2a7b3b20c537e52e74afd395eb85f61f', NULL, 0, '', '2023-10-18 16:07:46', '1', '2023-12-20 21:28:23', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (43, '微信小程序', 34, 1, 'wx63c280fe3248a3e7', '6f270509224a7ae1296bbf1c8cb97aed', NULL, 0, '', '2023-10-19 13:37:41', '1', '2023-12-20 21:28:25', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (44, '1', 10, 1, '2', '3', NULL, 0, '1', '2025-04-06 20:36:28', '1', '2025-04-06 20:43:12', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (45, '1', 10, 1, '2', '3', NULL, 1, '1', '2025-09-06 20:26:15', '1', '2025-09-06 20:27:55', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '钉钉', 20, 2, 'dingvrnreaje3yqvzhxg', 'i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI', NULL, NULL, 0, '', '2023-10-18 11:21:18', '1', '2023-12-20 21:28:26', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '钉钉(王土豆)', 20, 2, 'dingtsu9hpepjkbmthhw', 'FP_bnSq_HAHKCSncmJjw5hxhnzs6vaVDSZZn3egj6rdqTQ_hu5tQVJyLMpgCakdP', NULL, NULL, 0, '', '2023-10-18 11:21:18', '', '2023-12-20 21:28:26', b'1', 121);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '微信公众号', 31, 1, 'wx5b23ba7a5589ecbb', '2a7b3b20c537e52e74afd395eb85f61f', NULL, NULL, 0, '', '2023-10-18 16:07:46', '1', '2023-12-20 21:28:23', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (43, '微信小程序', 34, 1, 'wx63c280fe3248a3e7', '6f270509224a7ae1296bbf1c8cb97aed', NULL, NULL, 0, '', '2023-10-19 13:37:41', '1', '2023-12-20 21:28:25', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (44, '1', 10, 1, '2', '3', NULL, NULL, 0, '1', '2025-04-06 20:36:28', '1', '2025-04-06 20:43:12', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (45, '1', 10, 1, '2', '3', NULL, NULL, 1, '1', '2025-09-06 20:26:15', '1', '2025-09-06 20:27:55', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (46, '1', 10, 1, '2', '3', NULL, NULL, 0, '1', '2025-11-29 16:04:23', '1', '2025-11-29 16:04:26', b'1', 1);
INSERT INTO `system_social_client` (`id`, `name`, `social_type`, `user_type`, `client_id`, `client_secret`, `agent_id`, `public_key`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (47, '123', 10, 1, '1', '2', '3', NULL, 0, '1', '2025-12-21 10:27:02', '1', '2025-12-21 10:27:20', b'1', 1);
COMMIT;
-- ----------------------------
@ -3779,7 +3854,7 @@ CREATE TABLE `system_tenant` (
BEGIN;
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn,127.0.0.1:3000,wxc4598c446f8a9cb3', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2025-08-19 05:18:41', b'0');
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn,123321', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-08-19 21:19:29', b'0');
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2022-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2025-09-06 20:44:42', b'0');
INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2023-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2025-12-21 09:50:00', b'0');
COMMIT;
-- ----------------------------
@ -3822,7 +3897,7 @@ CREATE TABLE `system_user_post` (
`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 = 128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表';
) ENGINE = InnoDB AUTO_INCREMENT = 130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表';
-- ----------------------------
-- Records of system_user_post
@ -3837,6 +3912,8 @@ INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_t
INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (123, 115, 1, '1', '2024-04-04 09:37:14', '1', '2024-04-04 09:37:14', b'0', 1);
INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (124, 115, 2, '1', '2024-04-04 09:37:14', '1', '2024-04-04 09:37:14', b'0', 1);
INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (125, 1, 2, '1', '2024-07-13 22:31:39', '1', '2024-07-13 22:31:39', b'0', 1);
INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (128, 139, 2, '1', '2025-12-05 21:43:27', '1', '2025-12-05 21:43:27', b'0', 1);
INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (129, 139, 4, '1', '2025-12-05 21:43:27', '1', '2025-12-05 21:43:27', b'0', 1);
COMMIT;
-- ----------------------------
@ -3854,7 +3931,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 = 51 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表';
) ENGINE = InnoDB AUTO_INCREMENT = 55 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表';
-- ----------------------------
-- Records of system_user_role
@ -3877,6 +3954,10 @@ 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 (48, 100, 155, '1', '2025-04-04 10:41:14', '1', '2025-04-04 10:41:14', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (49, 142, 1, '1', '2025-07-23 09:11:42', '1', '2025-07-23 09:11:42', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (50, 142, 2, '1', '2025-10-07 20:50:37', '1', '2025-10-07 20:50:37', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (51, 139, 1, '1', '2025-12-05 22:36:57', '1', '2025-12-05 22:36:57', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (52, 139, 2, '1', '2025-12-05 22:37:00', '1', '2025-12-05 22:37:00', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (53, 114, 2, '1', '2026-01-04 18:15:40', '1', '2026-01-04 18:15:40', b'0', 1);
INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (54, 114, 3, '1', '2026-01-04 18:16:19', '1', '2026-01-04 18:16:19', b'0', 1);
COMMIT;
-- ----------------------------
@ -3905,16 +3986,16 @@ CREATE TABLE `system_users` (
`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 = 143 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
) ENGINE = InnoDB AUTO_INCREMENT = 145 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
-- ----------------------------
-- 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$04$KljJDa/LK7QfDm0lF5OhuePhlPfjRH3tB2Wu351Uidz.oQGJXevPi', '芋道源码', '管理员', 103, '[1,2]', '11aoteman@126.com', '18818260272', 2, 'http://test.yudao.iocoder.cn/20250921/avatar_1758423875594.png', 0, '0:0:0:0:0:0:0:1', '2025-11-22 18:50:21', 'admin', '2021-01-05 17:03:47', NULL, '2025-11-22 18:50:21', 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$h.aaPKgO.odHepnk5PCsWeEwKdojFWdTItxGKfx1r0e1CSeBzsTJ6', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-04-08 09:36:40', '', '2021-01-07 09:07:17', NULL, '2025-04-21 14:23:08', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/user/avatar/20251220/blob_1766215463801.jpg', 0, '0:0:0:0:0:0:0:1', '2026-02-14 09:07:33', 'admin', '2021-01-05 17:03:47', NULL, '2026-02-14 09:07:33', 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$h.aaPKgO.odHepnk5PCsWeEwKdojFWdTItxGKfx1r0e1CSeBzsTJ6', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-12-15 21:47:26', '', '2021-01-07 09:07:17', NULL, '2025-12-15 21:47:26', 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, NULL, 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', '1', '2025-07-09 23:41: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 (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-03-28 20:01:16', '', '2021-01-21 02:13:53', NULL, '2025-04-21 14:23:08', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-01-04 18:09:54', '', '2021-01-21 02:13:53', NULL, '2026-01-04 18:09:54', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2025-04-21 14:23:08', b'0', 118);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2025-04-21 14:23:08', b'0', 119);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2025-04-21 14:23:08', b'0', 120);
@ -3922,13 +4003,15 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, 'test', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '测试用户', NULL, NULL, '[]', '', '', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2023-12-30 11:42:17', '110', '2022-02-23 13:14:33', NULL, '2025-04-21 14:23:08', b'0', 121);
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 (112, 'newobject', '$2a$04$dB0z8Q819fJWz0hbaLe6B.VfHCjYgWx6LFfET5lyz3JwcqlyCkQ4C', '新对象', NULL, 100, '[]', '', '15601691235', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-03-16 23:11:38', '1', '2022-02-23 19:08:03', NULL, '2025-04-21 14:23:08', 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 (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道1', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', '1', '2025-05-05 15:30:53', b'0', 122);
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 (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-03-24 22:21:05', '1', '2022-03-19 21:50:58', NULL, '2025-04-21 14:23:08', 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 (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-01-04 18:16:01', '1', '2022-03-19 21:50:58', NULL, '2026-01-04 18:16:01', 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 (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, NULL, 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2025-04-21 14:23:08', 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 (117, 'admin123', '$2a$04$sEtimsHu9YCkYY4/oqElHem2Ijc9ld20eYO6lN.g/21NfLUTDLB9W', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-10-02 10:16:20', '1', '2022-07-09 17:40:26', '1', '2025-05-14 09:56:04', 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 (118, 'goudan', '$2a$04$3suGZjnA6rM5bErf38u1felbgqbsPHGdRG3l9NkxPCEt2ah9Y6aJi', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-11-23 15:28:25', '1', '2022-07-09 17:44:43', NULL, '2025-11-23 15:28:25', 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 (139, 'wwbwwb', '$2a$04$aOHoFbQU6zfBk/1Z9raF/ugTdhjNdx7culC1HhO0zvoczAnahCiMq', '小秃头', NULL, NULL, NULL, '', '', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2024-09-10 21:03:58', NULL, '2024-09-10 21:03:58', NULL, '2025-04-21 14:23:08', 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 (139, 'wwbwwb', '$2a$04$FJLIyg8lbPytP29pbZaiU.LesJvCsYfEaHqQfB0pGQhK3e9BeZmLy', '小秃头', '123', 108, '[2,4]', '', '', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-09-10 21:03:58', NULL, '2024-09-10 21:03:58', '1', '2025-12-15 22:38:15', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-04-08 13:09:07', '1', '2025-04-08 13:09:07', '1', '2025-05-14 19:11:48', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$IaR0fGYtalIDURMMdcaD2.4JDWZ15ueQZwap9oPUuxkwSbL66vIRG', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', '1', '2025-11-25 19:49:08', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', NULL, '2025-12-02 13:23:11', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (143, 'a00001', '$2a$04$GhVHFviOw/SsTmiQtifHJesDYFlHMeGK7OWh7aGCCjGGVCmbHVAwa', 'a00001', NULL, 104, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-12-01 16:10:13', NULL, '2025-12-01 16:10:13', '1', '2025-12-05 21:34:05', b'0', 1);
INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 116, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2025-12-15 15:55:54', b'0', 1);
COMMIT;
-- ----------------------------

View File

@ -79,6 +79,7 @@
<vertx.version>4.5.22</vertx.version>
<okhttp.version>4.12.0</okhttp.version>
<californium.version>3.12.0</californium.version>
<j2mod.version>3.2.1</j2mod.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.40.15</awssdk.version>
<justauth.version>1.16.7</justauth.version>
@ -671,6 +672,13 @@
<version>${californium.version}</version>
</dependency>
<!-- Modbus 相关 -->
<dependency>
<groupId>com.ghgande</groupId>
<artifactId>j2mod</artifactId>
<version>${j2mod.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>

View File

@ -8,8 +8,8 @@ package cn.iocoder.yudao.module.iot.enums;
public class DictTypeConstants {
public static final String NET_TYPE = "iot_net_type";
public static final String LOCATION_TYPE = "iot_location_type";
public static final String CODEC_TYPE = "iot_codec_type";
public static final String PROTOCOL_TYPE = "iot_protocol_type";
public static final String SERIALIZE_TYPE = "iot_serialize_type";
public static final String PRODUCT_STATUS = "iot_product_status";
public static final String PRODUCT_DEVICE_TYPE = "iot_product_device_type";

View File

@ -54,6 +54,14 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在");
ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除");
// ========== 设备 Modbus 配置 1-050-006-000 ==========
ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在");
ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在");
// ========== 设备 Modbus 点位 1-050-007-000 ==========
ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在");
ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在");
// ========== OTA 固件相关 1-050-008-000 ==========
ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在");

View File

@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable<Integer> {
TCP(2, "TCP"),
WEBSOCKET(3, "WebSocket"),
MQTT(10, "MQTT"), // TODO 待实现;
MQTT(10, "MQTT"), // TODO @puhui999待实现;
DATABASE(20, "Database"), // TODO @puhui999待实现可以简单点,对应的表名是什么,字段先固定了。
DATABASE(20, "Database"), // TODO @puhui999待实现
REDIS(21, "Redis"),
ROCKETMQ(30, "RocketMQ"),

View File

@ -18,13 +18,13 @@ public enum IotSceneRuleActionTypeEnum implements ArrayValuable<Integer> {
/**
*
*
* {@link IotDeviceMessageMethodEnum#PROPERTY_SET}
* {@link cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum#PROPERTY_SET}
*/
DEVICE_PROPERTY_SET(1),
/**
*
*
* {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE}
* {@link cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum#SERVICE_INVOKE}
*/
DEVICE_SERVICE_INVOKE(2),

View File

@ -1,10 +1,7 @@
package cn.iocoder.yudao.module.iot.core.biz;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.*;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
@ -50,4 +47,12 @@ public interface IotDeviceCommonApi {
*/
CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
/**
* Modbus
*
* @param listReqDTO
* @return Modbus
*/
CommonResult<List<IotModbusDeviceConfigRespDTO>> getModbusDeviceConfigList(IotModbusDeviceConfigListReqDTO listReqDTO);
}

View File

@ -34,8 +34,12 @@ public class IotDeviceRespDTO {
*/
private Long productId;
/**
*
*
*/
private String codecType;
private String protocolType;
/**
*
*/
private String serializeType;
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Set;
/**
* IoT Modbus Request DTO
*
* @author
*/
@Data
@Accessors(chain = true)
public class IotModbusDeviceConfigListReqDTO {
/**
*
*/
private Integer status;
/**
*
*/
private Integer mode;
/**
*
*/
private String protocolType;
/**
* ID
*/
private Set<Long> deviceIds;
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import lombok.Data;
import java.util.List;
/**
* IoT Modbus Response DTO
*
* @author
*/
@Data
public class IotModbusDeviceConfigRespDTO {
/**
*
*/
private Long deviceId;
/**
*
*/
private String productKey;
/**
*
*/
private String deviceName;
// ========== Modbus 连接配置 ==========
/**
* Modbus IP
*/
private String ip;
/**
* Modbus
*/
private Integer port;
/**
*
*/
private Integer slaveId;
/**
*
*/
private Integer timeout;
/**
*
*/
private Integer retryInterval;
/**
*
*/
private Integer mode;
/**
*
*/
private Integer frameFormat;
// ========== Modbus 点位配置 ==========
/**
*
*/
private List<IotModbusPointRespDTO> points;
}

View File

@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
import lombok.Data;
import java.math.BigDecimal;
/**
* IoT Modbus Response DTO
*
* @author
*/
@Data
public class IotModbusPointRespDTO {
/**
*
*/
private Long id;
/**
* identifier
*/
private String identifier;
/**
* name
*/
private String name;
// ========== Modbus 协议配置 ==========
/**
* Modbus
*
* FC01-04线
*/
private Integer functionCode;
/**
*
*/
private Integer registerAddress;
/**
*
*/
private Integer registerCount;
/**
*
*
* {@link IotModbusByteOrderEnum}
*/
private String byteOrder;
/**
*
*
* {@link IotModbusRawDataTypeEnum}
*/
private String rawDataType;
/**
*
*/
private BigDecimal scale;
/**
*
*/
private Integer pollInterval;
}

View File

@ -64,7 +64,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> {
// ========== OTA 固件 ==========
// 可参考https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates
OTA_UPGRADE("thing.ota.upgrade", "OTA 固信息推送", false),
OTA_UPGRADE("thing.ota.upgrade", "OTA 固信息推送", false),
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
;

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.iot.core.enums;
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 IotProtocolTypeEnum implements ArrayValuable<String> {
TCP("tcp"),
UDP("udp"),
WEBSOCKET("websocket"),
HTTP("http"),
MQTT("mqtt"),
EMQX("emqx"),
COAP("coap"),
MODBUS_TCP_CLIENT("modbus_tcp_client"),
MODBUS_TCP_SERVER("modbus_tcp_server");
public static final String[] ARRAYS = Arrays.stream(values()).map(IotProtocolTypeEnum::getType).toArray(String[]::new);
/**
*
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
public static IotProtocolTypeEnum of(String type) {
return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
}
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.iot.core.enums;
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 IotSerializeTypeEnum implements ArrayValuable<String> {
JSON("json"),
BINARY("binary");
public static final String[] ARRAYS = Arrays.stream(values()).map(IotSerializeTypeEnum::getType).toArray(String[]::new);
/**
*
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
public static IotSerializeTypeEnum of(String type) {
return ArrayUtil.firstMatch(e -> e.getType().equals(type), values());
}
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.core.enums;
package cn.iocoder.yudao.module.iot.core.enums.device;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.iot.core.enums.modbus;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus
*
* @author
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusByteOrderEnum implements ArrayValuable<String> {
AB("AB", "大端序16位", 2),
BA("BA", "小端序16位", 2),
ABCD("ABCD", "大端序32位", 4),
CDAB("CDAB", "大端字交换32位", 4),
DCBA("DCBA", "小端序32位", 4),
BADC("BADC", "小端字交换32位", 4);
public static final String[] ARRAYS = Arrays.stream(values())
.map(IotModbusByteOrderEnum::getOrder)
.toArray(String[]::new);
/**
*
*/
private final String order;
/**
*
*/
private final String name;
/**
*
*/
private final Integer byteCount;
@Override
public String[] array() {
return ARRAYS;
}
public static IotModbusByteOrderEnum getByOrder(String order) {
return Arrays.stream(values())
.filter(e -> e.getOrder().equals(order))
.findFirst()
.orElse(null);
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.iot.core.enums.modbus;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus
*
* @author
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusFrameFormatEnum implements ArrayValuable<Integer> {
MODBUS_TCP(1),
MODBUS_RTU(2);
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(IotModbusFrameFormatEnum::getFormat)
.toArray(Integer[]::new);
/**
*
*/
private final Integer format;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.core.enums.modbus;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus
*
* @author
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusModeEnum implements ArrayValuable<Integer> {
POLLING(1, "云端轮询"),
ACTIVE_REPORT(2, "边缘采集");
public static final Integer[] ARRAYS = Arrays.stream(values())
.map(IotModbusModeEnum::getMode)
.toArray(Integer[]::new);
/**
*
*/
private final Integer mode;
/**
*
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.iot.core.enums.modbus;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT Modbus
*
* @author
*/
@Getter
@RequiredArgsConstructor
public enum IotModbusRawDataTypeEnum implements ArrayValuable<String> {
INT16("INT16", "有符号 16 位整数", 1),
UINT16("UINT16", "无符号 16 位整数", 1),
INT32("INT32", "有符号 32 位整数", 2),
UINT32("UINT32", "无符号 32 位整数", 2),
FLOAT("FLOAT", "32 位浮点数", 2),
DOUBLE("DOUBLE", "64 位浮点数", 4),
BOOLEAN("BOOLEAN", "布尔值(用于线圈)", 1),
STRING("STRING", "字符串", null); // null 表示可变长度
public static final String[] ARRAYS = Arrays.stream(values())
.map(IotModbusRawDataTypeEnum::getType)
.toArray(String[]::new);
/**
*
*/
private final String type;
/**
*
*/
private final String name;
/**
* null
*/
private final Integer registerCount;
@Override
public String[] array() {
return ARRAYS;
}
public static IotModbusRawDataTypeEnum getByType(String type) {
return Arrays.stream(values())
.filter(e -> e.getType().equals(type))
.findFirst()
.orElse(null);
}
}

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.iot.core.messagebus.config;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
/**
* IoT 线
*

View File

@ -24,4 +24,14 @@ public interface IotMessageBus {
*/
void register(IotMessageSubscriber<?> subscriber);
/**
*
*
* @param subscriber
*/
default void unregister(IotMessageSubscriber<?> subscriber) {
// TODO 芋艿:暂时不实现,需求量不大,但是
// throw new UnsupportedOperationException("取消注册消息订阅者功能,尚未实现");
}
}

View File

@ -26,4 +26,16 @@ public interface IotMessageSubscriber<T> {
*/
void onMessage(T message);
/**
*
*/
default void start() {
}
/**
*
*/
default void stop() {
}
}

View File

@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.iot.core.mq.message;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.topic.state.IotDeviceStateUpdateReqDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -60,7 +60,7 @@ public class IotDeviceMessage {
*/
private String serverId;
// ========== codec编解码字段 ==========
// ========== serialize序列化相关字段 ==========
/**
*
@ -72,7 +72,7 @@ public class IotDeviceMessage {
*
*
* {@link IotDeviceMessageMethodEnum}
* thing.property.report
* thing.property.post
*/
private String method;
/**
@ -94,7 +94,7 @@ public class IotDeviceMessage {
*/
private String msg;
// ========== 基础方法:只传递"codec编解码字段" ==========
// ========== 基础方法:只传递"serialize序列化相关字段" ==========
public static IotDeviceMessage requestOf(String method) {
return requestOf(null, method, null);
@ -108,6 +108,23 @@ public class IotDeviceMessage {
return of(requestId, method, params, null, null, null);
}
/**
*
*
* @param deviceId
* @param tenantId
* @param serverId
* @param method
* @param params
* @return
*/
public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId,
String method, Object params) {
IotDeviceMessage message = of(null, method, params, null, null, null);
return message.setId(IotDeviceMessageUtils.generateMessageId())
.setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId);
}
public static IotDeviceMessage replyOf(String requestId, String method,
Object data, Integer code, String msg) {
if (code == null) {
@ -132,20 +149,12 @@ public class IotDeviceMessage {
public static IotDeviceMessage buildStateUpdateOnline() {
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
MapUtil.of("state", IotDeviceStateEnum.ONLINE.getState()));
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.ONLINE.getState()));
}
public static IotDeviceMessage buildStateOffline() {
return requestOf(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(),
MapUtil.of("state", IotDeviceStateEnum.OFFLINE.getState()));
}
public static IotDeviceMessage buildOtaUpgrade(String version, String fileUrl, Long fileSize,
String fileDigestAlgorithm, String fileDigestValue) {
return requestOf(IotDeviceMessageMethodEnum.OTA_UPGRADE.getMethod(), MapUtil.builder()
.put("version", version).put("fileUrl", fileUrl).put("fileSize", fileSize)
.put("fileDigestAlgorithm", fileDigestAlgorithm).put("fileDigestValue", fileDigestValue)
.build());
new IotDeviceStateUpdateReqDTO(IotDeviceStateEnum.OFFLINE.getState()));
}
}

View File

@ -1,13 +1,15 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* IoT Request DTO
* <p>
* /使 productSecret deviceSecret
* {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER} params
* <p>
* /使 productSecret deviceSecret
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification"> - </a>
@ -28,9 +30,11 @@ public class IotDeviceRegisterReqDTO {
private String deviceName;
/**
*
*
*
* @see cn.iocoder.yudao.module.iot.core.util.IotProductAuthUtils#buildSign(String, String, String)
*/
@NotEmpty(message = "产品密钥不能为空")
private String productSecret;
@NotEmpty(message = "签名不能为空")
private String sign;
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
/**
* IoT Response DTO
* <p>
* /
* {@link IotDeviceMessageMethodEnum#DEVICE_REGISTER}
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification"> - </a>

View File

@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* IoT Request DTO
* <p>
* thing.auth.register.sub params
*
* {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER} params
* <p>
* {@link #deviceName} deviceSecret
*
* @author

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.auth;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -7,7 +8,7 @@ import lombok.NoArgsConstructor;
/**
* IoT Response DTO
* <p>
* thing.auth.register.sub
* {@link IotDeviceMessageMethodEnum#SUB_DEVICE_REGISTER}
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices"> - </a>

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.iot.core.topic.config;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#CONFIG_PUSH} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1"> - </a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceConfigPushReqDTO {
/**
*
*/
private String configId;
/**
*
*/
private Long configSize;
/**
*
*/
private String signMethod;
/**
*
*/
private String sign;
/**
*
*/
private String url;
/**
*
* <p>
* file:
* content:
*/
private String getType;
}

View File

@ -1,11 +1,12 @@
package cn.iocoder.yudao.module.iot.core.topic.event;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.Data;
/**
* IoT Request DTO
* <p>
* thing.event.post params
* {@link IotDeviceMessageMethodEnum#EVENT_POST} params
*
* @author
* @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events"> - </a>

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.iot.core.topic.ota;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT OTA Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#OTA_PROGRESS} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates"> - OTA </a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceOtaProgressReqDTO {
/**
*
*/
private String version;
/**
*
*/
private Integer status;
/**
*
*/
private String description;
/**
* 0-100
*/
private Integer progress;
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.iot.core.topic.ota;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT OTA Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#OTA_UPGRADE} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/perform-ota-updates"> - OTA </a>
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceOtaUpgradeReqDTO {
/**
*
*/
private String version;
/**
*
*/
private String fileUrl;
/**
*
*/
private Long fileSize;
/**
*
*/
private String fileDigestAlgorithm;
/**
*
*/
private String fileDigestValue;
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.property;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.Data;
@ -9,7 +10,7 @@ import java.util.Map;
/**
* IoT Request DTO
* <p>
* thing.event.property.pack.post params
* {@link IotDeviceMessageMethodEnum#PROPERTY_PACK_POST} params
*
* @author
* @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches"> - </a>

View File

@ -1,12 +1,14 @@
package cn.iocoder.yudao.module.iot.core.topic.property;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import java.util.HashMap;
import java.util.Map;
/**
* IoT Request DTO
* <p>
* thing.property.post params
* {@link IotDeviceMessageMethodEnum#PROPERTY_POST} params
* <p>
* Mapkey value
*

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.core.topic.property;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import java.util.HashMap;
import java.util.Map;
/**
* IoT Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#PROPERTY_SET} params
* <p>
* Mapkey value
*
* @author
*/
public class IotDevicePropertySetReqDTO extends HashMap<String, Object> {
public IotDevicePropertySetReqDTO() {
super();
}
public IotDevicePropertySetReqDTO(Map<String, Object> properties) {
super(properties);
}
/**
* DTO
*
* @param properties
* @return DTO
*/
public static IotDevicePropertySetReqDTO of(Map<String, Object> properties) {
return new IotDevicePropertySetReqDTO(properties);
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.core.topic.service;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* IoT Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#SERVICE_INVOKE} params
*
* @author
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceServiceInvokeReqDTO {
/**
*
*/
private String identifier;
/**
*
*/
private Map<String, Object> inputParams;
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.iot.core.topic.state;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT Request DTO
* <p>
* {@link IotDeviceMessageMethodEnum#STATE_UPDATE} params
*
* @author
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceStateUpdateReqDTO {
/**
*
*/
private Integer state;
}

View File

@ -1,15 +1,16 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* IoT Request DTO
* <p>
* thing.topo.add params
* {@link IotDeviceMessageMethodEnum#TOPO_ADD} params
*
* @author
* @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship"> - </a>

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -10,7 +11,7 @@ import java.util.List;
/**
* IoT Request DTO
* <p>
* thing.topo.change params
* {@link IotDeviceMessageMethodEnum#TOPO_CHANGE} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes"> - </a>

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.Data;
@ -10,7 +11,7 @@ import java.util.List;
/**
* IoT Request DTO
* <p>
* thing.topo.delete params
* {@link IotDeviceMessageMethodEnum#TOPO_DELETE} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship"> - </a>

View File

@ -1,11 +1,12 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import lombok.Data;
/**
* IoT Request DTO
* <p>
* thing.topo.get params
* {@link IotDeviceMessageMethodEnum#TOPO_GET} params
*
* @author
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship"> - </a>

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.core.topic.topo;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import lombok.Data;
@ -8,7 +9,7 @@ import java.util.List;
/**
* IoT Response DTO
* <p>
* thing.topo.get
* {@link IotDeviceMessageMethodEnum#TOPO_GET}
*
* @author
* @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship"> - </a>

View File

@ -25,6 +25,14 @@ public class IotDeviceAuthUtils {
return String.format("%s.%s", productKey, deviceName);
}
public static String buildClientIdFromUsername(String username) {
IotDeviceIdentity identity = parseUsername(username);
if (identity == null) {
return null;
}
return buildClientId(identity.getProductKey(), identity.getDeviceName());
}
public static String buildUsername(String productKey, String deviceName) {
return String.format("%s&%s", deviceName, productKey);
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
/**
* IoT
* <p>
* 使 productSecret
*
* @author
*/
public class IotProductAuthUtils {
/**
*
*
* @param productKey
* @param deviceName
* @param productSecret
* @return
*/
public static String buildSign(String productKey, String deviceName, String productSecret) {
String content = buildContent(productKey, deviceName);
return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(productSecret))
.digestHex(content);
}
/**
*
*
* @param productKey
* @param deviceName
* @param productSecret
* @param sign
* @return
*/
public static boolean verifySign(String productKey, String deviceName, String productSecret, String sign) {
String expectedSign = buildSign(productKey, deviceName, productSecret);
return expectedSign.equals(sign);
}
/**
*
*
* @param productKey
* @param deviceName
* @return
*/
private static String buildContent(String productKey, String deviceName) {
return "deviceName" + deviceName + "productKey" + productKey;
}
}

View File

@ -33,7 +33,7 @@
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<!-- TODO @芋艿:消息队列,后续可能去掉,默认不使用 rocketmq -->
<!-- <optional>true</optional> -->
<optional>true</optional>
</dependency>
<!-- 工具类相关 -->
@ -48,6 +48,12 @@
<artifactId>vertx-mqtt</artifactId>
</dependency>
<!-- Modbus 相关 -->
<dependency>
<groupId>com.ghgande</groupId>
<artifactId>j2mod</artifactId>
</dependency>
<!-- CoAP 相关 - Eclipse Californium -->
<dependency>
<groupId>org.eclipse.californium</groupId>

View File

@ -1,33 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
/**
* {@link IotDeviceMessage}
*
* @author
*/
public interface IotDeviceMessageCodec {
/**
*
*
* @param message
* @return
*/
byte[] encode(IotDeviceMessage message);
/**
*
*
* @param bytes
* @return
*/
IotDeviceMessage decode(byte[] bytes);
/**
* @return
*/
String type();
}

View File

@ -1,89 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.alink;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* Alink {@link IotDeviceMessage}
*
* @author
*/
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "Alink";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class AlinkMessage {
public static final String VERSION_1 = "1.0";
/**
* ID ID
*/
private String id;
/**
*
*/
private String version;
/**
*
*/
private String method;
/**
*
*/
private Object params;
/**
*
*/
private Object data;
/**
*
*/
private Integer code;
/**
*
*
* message {@link CommonResult#getMsg()}
*/
private String msg;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1,
message.getMethod(), message.getParams(), message.getData(), message.getCode(), message.getMsg());
return JsonUtils.toJsonByte(alinkMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
AlinkMessage alinkMessage = JsonUtils.parseObject(bytes, AlinkMessage.class);
Assert.notNull(alinkMessage, "消息不能为空");
Assert.equals(alinkMessage.getVersion(), AlinkMessage.VERSION_1, "消息版本号必须是 1.0");
return IotDeviceMessage.of(alinkMessage.getId(), alinkMessage.getMethod(), alinkMessage.getParams(),
alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg());
}
}

View File

@ -1,4 +0,0 @@
/**
*
*/
package cn.iocoder.yudao.module.iot.gateway.codec;

View File

@ -1,4 +0,0 @@
/**
* TODO @ alink xml
*/
package cn.iocoder.yudao.module.iot.gateway.codec.simple;

View File

@ -1,110 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.codec.tcp;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* TCP/UDP JSON {@link IotDeviceMessage}
*
* JSON
* {
* "id": "消息 ID",
* "method": "消息方法",
* "params": {...}, // 请求参数
* "data": {...}, // 响应结果
* "code": 200, // 响应错误码
* "msg": "success", // 响应提示
* "timestamp":
* }
*
* @author
*/
@Component
public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec {
public static final String TYPE = "TCP_JSON";
@Data
@NoArgsConstructor
@AllArgsConstructor
private static class TcpJsonMessage {
/**
* ID ID
*/
private String id;
/**
*
*/
private String method;
/**
*
*/
private Object params;
/**
*
*/
private Object data;
/**
*
*/
private Integer code;
/**
*
*/
private String msg;
/**
*
*/
private Long timestamp;
}
@Override
public String type() {
return TYPE;
}
@Override
public byte[] encode(IotDeviceMessage message) {
TcpJsonMessage tcpJsonMessage = new TcpJsonMessage(
message.getRequestId(),
message.getMethod(),
message.getParams(),
message.getData(),
message.getCode(),
message.getMsg(),
System.currentTimeMillis());
return JsonUtils.toJsonByte(tcpJsonMessage);
}
@Override
@SuppressWarnings("DataFlowIssue")
public IotDeviceMessage decode(byte[] bytes) {
String jsonStr = StrUtil.utf8Str(bytes).trim();
TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(jsonStr, TcpJsonMessage.class);
Assert.notNull(tcpJsonMessage, "消息不能为空");
Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空");
return IotDeviceMessage.of(
tcpJsonMessage.getId(),
tcpJsonMessage.getMethod(),
tcpJsonMessage.getParams(),
tcpJsonMessage.getData(),
tcpJsonMessage.getCode(),
tcpJsonMessage.getMsg());
}
}

View File

@ -1,254 +1,28 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocolManager;
import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* IoT
*
* @author
*/
@Configuration
@EnableConfigurationProperties(IotGatewayProperties.class)
@Slf4j
public class IotGatewayConfiguration {
/**
* IoT HTTP
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.http", name = "enabled", havingValue = "true")
@Slf4j
public static class HttpProtocolConfiguration {
@Bean(name = "httpVertx", destroyMethod = "close")
public Vertx httpVertx() {
return Vertx.vertx();
}
@Bean
public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("httpVertx") Vertx httpVertx) {
return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
}
@Bean
public IotHttpDownstreamSubscriber iotHttpDownstreamSubscriber(IotHttpUpstreamProtocol httpUpstreamProtocol,
IotMessageBus messageBus) {
return new IotHttpDownstreamSubscriber(httpUpstreamProtocol, messageBus);
}
@Bean
public IotMessageSerializerManager iotMessageSerializerManager() {
return new IotMessageSerializerManager();
}
/**
* IoT EMQX
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.emqx", name = "enabled", havingValue = "true")
@Slf4j
public static class EmqxProtocolConfiguration {
@Bean(name = "emqxVertx", destroyMethod = "close")
public Vertx emqxVertx() {
return Vertx.vertx();
}
@Bean
public IotEmqxAuthEventProtocol iotEmqxAuthEventProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxAuthEventProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxUpstreamProtocol iotEmqxUpstreamProtocol(IotGatewayProperties gatewayProperties,
@Qualifier("emqxVertx") Vertx emqxVertx) {
return new IotEmqxUpstreamProtocol(gatewayProperties.getProtocol().getEmqx(), emqxVertx);
}
@Bean
public IotEmqxDownstreamSubscriber iotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol mqttUpstreamProtocol,
IotMessageBus messageBus) {
return new IotEmqxDownstreamSubscriber(mqttUpstreamProtocol, messageBus);
}
}
/**
* IoT TCP
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.tcp", name = "enabled", havingValue = "true")
@Slf4j
public static class TcpProtocolConfiguration {
@Bean(name = "tcpVertx", destroyMethod = "close")
public Vertx tcpVertx() {
return Vertx.vertx();
}
@Bean
public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
@Qualifier("tcpVertx") Vertx tcpVertx) {
return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(),
deviceService, messageService, connectionManager, tcpVertx);
}
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
/**
* IoT MQTT
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt", name = "enabled", havingValue = "true")
@Slf4j
public static class MqttProtocolConfiguration {
@Bean(name = "mqttVertx", destroyMethod = "close")
public Vertx mqttVertx() {
return Vertx.vertx();
}
@Bean
public IotMqttUpstreamProtocol iotMqttUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager,
@Qualifier("mqttVertx") Vertx mqttVertx) {
return new IotMqttUpstreamProtocol(gatewayProperties.getProtocol().getMqtt(), messageService,
connectionManager, mqttVertx);
}
@Bean
public IotMqttDownstreamHandler iotMqttDownstreamHandler(IotDeviceMessageService messageService,
IotMqttConnectionManager connectionManager) {
return new IotMqttDownstreamHandler(messageService, connectionManager);
}
@Bean
public IotMqttDownstreamSubscriber iotMqttDownstreamSubscriber(IotMqttUpstreamProtocol mqttUpstreamProtocol,
IotMqttDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
return new IotMqttDownstreamSubscriber(mqttUpstreamProtocol, downstreamHandler, messageBus);
}
}
/**
* IoT UDP
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
@Slf4j
public static class UdpProtocolConfiguration {
@Bean(name = "udpVertx", destroyMethod = "close")
public Vertx udpVertx() {
return Vertx.vertx();
}
@Bean
public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
@Qualifier("udpVertx") Vertx udpVertx) {
return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
deviceService, messageService, sessionManager, udpVertx);
}
@Bean
public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotUdpSessionManager sessionManager,
IotMessageBus messageBus) {
return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
}
}
/**
* IoT CoAP
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
@Slf4j
public static class CoapProtocolConfiguration {
@Bean
public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
}
@Bean
public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
IotMessageBus messageBus) {
return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
}
}
/**
* IoT WebSocket
*/
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
@Slf4j
public static class WebSocketProtocolConfiguration {
@Bean(name = "websocketVertx", destroyMethod = "close")
public Vertx websocketVertx() {
return Vertx.vertx();
}
@Bean
public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
IotDeviceService deviceService,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
@Qualifier("websocketVertx") Vertx websocketVertx) {
return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
deviceService, messageService, connectionManager, websocketVertx);
}
@Bean
public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
IotWebSocketConnectionManager connectionManager,
IotMessageBus messageBus) {
return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
@Bean
public IotProtocolManager iotProtocolManager(IotGatewayProperties gatewayProperties) {
return new IotProtocolManager(gatewayProperties);
}
}

View File

@ -1,11 +1,22 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpConfig;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketConfig;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.List;
@ -24,9 +35,9 @@ public class IotGatewayProperties {
private TokenProperties token;
/**
*
*
*/
private ProtocolProperties protocol;
private List<ProtocolProperties> protocols;
@Data
public static class RpcProperties {
@ -65,582 +76,158 @@ public class IotGatewayProperties {
}
/**
*
*/
@Data
public static class ProtocolProperties {
/**
* HTTP
* ID "http-alink""tcp-binary"
*/
private HttpProperties http;
@NotEmpty(message = "协议实例 ID 不能为空")
private String id;
/**
* EMQX
*
*/
private EmqxProperties emqx;
@NotNull(message = "是否启用不能为空")
private Boolean enabled = true;
/**
* TCP
*
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum
*/
private TcpProperties tcp;
/**
* MQTT
*/
private MqttProperties mqtt;
/**
* MQTT WebSocket
*/
private MqttWsProperties mqttWs;
/**
* UDP
*/
private UdpProperties udp;
/**
* CoAP
*/
private CoapProperties coap;
/**
* WebSocket
*/
private WebSocketProperties websocket;
}
@Data
public static class HttpProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
@NotEmpty(message = "协议类型不能为空")
private String protocol;
/**
*
*/
private Integer serverPort;
/**
* SSL
*/
@NotNull(message = "是否开启 SSL 不能为空")
private Boolean sslEnabled = false;
/**
* SSL
*/
private String sslKeyPath;
/**
* SSL
*/
private String sslCertPath;
}
@Data
public static class EmqxProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* HTTP 8090
*/
private Integer httpPort = 8090;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 服务器地址不能为空")
private String mqttHost;
/**
* MQTT 1883
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort = 1883;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 用户名不能为空")
private String mqttUsername;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 密码不能为空")
private String mqttPassword;
/**
* MQTT SSL
*/
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
private Boolean mqttSsl = false;
/**
* MQTT ID
*/
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
private String mqttClientId;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 主题不能为空")
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
/**
* QoS
* <p>
* 0 -
* 1 -
* 2 -
*
* 1. TCP/UDP/HTTP/WebSocket/MQTT/CoAP
* 2. EMQX EMQX HTTP Hook /mqtt/auth/mqtt/acl/mqtt/event
*/
private Integer mqttQos = 1;
@NotNull(message = "服务端口不能为空")
private Integer port;
/**
*
*
* @see cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum
*
*
* 1. {@link IotProtocolTypeEnum#HTTP}{@link IotProtocolTypeEnum#COAP} JSON
* 2. {@link IotProtocolTypeEnum#EMQX}
*/
private String serialize;
// ========== SSL 配置 ==========
/**
*
* SSL null
*/
private Integer connectTimeoutSeconds = 10;
@Valid
private SslConfig ssl;
// ========== 各协议配置 ==========
/**
*
* HTTP
*/
private Long reconnectDelayMs = 5000L;
@Valid
private IotHttpConfig http;
/**
* WebSocket
*/
@Valid
private IotWebSocketConfig websocket;
/**
* Clean Session ()
* true: Broker 线
* 线 true
* TCP
*/
private Boolean cleanSession = true;
@Valid
private IotTcpConfig tcp;
/**
* UDP
*/
@Valid
private IotUdpConfig udp;
/**
*
*
* CoAP
*/
private Integer keepAliveIntervalSeconds = 60;
@Valid
private IotCoapConfig coap;
/**
*
* Broker QoS 1/2
* MQTT
*/
private Integer maxInflightQueue = 10000;
@Valid
private IotMqttConfig mqtt;
/**
* EMQX
*/
@Valid
private IotEmqxConfig emqx;
/**
* SSL
* 使
* false
* Modbus TCP Client
*/
private Boolean trustAll = false;
@Valid
private IotModbusTcpClientConfig modbusTcpClient;
/**
* (线)
* Modbus TCP Server
*/
private final Will will = new Will();
/**
* SSL/TLS ()
*/
private final Ssl sslOptions = new Ssl();
/**
* (Last Will and Testament)
*/
@Data
public static class Will {
/**
*
*/
private boolean enabled = false;
/**
*
*/
private String topic;
/**
*
*/
private String payload;
/**
* QoS
*/
private Integer qos = 1;
/**
*
*/
private boolean retain = true;
}
/**
* SSL/TLS
*/
@Data
public static class Ssl {
/**
* KeyStoreclasspath:certs/client.jks
*
*/
private String keyStorePath;
/**
*
*/
private String keyStorePassword;
/**
* TrustStoreclasspath:certs/trust.jks
* CA
*/
private String trustStorePath;
/**
*
*/
private String trustStorePassword;
}
@Valid
private IotModbusTcpServerConfig modbusTcpServer;
}
/**
* SSL
*/
@Data
public static class TcpProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
*
*/
private Integer port = 8091;
/**
*
*/
private Long keepAliveTimeoutMs = 30000L;
/**
*
*/
private Integer maxConnections = 1000;
/**
* SSL
*/
private Boolean sslEnabled = false;
/**
* SSL
*/
private String sslCertPath;
/**
* SSL
*/
private String sslKeyPath;
}
@Data
public static class MqttProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
*
*/
private Integer port = 1883;
/**
*
*/
private Integer maxMessageSize = 8192;
/**
*
*/
private Integer connectTimeoutSeconds = 60;
/**
*
*/
private Integer keepAliveTimeoutSeconds = 300;
public static class SslConfig {
/**
* SSL
*/
private Boolean sslEnabled = false;
/**
* SSL
*/
private SslOptions sslOptions = new SslOptions();
/**
* SSL
*/
@Data
public static class SslOptions {
/**
*
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
*
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL
*/
private String certPath;
/**
* SSL
*/
private String keyPath;
/**
*
*/
private String trustStorePath;
/**
*
*/
private String trustStorePassword;
}
}
@Data
public static class MqttWsProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* WebSocket 8083
*/
private Integer port = 8083;
/**
* WebSocket /mqtt
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/mqtt";
/**
*
*/
private Integer maxMessageSize = 8192;
/**
*
*/
private Integer connectTimeoutSeconds = 60;
/**
*
*/
private Integer keepAliveTimeoutSeconds = 300;
/**
* SSLwss://
*/
private Boolean sslEnabled = false;
/**
* SSL
*/
private SslOptions sslOptions = new SslOptions();
/**
* WebSocket "mqtt" "mqttv3.1"
*/
@NotEmpty(message = "WebSocket 子协议不能为空")
private String subProtocol = "mqtt";
/**
*
*/
private Integer maxFrameSize = 65536;
/**
* SSL
*/
@Data
public static class SslOptions {
/**
*
*/
private io.vertx.core.net.KeyCertOptions keyCertOptions;
/**
*
*/
private io.vertx.core.net.TrustOptions trustOptions;
/**
* SSL
*/
private String certPath;
/**
* SSL
*/
private String keyPath;
/**
*
*/
private String trustStorePath;
/**
*
*/
private String trustStorePassword;
}
}
@Data
public static class UdpProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 8093
*/
private Integer port = 8093;
/**
* 64KB
*/
private Integer receiveBufferSize = 65536;
/**
* 64KB
*/
private Integer sendBufferSize = 65536;
/**
* 60
* <p>
*
*/
private Long sessionTimeoutMs = 60000L;
/**
* 30
*/
private Long sessionCleanIntervalMs = 30000L;
}
@Data
public static class CoapProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* CoAP 5683
*/
@NotNull(message = "服务端口不能为空")
private Integer port = 5683;
/**
*
*/
@NotNull(message = "最大消息大小不能为空")
private Integer maxMessageSize = 1024;
/**
* ACK
*/
@NotNull(message = "ACK 超时时间不能为空")
private Integer ackTimeout = 2000;
/**
*
*/
@NotNull(message = "最大重传次数不能为空")
private Integer maxRetransmit = 4;
}
@Data
public static class WebSocketProperties {
/**
*
*/
@NotNull(message = "是否开启不能为空")
private Boolean enabled;
/**
* 8094
*/
private Integer port = 8094;
/**
* WebSocket /ws
*/
@NotEmpty(message = "WebSocket 路径不能为空")
private String path = "/ws";
/**
* 64KB
*/
private Integer maxMessageSize = 65536;
/**
* 64KB
*/
private Integer maxFrameSize = 65536;
/**
* 60
*/
private Integer idleTimeoutSeconds = 60;
/**
* SSLwss://
*/
private Boolean sslEnabled = false;
@NotNull(message = "是否启用 SSL 不能为空")
private Boolean ssl = false;
/**
* SSL
*/
@NotEmpty(message = "SSL 证书路径不能为空")
private String sslCertPath;
/**
* SSL
*/
@NotEmpty(message = "SSL 私钥路径不能为空")
private String sslKeyPath;
/**
* KeyStore
* <p>
*
*/
private String keyStorePath;
/**
*
*/
private String keyStorePassword;
/**
* TrustStore
* <p>
* CA
*/
private String trustStorePath;
/**
*
*/
private String trustStorePassword;
}
}

View File

@ -1,50 +1,53 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxDownstreamHandler;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
/**
* IoT EMQX
* IoT
*
* 线
*
* @author
*/
@AllArgsConstructor
@Slf4j
public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
public abstract class AbstractIotProtocolDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotEmqxDownstreamHandler downstreamHandler;
private final IotProtocol protocol;
private final IotMessageBus messageBus;
private final IotEmqxUpstreamProtocol protocol;
public IotEmqxDownstreamSubscriber(IotEmqxUpstreamProtocol protocol, IotMessageBus messageBus) {
this.protocol = protocol;
this.messageBus = messageBus;
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
}
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
/**
* Group使 Topic Group
*/
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void start() {
messageBus.register(this);
log.info("[start][{} 下行消息订阅成功Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void stop() {
messageBus.unregister(this);
log.info("[stop][{} 下行消息订阅已停止Topic{}]", protocol.getType().name(), getTopic());
}
@Override
public void onMessage(IotDeviceMessage message) {
log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]",
@ -52,18 +55,25 @@ public class IotEmqxDownstreamSubscriber implements IotMessageSubscriber<IotDevi
try {
// 1. 校验
String method = message.getMethod();
if (method == null) {
if (StrUtil.isBlank(method)) {
log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]",
message.getId(), message.getDeviceId());
return;
}
// 2. 处理下行消息
downstreamHandler.handle(message);
handleMessage(message);
} catch (Exception e) {
log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]",
message.getId(), message.getMethod(), message.getDeviceId(), e);
}
}
}
/**
*
*
* @param message
*/
protected abstract void handleMessage(IotDeviceMessage message);
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
/**
* IoT
*
*
*
* @author
*/
public interface IotProtocol {
/**
* ID
*
* @return ID "http-alink""tcp-binary"
*/
String getId();
/**
* ID
*
* @return ID
*/
String getServerId();
/**
*
*
* @return
*/
IotProtocolTypeEnum getType();
/**
*
*/
void start();
/**
*
*/
void stop();
/**
*
*
* @return
*/
boolean isRunning();
}

View File

@ -0,0 +1,217 @@
package cn.iocoder.yudao.module.iot.gateway.protocol;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.IotModbusTcpServerProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketProtocol;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.SmartLifecycle;
import java.util.ArrayList;
import java.util.List;
/**
* IoT
*
* @author
*/
@Slf4j
public class IotProtocolManager implements SmartLifecycle {
private final IotGatewayProperties gatewayProperties;
/**
*
*/
private final List<IotProtocol> protocols = new ArrayList<>();
@Getter
private volatile boolean running = false;
public IotProtocolManager(IotGatewayProperties gatewayProperties) {
this.gatewayProperties = gatewayProperties;
}
@Override
public void start() {
if (running) {
return;
}
List<IotGatewayProperties.ProtocolProperties> protocolConfigs = gatewayProperties.getProtocols();
if (CollUtil.isEmpty(protocolConfigs)) {
log.info("[start][没有配置协议实例,跳过启动]");
return;
}
for (IotGatewayProperties.ProtocolProperties config : protocolConfigs) {
if (BooleanUtil.isFalse(config.getEnabled())) {
log.info("[start][协议实例 {} 未启用,跳过]", config.getId());
continue;
}
IotProtocol protocol = createProtocol(config);
if (protocol == null) {
continue;
}
protocol.start();
protocols.add(protocol);
}
running = true;
log.info("[start][协议管理器启动完成,共启动 {} 个协议实例]", protocols.size());
}
@Override
public void stop() {
if (!running) {
return;
}
for (IotProtocol protocol : protocols) {
try {
protocol.stop();
} catch (Exception e) {
log.error("[stop][协议实例 {} 停止失败]", protocol.getId(), e);
}
}
protocols.clear();
running = false;
log.info("[stop][协议管理器已停止]");
}
/**
*
*
* @param config
* @return
*/
@SuppressWarnings({"EnhancedSwitchMigration"})
private IotProtocol createProtocol(IotGatewayProperties.ProtocolProperties config) {
IotProtocolTypeEnum protocolType = IotProtocolTypeEnum.of(config.getProtocol());
if (protocolType == null) {
log.error("[createProtocol][协议实例 {} 的协议类型 {} 不存在]", config.getId(), config.getProtocol());
return null;
}
switch (protocolType) {
case HTTP:
return createHttpProtocol(config);
case TCP:
return createTcpProtocol(config);
case UDP:
return createUdpProtocol(config);
case COAP:
return createCoapProtocol(config);
case WEBSOCKET:
return createWebSocketProtocol(config);
case MQTT:
return createMqttProtocol(config);
case EMQX:
return createEmqxProtocol(config);
case MODBUS_TCP_CLIENT:
return createModbusTcpClientProtocol(config);
case MODBUS_TCP_SERVER:
return createModbusTcpServerProtocol(config);
default:
throw new IllegalArgumentException(String.format(
"[createProtocol][协议实例 %s 的协议类型 %s 暂不支持]", config.getId(), protocolType));
}
}
/**
* HTTP
*
* @param config
* @return HTTP
*/
private IotHttpProtocol createHttpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotHttpProtocol(config);
}
/**
* TCP
*
* @param config
* @return TCP
*/
private IotTcpProtocol createTcpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotTcpProtocol(config);
}
/**
* UDP
*
* @param config
* @return UDP
*/
private IotUdpProtocol createUdpProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotUdpProtocol(config);
}
/**
* CoAP
*
* @param config
* @return CoAP
*/
private IotCoapProtocol createCoapProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotCoapProtocol(config);
}
/**
* WebSocket
*
* @param config
* @return WebSocket
*/
private IotWebSocketProtocol createWebSocketProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotWebSocketProtocol(config);
}
/**
* MQTT
*
* @param config
* @return MQTT
*/
private IotMqttProtocol createMqttProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotMqttProtocol(config);
}
/**
* EMQX
*
* @param config
* @return EMQX
*/
private IotEmqxProtocol createEmqxProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotEmqxProtocol(config);
}
/**
* Modbus TCP Client
*
* @param config
* @return Modbus TCP Client
*/
private IotModbusTcpClientProtocol createModbusTcpClientProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpClientProtocol(config);
}
/**
* Modbus TCP Server
*
* @param config
* @return Modbus TCP Server
*/
private IotModbusTcpServerProtocol createModbusTcpServerProtocol(IotGatewayProperties.ProtocolProperties config) {
return new IotModbusTcpServerProtocol(config);
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT CoAP
*
* @author
*/
@Data
public class IotCoapConfig {
/**
*
*/
@NotNull(message = "最大消息大小不能为空")
@Min(value = 64, message = "最大消息大小必须大于 64 字节")
private Integer maxMessageSize = 1024;
/**
* ACK
*/
@NotNull(message = "ACK 超时时间不能为空")
@Min(value = 100, message = "ACK 超时时间必须大于 100 毫秒")
private Integer ackTimeoutMs = 2000;
/**
*
*/
@NotNull(message = "最大重传次数不能为空")
@Min(value = 0, message = "最大重传次数必须大于等于 0")
private Integer maxRetransmit = 4;
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
/**
* IoT CoAP
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotCoapUpstreamProtocol protocol;
private final IotMessageBus messageBus;
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@ -0,0 +1,168 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream.IotCoapDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import java.util.concurrent.TimeUnit;
/**
* IoT CoAP
* <p>
* Eclipse Californium
* 1. POST /auth
* 2. POST /auth/register/device
* 3. POST /auth/register/sub-device/{productKey}/{deviceName}
* 4. POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 5. POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author
*/
@Slf4j
public class IotCoapProtocol implements IotProtocol {
/**
*
*/
private final ProtocolProperties properties;
/**
* ID
*/
@Getter
private final String serverId;
/**
*
*/
@Getter
private volatile boolean running = false;
/**
* CoAP
*/
private CoapServer coapServer;
/**
*
*/
private IotCoapDownstreamSubscriber downstreamSubscriber;
public IotCoapProtocol(ProtocolProperties properties) {
IotCoapConfig coapConfig = properties.getCoap();
Assert.notNull(coapConfig, "CoAP 协议配置coap不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.COAP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT CoAP 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 创建 CoAP 配置
IotCoapConfig coapConfig = properties.getCoap();
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, properties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapConfig.getMaxMessageSize());
config.set(CoapConfig.ACK_TIMEOUT, coapConfig.getAckTimeoutMs(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapConfig.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler(serverId);
IotCoapAuthResource authResource = new IotCoapAuthResource(authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
// 2.3 添加 /auth/register/sub-device/{productKey}/{deviceName} 子设备动态注册资源
IotCoapRegisterSubHandler registerSubHandler = new IotCoapRegisterSubHandler();
IotCoapRegisterSubResource registerSubResource = new IotCoapRegisterSubResource(registerSubHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
add(registerSubResource);
}});
// 2.4 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler(serverId);
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(serverId, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
running = true;
log.info("[start][IoT CoAP 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 4. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotCoapDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT CoAP 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT CoAP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2. 关闭 CoAP 服务器
if (coapServer != null) {
try {
coapServer.stop();
coapServer.destroy();
coapServer = null;
log.info("[stop][IoT CoAP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT CoAP 协议 {} 服务器停止失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT CoAP 协议 {} 已停止]", getId());
}
}

View File

@ -1,90 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.CoapServer;
import org.eclipse.californium.core.config.CoapConfig;
import org.eclipse.californium.elements.config.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.TimeUnit;
/**
* IoT CoAP
*
* Eclipse Californium
* 1. POST /auth
* 2. POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 3. POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* @author
*/
@Slf4j
public class IotCoapUpstreamProtocol {
private final IotGatewayProperties.CoapProperties coapProperties;
private CoapServer coapServer;
@Getter
private final String serverId;
public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
this.coapProperties = coapProperties;
this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
}
@PostConstruct
public void start() {
try {
// 1.1 创建网络配置Californium 3.x API
Configuration config = Configuration.createStandardWithoutFile();
config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
// 1.2 创建 CoAP 服务器
coapServer = new CoapServer(config);
// 2.1 添加 /auth 认证资源
IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
coapServer.add(authResource);
// 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
authResource.add(new CoapResource("register") {{
add(registerResource);
}});
// 2.3 添加 /topic 根资源(用于上行消息)
IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
coapServer.add(topicResource);
// 3. 启动服务器
coapServer.start();
log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
} catch (Exception e) {
log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (coapServer != null) {
try {
coapServer.stop();
log.info("[stop][IoT 网关 CoAP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
}
}
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT CoAP
*
* @author
*/
@Slf4j
public class IotCoapDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotCoapDownstreamSubscriber(IotCoapProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
// 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
log.warn("[handleMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
}
}

View File

@ -0,0 +1,186 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
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.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* IoT CoAP
*
* @author
*/
@Slf4j
public abstract class IotCoapAbstractHandler {
/**
* CoAP Option Token
* <p>
* CoAP Option 2048-65535 /
*/
public static final int OPTION_TOKEN = 2088;
private final IotDeviceTokenService deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
/**
* CoAP
*
* @param exchange CoAP
*/
public final void handle(CoapExchange exchange) {
try {
// 1. 前置处理
beforeHandle(exchange);
// 2. 执行业务逻辑
CommonResult<Object> result = handle0(exchange);
writeResponse(exchange, result);
} catch (ServiceException e) {
// 业务异常,返回对应的错误码和消息
writeResponse(exchange, CommonResult.error(e.getCode(), e.getMessage()));
} catch (IllegalArgumentException e) {
// 参数校验异常hutool Assert 抛出),返回 BAD_REQUEST
writeResponse(exchange, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
} catch (Exception e) {
// 其他未知异常,返回 INTERNAL_SERVER_ERROR
log.error("[handle][CoAP 请求处理异常]", e);
writeResponse(exchange, CommonResult.error(INTERNAL_SERVER_ERROR));
}
}
/**
* CoAP
*
* @param exchange CoAP
* @return
*/
protected abstract CommonResult<Object> handle0(CoapExchange exchange);
/**
*
*
* @param exchange CoAP
*/
private void beforeHandle(CoapExchange exchange) {
// 1.1 如果不需要认证,则不走前置处理
if (!requiresAuthentication()) {
return;
}
// 1.2 从自定义 Option 获取 token
String token = getTokenFromOption(exchange);
if (StrUtil.isEmpty(token)) {
throw exception(UNAUTHORIZED);
}
// 1.3 校验 token
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
throw exception(UNAUTHORIZED);
}
// 2.1 解析 productKey 和 deviceName
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceName)) {
throw exception(BAD_REQUEST);
}
// 2.2 校验设备信息是否匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
throw exception(FORBIDDEN);
}
}
// ========== Token 相关方法 ==========
/**
*
* <p>
*
*
* @return
*/
protected boolean requiresAuthentication() {
return false;
}
/**
* URI productKey
* <p>
*
*
* @param uriPath URI
* @return productKey
*/
protected String getProductKey(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getProductKey 方法");
}
/**
* URI deviceName
* <p>
*
*
* @param uriPath URI
* @return deviceName
*/
protected String getDeviceName(List<String> uriPath) {
throw new UnsupportedOperationException("子类需要实现 getDeviceName 方法");
}
/**
* CoAP Option Token
*
* @param exchange CoAP
* @return Token null
*/
protected String getTokenFromOption(CoapExchange exchange) {
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
o -> o.getNumber() == OPTION_TOKEN);
return option != null ? new String(option.getValue()) : null;
}
// ========== 序列化相关方法 ==========
/**
*
*
* @param exchange CoAP
* @param clazz
* @param <T>
* @return null
*/
protected <T> T deserializeRequest(CoapExchange exchange, Class<T> clazz) {
byte[] payload = exchange.getRequestPayload();
if (ArrayUtil.isEmpty(payload)) {
return null;
}
return JsonUtils.parseObject(payload, clazz);
}
private static String serializeResponse(Object data) {
return JsonUtils.toJsonString(data);
}
protected void writeResponse(CoapExchange exchange, CommonResult<?> data) {
String json = serializeResponse(data);
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
}
}

View File

@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
/**
* IoT CoAP
*
* @author
*/
@Slf4j
public class IotCoapAuthHandler extends IotCoapAbstractHandler {
private final String serverId;
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService deviceMessageService;
public IotCoapAuthHandler(String serverId) {
this.serverId = serverId;
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1. 解析参数
IotDeviceAuthReqDTO request = deserializeRequest(exchange, IotDeviceAuthReqDTO.class);
Assert.notNull(request, "请求体不能为空");
Assert.notBlank(request.getClientId(), "clientId 不能为空");
Assert.notBlank(request.getUsername(), "username 不能为空");
Assert.notBlank(request.getPassword(), "password 不能为空");
// 2.1 执行认证
CommonResult<Boolean> result = deviceApi.authDevice(request);
result.checkError();
if (BooleanUtil.isFalse(result.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空");
// 3. 执行上线
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
// 4. 构建响应数据
return CommonResult.success(MapUtil.of("token", token));
}
}

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
@ -17,13 +16,10 @@ public class IotCoapAuthResource extends CoapResource {
public static final String PATH = "auth";
private final IotCoapUpstreamProtocol protocol;
private final IotCoapAuthHandler authHandler;
public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
IotCoapAuthHandler authHandler) {
public IotCoapAuthResource(IotCoapAuthHandler authHandler) {
super(PATH);
this.protocol = protocol;
this.authHandler = authHandler;
log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
}
@ -31,7 +27,7 @@ public class IotCoapAuthResource extends CoapResource {
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到 /auth POST 请求]");
authHandler.handle(exchange, protocol);
authHandler.handle(exchange);
}
}

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
/**
* IoT CoAP
* <p>
* /
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification"> - </a>
*/
@Slf4j
public class IotCoapRegisterHandler extends IotCoapAbstractHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1. 解析参数
IotDeviceRegisterReqDTO request = deserializeRequest(exchange, IotDeviceRegisterReqDTO.class);
Assert.notNull(request, "请求体不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
result.checkError();
// 3. 构建响应数据
return CommonResult.success(result.getData());
}
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;

View File

@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* IoT CoAP
* <p>
*
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices"> - </a>
*/
@Slf4j
public class IotCoapRegisterSubHandler extends IotCoapAbstractHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterSubHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1.1 解析通用参数(从 URI 路径获取网关设备信息)
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
// 1.2 解析子设备列表
SubDeviceRegisterRequest request = deserializeRequest(exchange, SubDeviceRegisterRequest.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notEmpty(request.getParams(), "params 不能为空");
// 2. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey)
.setGatewayDeviceName(deviceName)
.setSubDevices(request.getParams());
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 3. 返回结果
return success(result.getData());
}
@Override
protected boolean requiresAuthentication() {
return true;
}
@Override
protected String getProductKey(List<String> uriPath) {
// 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
return CollUtil.get(uriPath, 3);
}
@Override
protected String getDeviceName(List<String> uriPath) {
// 路径格式:/auth/register/sub-device/{productKey}/{deviceName}
return CollUtil.get(uriPath, 4);
}
@Data
public static class SubDeviceRegisterRequest {
private List<IotSubDeviceRegisterReqDTO> params;
}
}

View File

@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
import org.eclipse.californium.core.server.resources.Resource;
/**
* IoT CoAP /auth/register/sub-device/{productKey}/{deviceName}
* <p>
*
* <p>
* productKey deviceName
*
* @author
*/
@Slf4j
public class IotCoapRegisterSubResource extends CoapResource {
public static final String PATH = "sub-device";
private final IotCoapRegisterSubHandler registerSubHandler;
/**
* /auth/register/sub-device
*/
public IotCoapRegisterSubResource(IotCoapRegisterSubHandler registerSubHandler) {
this(PATH, registerSubHandler);
log.info("[IotCoapRegisterSubResource][创建 CoAP 子设备动态注册资源: /auth/register/{}]", PATH);
}
/**
*
*/
private IotCoapRegisterSubResource(String name, IotCoapRegisterSubHandler registerSubHandler) {
super(name);
this.registerSubHandler = registerSubHandler;
}
@Override
public Resource getChild(String name) {
// 递归创建动态子资源,支持 /sub-device/{productKey}/{deviceName} 路径
return new IotCoapRegisterSubResource(name, registerSubHandler);
}
@Override
public void handlePOST(CoapExchange exchange) {
log.debug("[handlePOST][收到子设备动态注册请求]");
registerSubHandler.handle(exchange);
}
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
/**
* IoT CoAP
*
* CoAP
* 1. POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 2. POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* Token CoAP Option 2088
*
* @author
*/
@Slf4j
public class IotCoapUpstreamHandler extends IotCoapAbstractHandler {
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
public IotCoapUpstreamHandler(String serverId) {
this.serverId = serverId;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
protected CommonResult<Object> handle0(CoapExchange exchange) {
// 1.1 解析通用参数
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = getProductKey(uriPath);
String deviceName = getDeviceName(uriPath);
String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
// 1.2 解析消息
IotDeviceMessage message = deserializeRequest(exchange, IotDeviceMessage.class);
Assert.notNull(message, "请求参数不能为空");
Assert.equals(method, message.getMethod(), "method 不匹配");
// 2. 发送消息
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
// 3. 返回结果
return CommonResult.success(MapUtil.of("messageId", message.getId()));
}
@Override
protected boolean requiresAuthentication() {
return true;
}
@Override
protected String getProductKey(List<String> uriPath) {
// 路径格式:/topic/sys/{productKey}/{deviceName}/...
return CollUtil.get(uriPath, 2);
}
@Override
protected String getDeviceName(List<String> uriPath) {
// 路径格式:/topic/sys/{productKey}/{deviceName}/...
return CollUtil.get(uriPath, 3);
}
}

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.handler.upstream;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapResource;
import org.eclipse.californium.core.server.resources.CoapExchange;
@ -20,15 +19,15 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
public static final String PATH = "topic";
private final IotCoapUpstreamProtocol protocol;
private final String serverId;
private final IotCoapUpstreamHandler upstreamHandler;
/**
* /topic
*/
public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
public IotCoapUpstreamTopicResource(String serverId,
IotCoapUpstreamHandler upstreamHandler) {
this(PATH, protocol, upstreamHandler);
this(PATH, serverId, upstreamHandler);
log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
}
@ -36,32 +35,32 @@ public class IotCoapUpstreamTopicResource extends CoapResource {
*
*/
private IotCoapUpstreamTopicResource(String name,
IotCoapUpstreamProtocol protocol,
String serverId,
IotCoapUpstreamHandler upstreamHandler) {
super(name);
this.protocol = protocol;
this.serverId = serverId;
this.upstreamHandler = upstreamHandler;
}
@Override
public Resource getChild(String name) {
// 递归创建动态子资源,支持任意深度路径
return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
return new IotCoapUpstreamTopicResource(name, serverId, upstreamHandler);
}
@Override
public void handleGET(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
@Override
public void handlePOST(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
@Override
public void handlePUT(CoapExchange exchange) {
upstreamHandler.handle(exchange, protocol);
upstreamHandler.handle(exchange);
}
}

View File

@ -2,12 +2,5 @@
* CoAP
* <p>
* Eclipse Californium IoT
* <p>
* URI
* - POST /auth
* - POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* - POST /topic/sys/{productKey}/{deviceName}/thing/event/post
* <p>
* Token CoAP Option 2088
*/
package cn.iocoder.yudao.module.iot.gateway.protocol.coap;

View File

@ -1,117 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.Map;
/**
* IoT CoAP
*
* {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler}
*
* @author
*/
@Slf4j
public class IotCoapAuthHandler {
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceCommonApi deviceApi;
private final IotDeviceMessageService deviceMessageService;
public IotCoapAuthHandler() {
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
/**
*
*
* @param exchange CoAP
* @param protocol
*/
@SuppressWarnings("unchecked")
public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
try {
// 1.1 解析请求体
byte[] payload = exchange.getRequestPayload();
if (payload == null || payload.length == 0) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
return;
}
Map<String, Object> body;
try {
body = JsonUtils.parseObject(new String(payload), Map.class);
} catch (Exception e) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
return;
}
// 1.2 解析参数
String clientId = MapUtil.getStr(body, "clientId");
if (StrUtil.isEmpty(clientId)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
return;
}
String username = MapUtil.getStr(body, "username");
if (StrUtil.isEmpty(username)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
return;
}
String password = MapUtil.getStr(body, "password");
if (StrUtil.isEmpty(password)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
return;
}
// 2.1 执行认证
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
if (result.isError()) {
log.warn("[handle][认证失败clientId: {}, 错误: {}]", clientId, result.getMsg());
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
return;
}
if (!BooleanUtil.isTrue(result.getData())) {
log.warn("[handle][认证失败clientId: {}]", clientId);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
return;
}
// 2.2 生成 Token
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空");
// 3. 执行上线
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
// 4. 返回成功响应
log.info("[handle][认证成功productKey: {}, deviceName: {}]",
deviceInfo.getProductKey(), deviceInfo.getDeviceName());
IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
} catch (Exception e) {
log.error("[handle][认证处理异常]", e);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}

View File

@ -1,98 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.Map;
/**
* IoT CoAP
* <p>
* /
*
* @author
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification"> - </a>
* @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
*/
@Slf4j
public class IotCoapRegisterHandler {
private final IotDeviceCommonApi deviceApi;
public IotCoapRegisterHandler() {
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
/**
*
*
* @param exchange CoAP
*/
@SuppressWarnings("unchecked")
public void handle(CoapExchange exchange) {
try {
// 1.1 解析请求体
byte[] payload = exchange.getRequestPayload();
if (payload == null || payload.length == 0) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
return;
}
Map<String, Object> body;
try {
body = JsonUtils.parseObject(new String(payload), Map.class);
} catch (Exception e) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
return;
}
// 1.2 解析参数
String productKey = MapUtil.getStr(body, "productKey");
if (StrUtil.isEmpty(productKey)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
return;
}
String deviceName = MapUtil.getStr(body, "deviceName");
if (StrUtil.isEmpty(deviceName)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
return;
}
String productSecret = MapUtil.getStr(body, "productSecret");
if (StrUtil.isEmpty(productSecret)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
return;
}
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey)
.setDeviceName(deviceName)
.setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
if (result.isError()) {
log.warn("[handle][设备动态注册失败productKey: {}, deviceName: {}, 错误: {}]",
productKey, deviceName, result.getMsg());
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
"设备动态注册失败:" + result.getMsg());
return;
}
// 3. 返回成功响应
log.info("[handle][设备动态注册成功productKey: {}, deviceName: {}]", productKey, deviceName);
IotCoapUtils.respondSuccess(exchange, result.getData());
} catch (Exception e) {
log.error("[handle][设备动态注册处理异常]", e);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}

View File

@ -1,110 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.server.resources.CoapExchange;
import java.util.List;
/**
* IoT CoAP
*
* CoAP
* 1. POST /topic/sys/{productKey}/{deviceName}/thing/property/post
* 2. POST /topic/sys/{productKey}/{deviceName}/thing/event/post
*
* Token CoAP Option 2088
*
* @author
*/
@Slf4j
public class IotCoapUpstreamHandler {
private final IotDeviceTokenService deviceTokenService;
private final IotDeviceMessageService deviceMessageService;
public IotCoapUpstreamHandler() {
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
/**
* CoAP
*
* @param exchange CoAP
* @param protocol
*/
public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
try {
// 1. 解析通用参数
List<String> uriPath = exchange.getRequestOptions().getUriPath();
String productKey = CollUtil.get(uriPath, 2);
String deviceName = CollUtil.get(uriPath, 3);
byte[] payload = exchange.getRequestPayload();
if (StrUtil.isEmpty(productKey)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
return;
}
if (StrUtil.isEmpty(deviceName)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
return;
}
if (ArrayUtil.isEmpty(payload)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
return;
}
// 2. 认证:从自定义 Option 获取 token
String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
if (StrUtil.isEmpty(token)) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
return;
}
// 验证 token
IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
if (deviceInfo == null) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
return;
}
// 验证设备信息匹配
if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
|| ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
return;
}
// 2.1 解析 methoddeviceName 后面的路径,用 . 拼接
// 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
// 2.2 解码消息
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
if (ObjUtil.notEqual(method, message.getMethod())) {
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
return;
}
// 2.3 发送消息到消息总线
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
// 3. 返回成功响应
IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
} catch (Exception e) {
log.error("[handle][CoAP 请求处理异常]", e);
IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
}
}
}

View File

@ -1,84 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.MediaTypeRegistry;
import org.eclipse.californium.core.coap.Option;
import org.eclipse.californium.core.server.resources.CoapExchange;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
/**
* IoT CoAP
*
* @author
*/
public class IotCoapUtils {
/**
* CoAP Option Token
* <p>
* CoAP Option 2048-65535 /
*/
public static final int OPTION_TOKEN = 2088;
/**
*
*
* @param exchange CoAP
* @param data
*/
public static void respondSuccess(CoapExchange exchange, Object data) {
CommonResult<Object> result = CommonResult.success(data);
String json = JsonUtils.toJsonString(result);
exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
}
/**
*
*
* @param exchange CoAP
* @param code CoAP
* @param message
*/
public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
int errorCode = mapCoapCodeToErrorCode(code);
CommonResult<Object> result = CommonResult.error(errorCode, message);
String json = JsonUtils.toJsonString(result);
exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
}
/**
* CoAP Option Token
*
* @param exchange CoAP
* @param optionNumber Option
* @return Token null
*/
public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
o -> o.getNumber() == optionNumber);
return option != null ? new String(option.getValue()) : null;
}
/**
* CoAP
*
* @param code CoAP
* @return
*/
public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
if (code == CoAP.ResponseCode.BAD_REQUEST) {
return BAD_REQUEST.getCode();
} else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
return UNAUTHORIZED.getCode();
} else if (code == CoAP.ResponseCode.FORBIDDEN) {
return FORBIDDEN.getCode();
} else {
return INTERNAL_SERVER_ERROR.getCode();
}
}
}

View File

@ -1,105 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxAuthEventHandler;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
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;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* IoT EMQX
* <p>
* EMQX HTTP
* 1. - EMQX HTTP
* 2. - EMQX Webhook
*
* @author
*/
@Slf4j
public class IotEmqxAuthEventProtocol {
private final IotGatewayProperties.EmqxProperties emqxProperties;
private final String serverId;
private final Vertx vertx;
private HttpServer httpServer;
public IotEmqxAuthEventProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
Vertx vertx) {
this.emqxProperties = emqxProperties;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
}
@PostConstruct
public void start() {
try {
startHttpServer();
log.info("[start][IoT 网关 EMQX 认证事件协议服务启动成功, 端口: {}]", emqxProperties.getHttpPort());
} catch (Exception e) {
log.error("[start][IoT 网关 EMQX 认证事件协议服务启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
stopHttpServer();
log.info("[stop][IoT 网关 EMQX 认证事件协议服务已停止]");
}
/**
* HTTP
*/
private void startHttpServer() {
int port = emqxProperties.getHttpPort();
// 1. 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 2. 创建处理器,传入 serverId
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId);
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
// TODO @haohao/mqtt/acl 需要处理么?
// TODO @芋艿:已在 EMQX 处理,如果是“设备直连”模式需要处理
// 3. 启动 HTTP 服务器
try {
httpServer = vertx.createHttpServer()
.requestHandler(router)
.listen(port)
.result();
} catch (Exception e) {
log.error("[startHttpServer][HTTP 服务器启动失败, 端口: {}]", port, e);
throw e;
}
}
/**
* HTTP
*/
private void stopHttpServer() {
if (httpServer == null) {
return;
}
try {
httpServer.close().result();
log.info("[stopHttpServer][HTTP 服务器已停止]");
} catch (Exception e) {
log.error("[stopHttpServer][HTTP 服务器停止失败]", e);
}
}
}

View File

@ -0,0 +1,225 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* IoT EMQX
*
* @author
*/
@Data
public class IotEmqxConfig {
// ========== MQTT Client 配置(连接 EMQX Broker ==========
/**
* MQTT
*/
@NotEmpty(message = "MQTT 服务器地址不能为空")
private String mqttHost;
/**
* MQTT 1883
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort = 1883;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 用户名不能为空")
private String mqttUsername;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 密码不能为空")
private String mqttPassword;
/**
* MQTT SSL
*/
@NotNull(message = "MQTT 是否开启 SSL 不能为空")
private Boolean mqttSsl = false;
/**
* MQTT ID
*/
@NotEmpty(message = "MQTT 客户端 ID 不能为空")
private String mqttClientId;
/**
* MQTT
*/
@NotEmpty(message = "MQTT 主题不能为空")
private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
/**
* QoS
* <p>
* 0 -
* 1 -
* 2 -
*/
@NotNull(message = "MQTT QoS 不能为空")
@Min(value = 0, message = "MQTT QoS 不能小于 0")
@Max(value = 2, message = "MQTT QoS 不能大于 2")
private Integer mqttQos = 1;
/**
*
*/
@NotNull(message = "连接超时时间不能为空")
@Min(value = 1, message = "连接超时时间不能小于 1 秒")
private Integer connectTimeoutSeconds = 10;
/**
*
*/
@NotNull(message = "重连延迟时间不能为空")
@Min(value = 0, message = "重连延迟时间不能小于 0 毫秒")
private Long reconnectDelayMs = 5000L;
/**
* Clean Session ()
* true: Broker 线
* 线 true
*/
@NotNull(message = "是否启用 Clean Session 不能为空")
private Boolean cleanSession = true;
/**
*
*
*/
@NotNull(message = "心跳间隔不能为空")
@Min(value = 1, message = "心跳间隔不能小于 1 秒")
private Integer keepAliveIntervalSeconds = 60;
/**
*
* Broker QoS 1/2
*/
@NotNull(message = "最大未确认消息队列大小不能为空")
@Min(value = 1, message = "最大未确认消息队列大小不能小于 1")
private Integer maxInflightQueue = 10000;
/**
* SSL
* 使
* false
*/
@NotNull(message = "是否信任所有 SSL 证书不能为空")
private Boolean trustAll = false;
// ========== MQTT Will / SSL 高级配置 ==========
/**
* (线)
*/
@Valid
private Will will = new Will();
/**
* SSL/TLS ()
*/
@Valid
private Ssl sslOptions = new Ssl();
// ========== HTTP Hook 配置(网关提供给 EMQX 调用) ==========
/**
* HTTP Hook /mqtt/auth/mqtt/event
*/
@Valid
private Http http = new Http();
/**
* (Last Will and Testament)
*/
@Data
public static class Will {
/**
*
*/
private boolean enabled = false;
/**
*
*/
private String topic;
/**
*
*/
private String payload;
/**
* QoS
*/
@Min(value = 0, message = "遗嘱消息 QoS 不能小于 0")
@Max(value = 2, message = "遗嘱消息 QoS 不能大于 2")
private Integer qos = 1;
/**
*
*/
private boolean retain = true;
}
/**
* SSL/TLS
*/
@Data
public static class Ssl {
/**
* KeyStoreclasspath:certs/client.jks
*
*/
private String keyStorePath;
/**
*
*/
private String keyStorePassword;
/**
* TrustStoreclasspath:certs/trust.jks
* CA
*/
private String trustStorePath;
/**
*
*/
private String trustStorePassword;
}
/**
* HTTP Hook SSL
*/
@Data
public static class Http {
/**
* SSL
*/
private Boolean sslEnabled = false;
/**
* SSL
*/
private String sslCertPath;
/**
* SSL
*/
private String sslKeyPath;
}
}

View File

@ -0,0 +1,532 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxAuthEventHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream.IotEmqxUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
* IoT EMQX
* <p>
* 1. HTTP Hook /mqtt/auth/mqtt/acl/mqtt/event EMQX
* 2. MQTT Client Broker
*
* @author
*/
@Slf4j
public class IotEmqxProtocol implements IotProtocol {
/**
*
*/
private final ProtocolProperties properties;
/**
* EMQX
*/
private final IotEmqxConfig emqxConfig;
/**
* ID
*/
@Getter
private final String serverId;
/**
*
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x
*/
private Vertx vertx;
/**
* HTTP Hook
*/
private HttpServer httpServer;
/**
* MQTT Client
*/
private volatile MqttClient mqttClient;
/**
* MQTT ID
*/
private volatile Long reconnectTimerId;
/**
*
*/
private final IotEmqxUpstreamHandler upstreamHandler;
/**
*
*/
private IotEmqxDownstreamSubscriber downstreamSubscriber;
public IotEmqxProtocol(ProtocolProperties properties) {
Assert.notNull(properties, "协议实例配置不能为空");
Assert.notNull(properties.getEmqx(), "EMQX 协议配置emqx不能为空");
this.properties = properties;
this.emqxConfig = properties.getEmqx();
Assert.notNull(emqxConfig.getConnectTimeoutSeconds(),
"MQTT 连接超时时间(emqx.connect-timeout-seconds)不能为空");
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
this.upstreamHandler = new IotEmqxUpstreamHandler(serverId);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.EMQX;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT EMQX 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例 和 下行消息订阅者
this.vertx = Vertx.vertx();
try {
// 1.2 启动 HTTP Hook 服务
startHttpServer();
// 1.3 启动 MQTT Client
startMqttClient();
running = true;
log.info("[start][IoT EMQX 协议 {} 启动成功hookPort{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotEmqxDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT EMQX 协议 {} 启动失败]", getId(), e);
// 启动失败时,关闭资源
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT EMQX 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 先置为 false避免 closeHandler 触发重连
running = false;
stopMqttClientReconnectChecker();
// 2.2 停止 MQTT Client
stopMqttClient();
// 2.3 停止 HTTP Hook 服务
stopHttpServer();
// 2.4 关闭 Vertx
if (vertx != null) {
try {
vertx.close().toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[stop][IoT EMQX 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT EMQX 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
log.info("[stop][IoT EMQX 协议 {} 已停止]", getId());
}
// ======================================= HTTP Hook Server =======================================
/**
* HTTP Hook /mqtt/auth/mqtt/acl/mqtt/event
*/
private void startHttpServer() {
// 1. 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create().setBodyLimit(1024 * 1024)); // 限制 body 大小为 1MB防止大包攻击
// 2. 创建处理器
IotEmqxAuthEventHandler handler = new IotEmqxAuthEventHandler(serverId, this);
router.post(IotMqttTopicUtils.MQTT_AUTH_PATH).handler(handler::handleAuth);
router.post(IotMqttTopicUtils.MQTT_ACL_PATH).handler(handler::handleAcl);
router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(handler::handleEvent);
// 3. 启动 HTTP Server支持 HTTPS
IotEmqxConfig.Http httpConfig = emqxConfig.getHttp();
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
if (httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled())) {
Assert.notBlank(httpConfig.getSslCertPath(), "EMQX HTTP SSL 证书路径(emqx.http.ssl-cert-path)不能为空");
Assert.notBlank(httpConfig.getSslKeyPath(), "EMQX HTTP SSL 私钥路径(emqx.http.ssl-key-path)不能为空");
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(httpConfig.getSslKeyPath())
.setCertPath(httpConfig.getSslCertPath());
options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动成功, port: {}, ssl: {}]",
getId(), properties.getPort(), httpConfig != null && Boolean.TRUE.equals(httpConfig.getSslEnabled()));
} catch (Exception e) {
log.error("[startHttpServer][IoT EMQX 协议 {} HTTP Hook 服务启动失败, port: {}]", getId(), properties.getPort(), e);
throw new RuntimeException("HTTP Hook 服务启动失败", e);
}
}
private void stopHttpServer() {
if (httpServer == null) {
return;
}
try {
httpServer.close().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
log.info("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务已停止]", getId());
} catch (Exception e) {
log.error("[stopHttpServer][IoT EMQX 协议 {} HTTP Hook 服务停止失败]", getId(), e);
} finally {
httpServer = null;
}
}
// ======================================= MQTT Client ======================================
private void startMqttClient() {
// 1.1 创建 MQTT Client
MqttClient client = createMqttClient();
this.mqttClient = client;
// 1.2 连接 MQTT Broker
if (!connectMqttClient(client)) {
throw new RuntimeException("MQTT Client 启动失败: 连接 Broker 失败");
}
// 2. 启动定时重连检查
startMqttClientReconnectChecker();
}
private void stopMqttClient() {
MqttClient client = this.mqttClient;
this.mqttClient = null; // 先清理引用
if (client == null) {
return;
}
// 1. 批量取消订阅(仅在连接时)
if (client.isConnected()) {
List<String> topicList = emqxConfig.getMqttTopics();
if (CollUtil.isNotEmpty(topicList)) {
try {
client.unsubscribe(topicList).toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[stopMqttClient][IoT EMQX 协议 {} 取消订阅异常]", getId(), e);
}
}
}
// 2. 断开 MQTT 连接
try {
client.disconnect().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
log.warn("[stopMqttClient][IoT EMQX 协议 {} 断开连接异常]", getId(), e);
}
}
// ======================================= MQTT 基础方法 ======================================
/**
* MQTT
*
* @return MqttClient
*/
private MqttClient createMqttClient() {
// 1.1 基础配置
MqttClientOptions options = new MqttClientOptions()
.setClientId(emqxConfig.getMqttClientId())
.setUsername(emqxConfig.getMqttUsername())
.setPassword(emqxConfig.getMqttPassword())
.setSsl(Boolean.TRUE.equals(emqxConfig.getMqttSsl()))
.setCleanSession(Boolean.TRUE.equals(emqxConfig.getCleanSession()))
.setKeepAliveInterval(emqxConfig.getKeepAliveIntervalSeconds())
.setMaxInflightQueue(emqxConfig.getMaxInflightQueue());
options.setConnectTimeout(emqxConfig.getConnectTimeoutSeconds() * 1000); // Vert.x 需要毫秒
options.setTrustAll(Boolean.TRUE.equals(emqxConfig.getTrustAll()));
// 1.2 配置遗嘱消息
IotEmqxConfig.Will will = emqxConfig.getWill();
if (will != null && will.isEnabled()) {
Assert.notBlank(will.getTopic(), "遗嘱消息主题(emqx.will.topic)不能为空");
Assert.notNull(will.getPayload(), "遗嘱消息内容(emqx.will.payload)不能为空");
options.setWillFlag(true)
.setWillTopic(will.getTopic())
.setWillMessageBytes(Buffer.buffer(will.getPayload()))
.setWillQoS(will.getQos())
.setWillRetain(will.isRetain());
}
// 1.3 配置高级 SSL/TLS仅在启用 SSL 且不信任所有证书时生效,且需要 sslOptions 非空)
IotEmqxConfig.Ssl sslOptions = emqxConfig.getSslOptions();
if (Boolean.TRUE.equals(emqxConfig.getMqttSsl())
&& Boolean.FALSE.equals(emqxConfig.getTrustAll())
&& sslOptions != null) {
if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
options.setTrustStoreOptions(new JksOptions()
.setPath(sslOptions.getTrustStorePath())
.setPassword(sslOptions.getTrustStorePassword()));
}
if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
options.setKeyStoreOptions(new JksOptions()
.setPath(sslOptions.getKeyStorePath())
.setPassword(sslOptions.getKeyStorePassword()));
}
}
// 2. 创建客户端
return MqttClient.create(vertx, options);
}
/**
* MQTT Broker
*
* @param client MQTT
* @return true false
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private synchronized boolean connectMqttClient(MqttClient client) {
String host = emqxConfig.getMqttHost();
int port = emqxConfig.getMqttPort();
int timeoutSeconds = emqxConfig.getConnectTimeoutSeconds();
try {
// 1. 连接 Broker
client.connect(port, host).toCompletionStage().toCompletableFuture()
.get(timeoutSeconds, TimeUnit.SECONDS);
log.info("[connectMqttClient][IoT EMQX 协议 {} 连接成功, host: {}, port: {}]",
getId(), host, port);
// 2. 设置处理器
setupMqttClientHandlers(client);
subscribeMqttClientTopics(client);
return true;
} catch (Exception e) {
log.error("[connectMqttClient][IoT EMQX 协议 {} 连接发生异常]", getId(), e);
return false;
}
}
/**
* MQTT
*/
private void closeMqttClient() {
MqttClient oldClient = this.mqttClient;
this.mqttClient = null; // 先清理引用
if (oldClient == null) {
return;
}
// 尽力释放(无论是否连接都尝试 disconnect
try {
oldClient.disconnect().toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
}
// ======================================= MQTT 重连机制 ======================================
/**
* MQTT Client
*/
private void startMqttClientReconnectChecker() {
long interval = emqxConfig.getReconnectDelayMs();
this.reconnectTimerId = vertx.setPeriodic(interval, timerId -> {
if (!running) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[startMqttClientReconnectChecker][IoT EMQX 协议 {} 检测到断开,尝试重连]", getId());
// 用 executeBlocking 避免阻塞 event-looptryReconnectMqttClient 内部有同步等待)
vertx.executeBlocking(() -> {
tryReconnectMqttClient();
return null;
});
});
}
/**
* MQTT Client
*/
private void stopMqttClientReconnectChecker() {
if (reconnectTimerId != null && vertx != null) {
try {
vertx.cancelTimer(reconnectTimerId);
} catch (Exception ignored) {
}
reconnectTimerId = null;
}
}
/**
* MQTT Client
*/
private synchronized void tryReconnectMqttClient() {
// 1. 前置检查
if (!running) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[tryReconnectMqttClient][IoT EMQX 协议 {} 开始重连]", getId());
try {
// 2. 关闭旧客户端
closeMqttClient();
// 3.1 创建新客户端
MqttClient client = createMqttClient();
this.mqttClient = client;
// 3.2 连接(失败只打印日志,等下次定时)
if (!connectMqttClient(client)) {
log.warn("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连失败,等待下次重试]", getId());
}
} catch (Exception e) {
log.error("[tryReconnectMqttClient][IoT EMQX 协议 {} 重连异常]", getId(), e);
}
}
// ======================================= MQTT Handler ======================================
/**
* MQTT Client
*/
private void setupMqttClientHandlers(MqttClient client) {
// 1. 断开重连监听
client.closeHandler(closeEvent -> {
if (!running) {
return;
}
log.warn("[setupMqttClientHandlers][IoT EMQX 协议 {} 连接断开,立即尝试重连]", getId());
// 用 executeBlocking 避免阻塞 event-looptryReconnectMqttClient 内部有同步等待)
vertx.executeBlocking(() -> {
tryReconnectMqttClient();
return null;
});
});
// 2. 异常处理
client.exceptionHandler(exception ->
log.error("[setupMqttClientHandlers][IoT EMQX 协议 {} MQTT Client 异常]", getId(), exception));
// 3. 上行消息处理
client.publishHandler(upstreamHandler::handle);
}
/**
* MQTT Client
*/
private void subscribeMqttClientTopics(MqttClient client) {
List<String> topicList = emqxConfig.getMqttTopics();
if (!client.isConnected()) {
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} MQTT Client 未连接, 跳过订阅]", getId());
return;
}
if (CollUtil.isEmpty(topicList)) {
log.warn("[subscribeMqttClientTopics][IoT EMQX 协议 {} 未配置订阅主题, 跳过订阅]", getId());
return;
}
// 执行订阅
Map<String, Integer> topics = convertMap(emqxConfig.getMqttTopics(), topic -> topic,
topic -> emqxConfig.getMqttQos());
try {
client.subscribe(topics).toCompletionStage().toCompletableFuture()
.get(10, TimeUnit.SECONDS);
log.info("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅成功, 共 {} 个主题]", getId(), topicList.size());
} catch (Exception e) {
log.error("[subscribeMqttClientTopics][IoT EMQX 协议 {} 订阅失败]", getId(), e);
}
}
/**
* MQTT Broker
*
* @param topic
* @param payload
*/
public void publishMessage(String topic, byte[] payload) {
if (mqttClient == null || !mqttClient.isConnected()) {
log.warn("[publishMessage][IoT EMQX 协议 {} MQTT Client 未连接, 无法发布消息]", getId());
return;
}
MqttQoS qos = MqttQoS.valueOf(emqxConfig.getMqttQos());
mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false)
.onFailure(e -> log.error("[publishMessage][IoT EMQX 协议 {} 发布失败, topic: {}]", getId(), topic, e));
}
/**
* MQTT Broker
*
* @param topic
* @param payload
* @param delayMs
*/
public void publishDelayMessage(String topic, byte[] payload, long delayMs) {
vertx.setTimer(delayMs, id -> publishMessage(topic, payload));
}
}

View File

@ -1,365 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router.IotEmqxUpstreamHandler;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.JksOptions;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IoT EMQX
*
* @author
*/
@Slf4j
public class IotEmqxUpstreamProtocol {
private final IotGatewayProperties.EmqxProperties emqxProperties;
private volatile boolean isRunning = false;
private final Vertx vertx;
@Getter
private final String serverId;
private MqttClient mqttClient;
private IotEmqxUpstreamHandler upstreamHandler;
public IotEmqxUpstreamProtocol(IotGatewayProperties.EmqxProperties emqxProperties,
Vertx vertx) {
this.emqxProperties = emqxProperties;
this.serverId = IotDeviceMessageUtils.generateServerId(emqxProperties.getMqttPort());
this.vertx = vertx;
}
@PostConstruct
public void start() {
if (isRunning) {
return;
}
try {
// 1. 启动 MQTT 客户端
startMqttClient();
// 2. 标记服务为运行状态
isRunning = true;
log.info("[start][IoT 网关 EMQX 协议启动成功]");
} catch (Exception e) {
log.error("[start][IoT 网关 EMQX 协议服务启动失败,应用将关闭]", e);
stop();
// 异步关闭应用
Thread shutdownThread = new Thread(() -> {
try {
// 确保日志输出完成,使用更优雅的方式
log.error("[start][由于 MQTT 连接失败,正在关闭应用]");
// 等待日志输出完成
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.warn("[start][应用关闭被中断]");
}
System.exit(1);
});
shutdownThread.setDaemon(true);
shutdownThread.setName("emergency-shutdown");
shutdownThread.start();
throw e;
}
}
@PreDestroy
public void stop() {
if (!isRunning) {
return;
}
// 1. 停止 MQTT 客户端
stopMqttClient();
// 2. 标记服务为停止状态
isRunning = false;
log.info("[stop][IoT 网关 MQTT 协议服务已停止]");
}
/**
* MQTT
*/
private void startMqttClient() {
try {
// 1. 初始化消息处理器
this.upstreamHandler = new IotEmqxUpstreamHandler(this);
// 2. 创建 MQTT 客户端
createMqttClient();
// 3. 同步连接 MQTT Broker
connectMqttSync();
} catch (Exception e) {
log.error("[startMqttClient][MQTT 客户端启动失败]", e);
throw new RuntimeException("MQTT 客户端启动失败: " + e.getMessage(), e);
}
}
/**
* MQTT Broker
*/
private void connectMqttSync() {
String host = emqxProperties.getMqttHost();
int port = emqxProperties.getMqttPort();
// 1. 连接 MQTT Broker
CountDownLatch latch = new CountDownLatch(1);
AtomicBoolean success = new AtomicBoolean(false);
mqttClient.connect(port, host, connectResult -> {
if (connectResult.succeeded()) {
log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
setupMqttHandlers();
subscribeToTopics();
success.set(true);
} else {
log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
host, port, connectResult.cause());
}
latch.countDown();
});
// 2. 等待连接结果
try {
// 应用层超时控制防止启动过程无限阻塞与MQTT客户端的网络超时是不同层次的控制
boolean awaitResult = latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
if (!awaitResult) {
log.error("[connectMqttSync][等待连接结果超时]");
throw new RuntimeException("连接 MQTT Broker 超时");
}
if (!success.get()) {
throw new RuntimeException(String.format("首次连接 MQTT Broker 失败,地址: %s, 端口: %d", host, port));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("[connectMqttSync][等待连接结果被中断]", e);
throw new RuntimeException("连接 MQTT Broker 被中断", e);
}
}
/**
* MQTT Broker
*/
private void connectMqttAsync() {
String host = emqxProperties.getMqttHost();
int port = emqxProperties.getMqttPort();
mqttClient.connect(port, host, connectResult -> {
if (connectResult.succeeded()) {
log.info("[connectMqttAsync][MQTT 客户端重连成功]");
setupMqttHandlers();
subscribeToTopics();
} else {
log.error("[connectMqttAsync][连接 MQTT Broker 失败, host: {}, port: {}]",
host, port, connectResult.cause());
log.warn("[connectMqttAsync][重连失败,将再次尝试]");
reconnectWithDelay();
}
});
}
/**
*
*/
private void reconnectWithDelay() {
if (!isRunning) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
long delay = emqxProperties.getReconnectDelayMs();
log.info("[reconnectWithDelay][将在 {} 毫秒后尝试重连 MQTT Broker]", delay);
vertx.setTimer(delay, timerId -> {
if (!isRunning) {
return;
}
if (mqttClient != null && mqttClient.isConnected()) {
return;
}
log.info("[reconnectWithDelay][开始重连 MQTT Broker]");
try {
createMqttClient();
connectMqttAsync();
} catch (Exception e) {
log.error("[reconnectWithDelay][重连过程中发生异常]", e);
vertx.setTimer(delay, t -> reconnectWithDelay());
}
});
}
/**
* MQTT
*/
private void stopMqttClient() {
if (mqttClient == null) {
return;
}
try {
if (mqttClient.isConnected()) {
// 1. 取消订阅所有主题
List<String> topicList = emqxProperties.getMqttTopics();
for (String topic : topicList) {
try {
mqttClient.unsubscribe(topic);
} catch (Exception e) {
log.warn("[stopMqttClient][取消订阅主题({})异常]", topic, e);
}
}
// 2. 断开 MQTT 客户端连接
try {
CountDownLatch disconnectLatch = new CountDownLatch(1);
mqttClient.disconnect(ar -> disconnectLatch.countDown());
if (!disconnectLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)) {
log.warn("[stopMqttClient][断开 MQTT 连接超时]");
}
} catch (Exception e) {
log.warn("[stopMqttClient][关闭 MQTT 客户端异常]", e);
}
}
} catch (Exception e) {
log.warn("[stopMqttClient][停止 MQTT 客户端过程中发生异常]", e);
} finally {
mqttClient = null;
}
}
/**
* MQTT
*/
private void createMqttClient() {
// 1.1 创建基础配置
MqttClientOptions options = (MqttClientOptions) new MqttClientOptions()
.setClientId(emqxProperties.getMqttClientId())
.setUsername(emqxProperties.getMqttUsername())
.setPassword(emqxProperties.getMqttPassword())
.setSsl(emqxProperties.getMqttSsl())
.setCleanSession(emqxProperties.getCleanSession())
.setKeepAliveInterval(emqxProperties.getKeepAliveIntervalSeconds())
.setMaxInflightQueue(emqxProperties.getMaxInflightQueue())
.setConnectTimeout(emqxProperties.getConnectTimeoutSeconds() * 1000) // Vert.x 需要毫秒
.setTrustAll(emqxProperties.getTrustAll());
// 1.2 配置遗嘱消息
IotGatewayProperties.EmqxProperties.Will will = emqxProperties.getWill();
if (will.isEnabled()) {
Assert.notBlank(will.getTopic(), "遗嘱消息主题(will.topic)不能为空");
Assert.notNull(will.getPayload(), "遗嘱消息内容(will.payload)不能为空");
options.setWillFlag(true)
.setWillTopic(will.getTopic())
.setWillMessageBytes(Buffer.buffer(will.getPayload()))
.setWillQoS(will.getQos())
.setWillRetain(will.isRetain());
}
// 1.3 配置高级 SSL/TLS (仅在启用 SSL 且不信任所有证书时生效)
if (Boolean.TRUE.equals(emqxProperties.getMqttSsl()) && !Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
IotGatewayProperties.EmqxProperties.Ssl sslOptions = emqxProperties.getSslOptions();
if (StrUtil.isNotBlank(sslOptions.getTrustStorePath())) {
options.setTrustStoreOptions(new JksOptions()
.setPath(sslOptions.getTrustStorePath())
.setPassword(sslOptions.getTrustStorePassword()));
}
if (StrUtil.isNotBlank(sslOptions.getKeyStorePath())) {
options.setKeyStoreOptions(new JksOptions()
.setPath(sslOptions.getKeyStorePath())
.setPassword(sslOptions.getKeyStorePassword()));
}
}
// 1.4 安全警告日志
if (Boolean.TRUE.equals(emqxProperties.getTrustAll())) {
log.warn("[createMqttClient][安全警告:当前配置信任所有 SSL 证书trustAll=true这在生产环境中存在严重安全风险]");
}
// 2. 创建客户端实例
this.mqttClient = MqttClient.create(vertx, options);
}
/**
* MQTT
*/
private void setupMqttHandlers() {
// 1. 设置断开重连监听器
mqttClient.closeHandler(closeEvent -> {
if (!isRunning) {
return;
}
log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
reconnectWithDelay();
});
// 2. 设置异常处理器
mqttClient.exceptionHandler(exception ->
log.error("[exceptionHandler][MQTT 客户端异常]", exception));
// 3. 设置消息处理器
mqttClient.publishHandler(upstreamHandler::handle);
}
/**
*
*/
private void subscribeToTopics() {
// 1. 校验 MQTT 客户端是否连接
List<String> topicList = emqxProperties.getMqttTopics();
if (mqttClient == null || !mqttClient.isConnected()) {
log.warn("[subscribeToTopics][MQTT 客户端未连接, 跳过订阅]");
return;
}
// 2. 批量订阅所有主题
Map<String, Integer> topics = new HashMap<>();
int qos = emqxProperties.getMqttQos();
for (String topic : topicList) {
topics.put(topic, qos);
}
mqttClient.subscribe(topics, subscribeResult -> {
if (subscribeResult.succeeded()) {
log.info("[subscribeToTopics][订阅主题成功, 共 {} 个主题]", topicList.size());
} else {
log.error("[subscribeToTopics][订阅主题失败, 共 {} 个主题, 原因: {}]",
topicList.size(), subscribeResult.cause().getMessage(), subscribeResult.cause());
}
});
}
/**
* MQTT Broker
*
* @param topic
* @param payload
*/
public void publishMessage(String topic, byte[] payload) {
if (mqttClient == null || !mqttClient.isConnected()) {
log.warn("[publishMessage][MQTT 客户端未连接, 无法发布消息]");
return;
}
MqttQoS qos = MqttQoS.valueOf(emqxProperties.getMqttQos());
mqttClient.publish(topic, Buffer.buffer(payload), qos, false, false);
}
}

View File

@ -1,11 +1,11 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
@ -21,13 +21,13 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class IotEmqxDownstreamHandler {
private final IotEmqxUpstreamProtocol protocol;
private final IotEmqxProtocol protocol;
private final IotDeviceService deviceService;
private final IotDeviceMessageService deviceMessageService;
public IotEmqxDownstreamHandler(IotEmqxUpstreamProtocol protocol) {
public IotEmqxDownstreamHandler(IotEmqxProtocol protocol) {
this.protocol = protocol;
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
@ -53,9 +53,10 @@ public class IotEmqxDownstreamHandler {
return;
}
// 2.2 构建载荷
byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(),
byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
deviceInfo.getDeviceName());
// 2.3 发布消息
// 3. 发布消息
protocol.publishMessage(topic, payload);
}
@ -74,4 +75,4 @@ public class IotEmqxDownstreamHandler {
return IotMqttTopicUtils.buildTopicByMethod(message.getMethod(), productKey, deviceName, isReply);
}
}
}

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT EMQX
*
* @author
*/
@Slf4j
public class IotEmqxDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotEmqxDownstreamHandler downstreamHandler;
public IotEmqxDownstreamSubscriber(IotEmqxProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = new IotEmqxDownstreamHandler(protocol);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@ -1,25 +1,35 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.extern.slf4j.Slf4j;
import java.util.Locale;
/**
* IoT EMQX
* <p>
* EMQX HTTP
* 1. - EMQX HTTP
* 2. - EMQX Webhook
* 1. - EMQX HTTP {@link #handleAuth(RoutingContext)}
* 2. - EMQX Webhook {@link #handleEvent(RoutingContext)}
* 3. ACL - EMQX HTTP ACL {@link #handleAcl(RoutingContext)}
* 4. - {@link #handleDeviceRegister(RoutingContext, String, String)}
*
* @author
*/
@ -45,30 +55,43 @@ public class IotEmqxAuthEventHandler {
private static final String RESULT_IGNORE = "ignore";
/**
* EMQX
* EMQX -
*/
private static final String EVENT_CLIENT_CONNECTED = "client.connected";
/**
* EMQX -
*/
private static final String EVENT_CLIENT_DISCONNECTED = "client.disconnected";
/**
* -
*/
private static final String AUTH_TYPE_REGISTER = "|authType=register|";
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
private final IotEmqxProtocol protocol;
private final IotDeviceMessageService deviceMessageService;
private final IotDeviceCommonApi deviceApi;
public IotEmqxAuthEventHandler(String serverId) {
public IotEmqxAuthEventHandler(String serverId, IotEmqxProtocol protocol) {
this.serverId = serverId;
this.protocol = protocol;
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
}
// ========== 认证处理 ==========
/**
* EMQX
*/
public void handleAuth(RoutingContext context) {
JsonObject body = null;
try {
// 1. 参数校验
JsonObject body = parseRequestBody(context);
body = parseRequestBody(context);
if (body == null) {
return;
}
@ -82,7 +105,13 @@ public class IotEmqxAuthEventHandler {
return;
}
// 2. 执行认证
// 2.1 情况一:判断是否为注册请求
if (StrUtil.endWith(clientId, AUTH_TYPE_REGISTER)) {
handleDeviceRegister(context, username, password);
return;
}
// 2.2 情况二:执行认证
boolean authResult = handleDeviceAuth(clientId, username, password);
log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
if (authResult) {
@ -91,11 +120,179 @@ public class IotEmqxAuthEventHandler {
sendAuthResponse(context, RESULT_DENY);
}
} catch (Exception e) {
log.error("[handleAuth][设备认证异常]", e);
log.error("[handleAuth][设备认证异常][body={}]", body, e);
sendAuthResponse(context, RESULT_IGNORE);
}
}
/**
*
* <p>
* JSON result
*
* @param context
* @return JSONnull
*/
private JsonObject parseRequestBody(RoutingContext context) {
try {
JsonObject body = context.body().asJsonObject();
if (body == null) {
log.info("[parseRequestBody][请求体为空]");
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
return body;
} catch (Exception e) {
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
}
/**
*
*
* @param clientId ID
* @param username
* @param password
* @return
*/
private boolean handleDeviceAuth(String clientId, String username, String password) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
throw e;
}
}
/**
* EMQX
* EMQX JSON
*
* @param context
* @param result allowdenyignore
*/
private void sendAuthResponse(RoutingContext context, String result) {
// 构建符合 EMQX 官方规范的响应
JsonObject response = new JsonObject()
.put("result", result)
.put("is_superuser", false);
// 可以根据业务需求添加客户端属性
// response.put("client_attrs", new JsonObject().put("role", "device"));
// 可以添加认证过期时间(可选)
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
// 回复响应
context.response()
.setStatusCode(SUCCESS_STATUS_CODE)
.putHeader("Content-Type", "application/json; charset=utf-8")
.end(response.encode());
}
// ========== ACL 处理 ==========
/**
* EMQX ACL
* <p>
* EMQX HTTP ACL publish/subscribe
* ignore EMQX ACL
*/
public void handleAcl(RoutingContext context) {
JsonObject body = null;
try {
// 1.1 解析请求体
body = parseRequestBody(context);
if (body == null) {
return;
}
String username = body.getString("username");
String topic = body.getString("topic");
if (StrUtil.hasBlank(username, topic)) {
log.info("[handleAcl][ACL 参数不完整: username={}, topic={}]", username, topic);
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 1.2 解析设备身份
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 1.3 解析 ACL 动作(兼容多种 EMQX 版本/插件字段)
Boolean subscribe = parseAclSubscribeFlag(body);
if (subscribe == null) {
sendAuthResponse(context, RESULT_IGNORE);
return;
}
// 2. 执行 ACL 校验
boolean allowed = subscribe
? IotMqttTopicUtils.isTopicSubscribeAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName())
: IotMqttTopicUtils.isTopicPublishAllowed(topic, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
sendAuthResponse(context, allowed ? RESULT_ALLOW : RESULT_DENY);
} catch (Exception e) {
log.error("[handleAcl][ACL 处理失败][body={}]", body, e);
sendAuthResponse(context, RESULT_IGNORE);
}
}
/**
* ACL /
*
* @param body ACL
* @return true false null
*/
private static Boolean parseAclSubscribeFlag(JsonObject body) {
// 1. action 字段(常见为 publish/subscribe
String action = body.getString("action");
if (StrUtil.isNotBlank(action)) {
String lower = action.toLowerCase(Locale.ROOT);
if (lower.contains("sub")) {
return true;
}
if (lower.contains("pub")) {
return false;
}
}
// 2. access 字段:可能是数字或字符串
Integer access = body.getInteger("access");
if (access != null) {
if (access == 1) {
return true;
}
if (access == 2) {
return false;
}
}
String accessText = body.getString("access");
if (StrUtil.isNotBlank(accessText)) {
String lower = accessText.toLowerCase(Locale.ROOT);
if (lower.contains("sub")) {
return true;
}
if (lower.contains("pub")) {
return false;
}
if (StrUtil.isNumeric(accessText)) {
int value = Integer.parseInt(accessText);
if (value == 1) {
return true;
}
if (value == 2) {
return false;
}
}
}
return null;
}
// ========== 事件处理 ==========
/**
* EMQX EMQX Webhook
* client.connectedclient.disconnected
@ -124,58 +321,15 @@ public class IotEmqxAuthEventHandler {
break;
}
// EMQX Webhook 只需要 200 状态码,无需响应体
// 3. EMQX Webhook 只需要 200 状态码,无需响应体
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
} catch (Exception e) {
log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
// 即使处理失败,也返回 200 避免EMQX重试
log.error("[handleEvent][事件处理失败][body={}]", body, e);
// 即使处理失败,也返回 200 避免 EMQX 重试
context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
}
}
/**
*
*/
private void handleClientConnected(JsonObject body) {
String username = body.getString("username");
log.info("[handleClientConnected][设备上线: {}]", username);
handleDeviceStateChange(username, true);
}
/**
*
*/
private void handleClientDisconnected(JsonObject body) {
String username = body.getString("username");
String reason = body.getString("reason");
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
handleDeviceStateChange(username, false);
}
/**
*
* <p>
* JSON result
*
* @param context
* @return JSONnull
*/
private JsonObject parseRequestBody(RoutingContext context) {
try {
JsonObject body = context.body().asJsonObject();
if (body == null) {
log.info("[parseRequestBody][请求体为空]");
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
return body;
} catch (Exception e) {
log.error("[parseRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
sendAuthResponse(context, RESULT_IGNORE);
return null;
}
}
/**
*
* <p>
@ -201,23 +355,22 @@ public class IotEmqxAuthEventHandler {
}
/**
*
*
* @param clientId ID
* @param username
* @param password
* @return
*
*/
private boolean handleDeviceAuth(String clientId, String username, String password) {
try {
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
result.checkError();
return BooleanUtil.isTrue(result.getData());
} catch (Exception e) {
log.error("[handleDeviceAuth][设备({}) 认证接口调用失败]", username, e);
throw e;
}
private void handleClientConnected(JsonObject body) {
String username = body.getString("username");
log.info("[handleClientConnected][设备上线: {}]", username);
handleDeviceStateChange(username, true);
}
/**
*
*/
private void handleClientDisconnected(JsonObject body) {
String username = body.getString("username");
String reason = body.getString("reason");
log.info("[handleClientDisconnected][设备下线: {} ({})]", username, reason);
handleDeviceStateChange(username, false);
}
/**
@ -247,29 +400,74 @@ public class IotEmqxAuthEventHandler {
}
}
// ========= 注册处理 =========
/**
* EMQX
* EMQX JSON
*
*
* @param context
* @param result allowdenyignore
* @param context
* @param username
* @param password
*/
private void sendAuthResponse(RoutingContext context, String result) {
// 构建符合 EMQX 官方规范的响应
JsonObject response = new JsonObject()
.put("result", result)
.put("is_superuser", false);
private void handleDeviceRegister(RoutingContext context, String username, String password) {
try {
// 1. 解析设备信息
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
if (deviceInfo == null) {
log.warn("[handleDeviceRegister][设备注册失败: 无法解析 username={}]", username);
sendAuthResponse(context, RESULT_DENY);
return;
}
// 可以根据业务需求添加客户端属性
// response.put("client_attrs", new JsonObject().put("role", "device"));
// 2. 调用注册 API
IotDeviceRegisterReqDTO params = new IotDeviceRegisterReqDTO()
.setProductKey(deviceInfo.getProductKey())
.setDeviceName(deviceInfo.getDeviceName())
.setSign(password);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
result.checkError();
// 可以添加认证过期时间(可选)
// response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
// 3. 允许连接
log.info("[handleDeviceRegister][设备注册成功: {}]", username);
sendAuthResponse(context, RESULT_ALLOW);
context.response()
.setStatusCode(SUCCESS_STATUS_CODE)
.putHeader("Content-Type", "application/json; charset=utf-8")
.end(response.encode());
// 4. 延迟 5 秒发送注册结果(等待设备连接成功并完成订阅)
sendRegisterResultMessage(username, result.getData());
} catch (Exception e) {
log.warn("[handleDeviceRegister][设备注册失败: {}, 错误: {}]", username, e.getMessage());
sendAuthResponse(context, RESULT_DENY);
}
}
}
/**
*
* <p>
* 5
*
* @param username
* @param result
*/
@SuppressWarnings("DataFlowIssue")
private void sendRegisterResultMessage(String username, IotDeviceRegisterRespDTO result) {
IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
Assert.notNull(deviceInfo, "设备信息不能为空");
try {
// 1.1 构建响应消息
String method = IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod();
IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(null, method, result, 0, null);
// 1.2 序列化消息
byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage,
cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum.JSON);
// 1.3 构建响应主题
String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), true);
// 2. 构建响应主题,并延迟发布(等待设备连接成功并完成订阅)
protocol.publishDelayMessage(replyTopic, encodedData, 5000);
log.info("[sendRegisterResultMessage][发送注册结果: topic={}]", replyTopic);
} catch (Exception e) {
log.error("[sendRegisterResultMessage][发送注册结果失败: {}]", username, e);
}
}
}

View File

@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.extern.slf4j.Slf4j;
@ -20,41 +21,42 @@ public class IotEmqxUpstreamHandler {
private final String serverId;
public IotEmqxUpstreamHandler(IotEmqxUpstreamProtocol protocol) {
public IotEmqxUpstreamHandler(String serverId) {
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.serverId = protocol.getServerId();
this.serverId = serverId;
}
/**
* MQTT
*/
public void handle(MqttPublishMessage mqttMessage) {
log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
log.debug("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
String topic = mqttMessage.topicName();
byte[] payload = mqttMessage.payload().getBytes();
try {
// 1. 解析主题,一次性获取所有信息
String[] topicParts = topic.split("/");
if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
String productKey = ArrayUtil.get(topicParts, 2);
String deviceName = ArrayUtil.get(topicParts, 3);
if (topicParts.length < 4 || StrUtil.hasBlank(productKey, deviceName)) {
log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
return;
}
String productKey = topicParts[2];
String deviceName = topicParts[3];
// 3. 解码消息
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
// 2.1 反序列化消息
IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
if (message == null) {
log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
return;
}
// 2.2 标准化回复消息的 methodMQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
IotMqttTopicUtils.normalizeReplyMethod(message);
// 4. 发送消息到队列
// 3. 发送消息到队列
deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
} catch (Exception e) {
log.error("[handle][topic({}) payload({}) 处理异常]", topic, new String(payload), e);
}
}
}
}

View File

@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import lombok.Data;
/**
* IoT HTTP
*
* @author
*/
@Data
public class IotHttpConfig {
}

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
/**
* IoT HTTP
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public class IotHttpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
private final IotHttpUpstreamProtocol protocol;
private final IotMessageBus messageBus;
@PostConstruct
public void init() {
messageBus.register(this);
}
@Override
public String getTopic() {
return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
}
@Override
public String getGroup() {
// 保证点对点消费,需要保证独立的 Group所以使用 Topic 作为 Group
return getTopic();
}
@Override
public void onMessage(IotDeviceMessage message) {
log.info("[onMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
}
}

View File

@ -0,0 +1,176 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream.IotHttpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream.IotHttpUpstreamHandler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* IoT HTTP
* <p>
* Vert.x HTTP
*
* @author
*/
@Slf4j
public class IotHttpProtocol implements IotProtocol {
/**
*
*/
private final ProtocolProperties properties;
/**
* ID
*/
@Getter
private final String serverId;
/**
*
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x
*/
private Vertx vertx;
/**
* HTTP
*/
private HttpServer httpServer;
/**
*
*/
private IotHttpDownstreamSubscriber downstreamSubscriber;
public IotHttpProtocol(ProtocolProperties properties) {
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.HTTP;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT HTTP 协议 {} 已经在运行中]", getId());
return;
}
// 1.1 创建 Vertx 实例
this.vertx = Vertx.vertx();
// 1.2 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 1.3 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
// 1.4 启动 HTTP 服务器
HttpServerOptions options = new HttpServerOptions().setPort(properties.getPort());
IotGatewayProperties.SslConfig sslConfig = properties.getSsl();
if (sslConfig != null && Boolean.TRUE.equals(sslConfig.getSsl())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions()
.setKeyPath(sslConfig.getSslKeyPath())
.setCertPath(sslConfig.getSslCertPath());
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.result();
running = true;
log.info("[start][IoT HTTP 协议 {} 启动成功,端口:{}serverId{}]",
getId(), properties.getPort(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
this.downstreamSubscriber = new IotHttpDownstreamSubscriber(this, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT HTTP 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 关闭 HTTP 服务器
if (httpServer != null) {
try {
httpServer.close().result();
log.info("[stop][IoT HTTP 协议 {} 服务器已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} 服务器停止失败]", getId(), e);
}
httpServer = null;
}
// 2.2 关闭 Vertx 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT HTTP 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT HTTP 协议 {} Vertx 关闭失败]", getId(), e);
}
vertx = null;
}
running = false;
log.info("[stop][IoT HTTP 协议 {} 已停止]", getId());
}
}

View File

@ -1,92 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.PemKeyCertOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* IoT HTTP
*
* @author
*/
@Slf4j
public class IotHttpUpstreamProtocol {
private final IotGatewayProperties.HttpProperties httpProperties;
private final Vertx vertx;
private HttpServer httpServer;
@Getter
private final String serverId;
public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
this.httpProperties = httpProperties;
this.vertx = vertx;
this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
}
@PostConstruct
public void start() {
// 创建路由
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create());
// 创建处理器,添加路由处理器
IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
// 启动 HTTP 服务器
HttpServerOptions options = new HttpServerOptions()
.setPort(httpProperties.getServerPort());
if (Boolean.TRUE.equals(httpProperties.getSslEnabled())) {
PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions().setKeyPath(httpProperties.getSslKeyPath())
.setCertPath(httpProperties.getSslCertPath());
options = options.setSsl(true).setKeyCertOptions(pemKeyCertOptions);
}
try {
httpServer = vertx.createHttpServer(options)
.requestHandler(router)
.listen()
.result();
log.info("[start][IoT 网关 HTTP 协议启动成功,端口:{}]", httpProperties.getServerPort());
} catch (Exception e) {
log.error("[start][IoT 网关 HTTP 协议启动失败]", e);
throw e;
}
}
@PreDestroy
public void stop() {
if (httpServer != null) {
try {
httpServer.close().result();
log.info("[stop][IoT 网关 HTTP 协议已停止]");
} catch (Exception e) {
log.error("[stop][IoT 网关 HTTP 协议停止失败]", e);
}
}
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT HTTP
*
* @author
*/
@Slf4j
public class IotHttpDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
public IotHttpDownstreamSubscriber(IotProtocol protocol, IotMessageBus messageBus) {
super(protocol, messageBus);
}
@Override
protected void handleMessage(IotDeviceMessage message) {
log.info("[handleMessage][IoT 网关 HTTP 协议不支持下行消息,忽略消息:{}]", message);
}
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
@ -13,12 +14,10 @@ import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
@ -27,7 +26,6 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public abstract class IotHttpAbstractHandler implements Handler<RoutingContext> {
@ -43,15 +41,31 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
CommonResult<Object> result = handle0(context);
writeResponse(context, result);
} catch (ServiceException e) {
// 已知异常,返回对应的错误码和错误信息
writeResponse(context, CommonResult.error(e.getCode(), e.getMessage()));
} catch (IllegalArgumentException e) {
// 参数校验异常,返回 400 错误
writeResponse(context, CommonResult.error(BAD_REQUEST.getCode(), e.getMessage()));
} catch (Exception e) {
// 其他未知异常,返回 500 错误
log.error("[handle][path({}) 处理异常]", context.request().path(), e);
writeResponse(context, CommonResult.error(INTERNAL_SERVER_ERROR));
}
}
/**
* HTTP
*
* @param context RoutingContext
* @return
*/
protected abstract CommonResult<Object> handle0(RoutingContext context);
/**
*
*
* @param context RoutingContext
*/
private void beforeHandle(RoutingContext context) {
// 如果不需要认证,则不走前置处理
String path = context.request().path();
@ -83,12 +97,26 @@ public abstract class IotHttpAbstractHandler implements Handler<RoutingContext>
}
}
// ========== 序列化相关方法 ==========
protected static <T> T deserializeRequest(RoutingContext context, Class<T> clazz) {
byte[] body = context.body().buffer() != null ? context.body().buffer().getBytes() : null;
if (ArrayUtil.isEmpty(body)) {
throw invalidParamException("请求体不能为空");
}
return JsonUtils.parseObject(body, clazz);
}
private static String serializeResponse(Object data) {
return JsonUtils.toJsonString(data);
}
@SuppressWarnings("deprecation")
public static void writeResponse(RoutingContext context, Object data) {
public static void writeResponse(RoutingContext context, CommonResult<?> data) {
context.response()
.setStatusCode(200)
.putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
.end(JsonUtils.toJsonString(data));
.end(serializeResponse(data));
}
}

View File

@ -1,23 +1,20 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
@ -32,7 +29,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
public static final String PATH = "/auth";
private final IotHttpUpstreamProtocol protocol;
private final String serverId;
private final IotDeviceTokenService deviceTokenService;
@ -40,42 +37,31 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
private final IotDeviceMessageService deviceMessageService;
public IotHttpAuthHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
public IotHttpAuthHandler(IotHttpProtocol protocol) {
this.serverId = protocol.getServerId();
this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
@SuppressWarnings("DuplicatedCode")
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String clientId = body.getString("clientId");
if (StrUtil.isEmpty(clientId)) {
throw invalidParamException("clientId 不能为空");
}
String username = body.getString("username");
if (StrUtil.isEmpty(username)) {
throw invalidParamException("username 不能为空");
}
String password = body.getString("password");
if (StrUtil.isEmpty(password)) {
throw invalidParamException("password 不能为空");
}
IotDeviceAuthReqDTO request = deserializeRequest(context, IotDeviceAuthReqDTO.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notBlank(request.getClientId(), "clientId 不能为空");
Assert.notBlank(request.getUsername(), "username 不能为空");
Assert.notBlank(request.getPassword(), "password 不能为空");
// 2.1 执行认证
CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
.setClientId(clientId).setUsername(username).setPassword(password));
CommonResult<Boolean> result = deviceApi.authDevice(request);
result.checkError();
if (!BooleanUtil.isTrue(result.getData())) {
if (BooleanUtil.isFalse(result.getData())) {
throw exception(DEVICE_AUTH_FAIL);
}
// 2.2 生成 Token
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(request.getUsername());
Assert.notNull(deviceInfo, "设备信息不能为空");
String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
Assert.notBlank(token, "生成 token 不能为空位");
@ -83,7 +69,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
// 3. 执行上线
IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
deviceMessageService.sendDeviceMessage(message,
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
// 构建响应数据
return success(MapUtil.of("token", token));

View File

@ -1,15 +1,13 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@ -33,27 +31,14 @@ public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析参数
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
String productKey = body.getString("productKey");
if (StrUtil.isEmpty(productKey)) {
throw invalidParamException("productKey 不能为空");
}
String deviceName = body.getString("deviceName");
if (StrUtil.isEmpty(deviceName)) {
throw invalidParamException("deviceName 不能为空");
}
String productSecret = body.getString("productSecret");
if (StrUtil.isEmpty(productSecret)) {
throw invalidParamException("productSecret 不能为空");
}
IotDeviceRegisterReqDTO request = deserializeRequest(context, IotDeviceRegisterReqDTO.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notBlank(request.getProductKey(), "productKey 不能为空");
Assert.notBlank(request.getDeviceName(), "deviceName 不能为空");
Assert.notBlank(request.getSign(), "sign 不能为空");
// 2. 调用动态注册
IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
.setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(request);
result.checkError();
// 3. 返回结果

View File

@ -1,17 +1,17 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.Data;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
@ -39,29 +39,31 @@ public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
@Override
public CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
// 1.1 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
// 1.2 解析子设备列表
SubDeviceRegisterRequest request = deserializeRequest(context, SubDeviceRegisterRequest.class);
Assert.notNull(request, "请求参数不能为空");
Assert.notEmpty(request.getParams(), "params 不能为空");
// 2. 解析子设备列表
JsonObject body = context.body().asJsonObject();
if (body == null) {
throw invalidParamException("请求体不能为空");
}
if (body.getJsonArray("params") == null) {
throw invalidParamException("params 不能为空");
}
List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
// 3. 调用子设备动态注册
// 2. 调用子设备动态注册
IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
.setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
.setGatewayProductKey(productKey)
.setGatewayDeviceName(deviceName)
.setSubDevices(request.getParams());
CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
result.checkError();
// 4. 返回结果
// 3. 返回结果
return success(result.getData());
}
@Data
public static class SubDeviceRegisterRequest {
private List<IotSubDeviceRegisterReqDTO> params;
}
}

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
package cn.iocoder.yudao.module.iot.gateway.protocol.http.handler.upstream;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@ -6,55 +6,47 @@ import cn.hutool.core.text.StrPool;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpProtocol;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
/**
* IoT HTTP
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
public static final String PATH = "/topic/sys/:productKey/:deviceName/*";
private final IotHttpUpstreamProtocol protocol;
private final String serverId;
private final IotDeviceMessageService deviceMessageService;
public IotHttpUpstreamHandler(IotHttpUpstreamProtocol protocol) {
this.protocol = protocol;
public IotHttpUpstreamHandler(IotHttpProtocol protocol) {
this.serverId = protocol.getServerId();
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
}
@Override
protected CommonResult<Object> handle0(RoutingContext context) {
// 1. 解析通用参数
// 1.1 解析通用参数
String productKey = context.pathParam("productKey");
String deviceName = context.pathParam("deviceName");
String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
// 2.1 解析消息
if (context.body().buffer() == null) {
throw invalidParamException("请求体不能为空");
}
byte[] bytes = context.body().buffer().getBytes();
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
productKey, deviceName);
// 1.2 根据 Content-Type 反序列化消息
IotDeviceMessage message = deserializeRequest(context, IotDeviceMessage.class);
Assert.notNull(message, "请求参数不能为空");
Assert.equals(method, message.getMethod(), "method 不匹配");
// 2.2 发送消息
// 2. 发送消息
deviceMessageService.sendDeviceMessage(message,
productKey, deviceName, protocol.getServerId());
productKey, deviceName, serverId);
// 3. 返回结果
return CommonResult.success(MapUtil.of("messageId", message.getId()));
}
}
}

View File

@ -0,0 +1,278 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.manager;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import io.vertx.core.Vertx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* Modbus
* <p>
* per-device
* {@link #pollPoint(Long, Long)}
* <p>
*
* @author
*/
@Slf4j
public abstract class AbstractIotModbusPollScheduler {
protected final Vertx vertx;
/**
* Modbus
*/
private static final long MIN_REQUEST_INTERVAL = 1000;
/**
*
*/
private static final int MAX_QUEUE_SIZE = 1000;
/**
* deviceId -> (pointId -> PointTimerInfo)
*/
private final Map<Long, Map<Long, PointTimerInfo>> devicePointTimers = new ConcurrentHashMap<>();
/**
* per-device deviceId ->
*/
private final Map<Long, Queue<Runnable>> deviceRequestQueues = new ConcurrentHashMap<>();
/**
* per-device deviceId -> lastRequestTimeMs
*/
private final Map<Long, Long> deviceLastRequestTime = new ConcurrentHashMap<>();
/**
* per-device timer deviceId -> timer
*/
private final Map<Long, Boolean> deviceDelayTimerActive = new ConcurrentHashMap<>();
protected AbstractIotModbusPollScheduler(Vertx vertx) {
this.vertx = vertx;
}
/**
*
*/
@Data
@AllArgsConstructor
private static class PointTimerInfo {
/**
* Vert.x ID
*/
private Long timerId;
/**
*
*/
private Integer pollInterval;
}
// ========== 轮询管理 ==========
/**
*
*
* 1.
* 2.
* 3. pollInterval
* pollPoint configCache point
*/
public void updatePolling(IotModbusDeviceConfigRespDTO config) {
Long deviceId = config.getDeviceId();
List<IotModbusPointRespDTO> newPoints = config.getPoints();
Map<Long, PointTimerInfo> currentTimers = devicePointTimers
.computeIfAbsent(deviceId, k -> new ConcurrentHashMap<>());
// 1.1 计算新配置中的点位 ID 集合
Set<Long> newPointIds = convertSet(newPoints, IotModbusPointRespDTO::getId);
// 1.2 计算删除的点位 ID 集合
Set<Long> removedPointIds = new HashSet<>(currentTimers.keySet());
removedPointIds.removeAll(newPointIds);
// 2. 处理删除的点位:停止不再存在的定时器
for (Long pointId : removedPointIds) {
PointTimerInfo timerInfo = currentTimers.remove(pointId);
if (timerInfo != null) {
vertx.cancelTimer(timerInfo.getTimerId());
log.debug("[updatePolling][设备 {} 点位 {} 定时器已删除]", deviceId, pointId);
}
}
// 3. 处理新增和修改的点位
if (CollUtil.isEmpty(newPoints)) {
return;
}
for (IotModbusPointRespDTO point : newPoints) {
Long pointId = point.getId();
Integer newPollInterval = point.getPollInterval();
PointTimerInfo existingTimer = currentTimers.get(pointId);
// 3.1 新增点位:创建定时器
if (existingTimer == null) {
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已创建, interval={}ms]",
deviceId, pointId, newPollInterval);
}
} else if (!Objects.equals(existingTimer.getPollInterval(), newPollInterval)) {
// 3.2 pollInterval 变化:重建定时器
vertx.cancelTimer(existingTimer.getTimerId());
Long timerId = createPollTimer(deviceId, pointId, newPollInterval);
if (timerId != null) {
currentTimers.put(pointId, new PointTimerInfo(timerId, newPollInterval));
log.debug("[updatePolling][设备 {} 点位 {} 定时器已更新, interval={}ms -> {}ms]",
deviceId, pointId, existingTimer.getPollInterval(), newPollInterval);
} else {
currentTimers.remove(pointId);
}
}
// 3.3 其他属性变化:无需重建定时器,因为 pollPoint() 运行时从 configCache 获取最新 point自动使用新配置
}
}
/**
*
*/
private Long createPollTimer(Long deviceId, Long pointId, Integer pollInterval) {
if (pollInterval == null || pollInterval <= 0) {
return null;
}
return vertx.setPeriodic(pollInterval, timerId -> {
try {
submitPollRequest(deviceId, pointId);
} catch (Exception e) {
log.error("[createPollTimer][轮询点位失败, deviceId={}, pointId={}]", deviceId, pointId, e);
}
});
}
// ========== 请求队列per-device 限速) ==========
/**
*
*/
private void submitPollRequest(Long deviceId, Long pointId) {
// 1. 【重要】将请求添加到设备的请求队列
Queue<Runnable> queue = deviceRequestQueues.computeIfAbsent(deviceId, k -> new ConcurrentLinkedQueue<>());
while (queue.size() >= MAX_QUEUE_SIZE) {
// 超出上限时,丢弃最旧的请求
queue.poll();
log.warn("[submitPollRequest][设备 {} 请求队列已满({}), 丢弃最旧请求]", deviceId, MAX_QUEUE_SIZE);
}
queue.offer(() -> pollPoint(deviceId, pointId));
// 2. 处理设备请求队列(如果没有延迟 timer 在等待)
processDeviceQueue(deviceId);
}
/**
*
*/
private void processDeviceQueue(Long deviceId) {
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 检查是否已有延迟 timer 在等待
if (Boolean.TRUE.equals(deviceDelayTimerActive.get(deviceId))) {
return;
}
// 不满足间隔要求,延迟执行
long now = System.currentTimeMillis();
long lastTime = deviceLastRequestTime.getOrDefault(deviceId, 0L);
long elapsed = now - lastTime;
if (elapsed < MIN_REQUEST_INTERVAL) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL - elapsed);
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, now);
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
}
private void scheduleNextRequest(Long deviceId) {
scheduleNextRequest(deviceId, MIN_REQUEST_INTERVAL);
}
private void scheduleNextRequest(Long deviceId, long delayMs) {
deviceDelayTimerActive.put(deviceId, true);
vertx.setTimer(delayMs, id -> {
deviceDelayTimerActive.put(deviceId, false);
Queue<Runnable> queue = deviceRequestQueues.get(deviceId);
if (CollUtil.isEmpty(queue)) {
return;
}
// 满足间隔要求,立即执行
Runnable task = queue.poll();
if (task == null) {
return;
}
deviceLastRequestTime.put(deviceId, System.currentTimeMillis());
task.run();
// 继续处理队列中的下一个(如果有的话,需要延迟)
if (CollUtil.isNotEmpty(queue)) {
scheduleNextRequest(deviceId);
}
});
}
// ========== 轮询执行 ==========
/**
*
*
* @param deviceId ID
* @param pointId ID
*/
protected abstract void pollPoint(Long deviceId, Long pointId);
// ========== 停止 ==========
/**
*
*/
public void stopPolling(Long deviceId) {
Map<Long, PointTimerInfo> timers = devicePointTimers.remove(deviceId);
if (CollUtil.isEmpty(timers)) {
return;
}
for (PointTimerInfo timerInfo : timers.values()) {
vertx.cancelTimer(timerInfo.getTimerId());
}
// 清理请求队列
deviceRequestQueues.remove(deviceId);
deviceLastRequestTime.remove(deviceId);
deviceDelayTimerActive.remove(deviceId);
log.debug("[stopPolling][设备 {} 停止了 {} 个轮询定时器]", deviceId, timers.size());
}
/**
*
*/
public void stopAll() {
for (Long deviceId : new ArrayList<>(devicePointTimers.keySet())) {
stopPolling(deviceId);
}
}
}

View File

@ -0,0 +1,557 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusByteOrderEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusRawDataTypeEnum;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpserver.codec.IotModbusFrame;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* IoT Modbus
* <p>
* Modbus
* <ul>
* <li>FC01~FC16</li>
* <li>//</li>
* <li>CRC-16/MODBUS </li>
* <li> {@link #convertToPropertyValue} / {@link #convertToRawValues}</li>
* <li> Modbus /线{@link #extractValues}</li>
* <li>{@link #findPoint}</li>
* </ul>
*
* @author
*/
@UtilityClass
@Slf4j
public class IotModbusCommonUtils {
/** FC01: 读线圈 */
public static final int FC_READ_COILS = 1;
/** FC02: 读离散输入 */
public static final int FC_READ_DISCRETE_INPUTS = 2;
/** FC03: 读保持寄存器 */
public static final int FC_READ_HOLDING_REGISTERS = 3;
/** FC04: 读输入寄存器 */
public static final int FC_READ_INPUT_REGISTERS = 4;
/** FC05: 写单个线圈 */
public static final int FC_WRITE_SINGLE_COIL = 5;
/** FC06: 写单个寄存器 */
public static final int FC_WRITE_SINGLE_REGISTER = 6;
/** FC15: 写多个线圈 */
public static final int FC_WRITE_MULTIPLE_COILS = 15;
/** FC16: 写多个寄存器 */
public static final int FC_WRITE_MULTIPLE_REGISTERS = 16;
/**
* 1
* FC=0x03 FC=0x830x03 | 0x80
*/
public static final int FC_EXCEPTION_MASK = 0x80;
/**
*
* FC=0x83 FC = 0x83 & 0x7F = 0x03
*/
public static final int FC_MASK = 0x7F;
// ==================== 功能码分类判断 ====================
/**
* FC01-04
*/
public static boolean isReadResponse(int functionCode) {
return functionCode >= FC_READ_COILS && functionCode <= FC_READ_INPUT_REGISTERS;
}
/**
* FC05/06/15/16
*/
public static boolean isWriteResponse(int functionCode) {
return functionCode == FC_WRITE_SINGLE_COIL || functionCode == FC_WRITE_SINGLE_REGISTER
|| functionCode == FC_WRITE_MULTIPLE_COILS || functionCode == FC_WRITE_MULTIPLE_REGISTERS;
}
/**
*
*/
public static boolean isExceptionResponse(int functionCode) {
return (functionCode & FC_EXCEPTION_MASK) != 0;
}
/**
*
*/
public static int extractOriginalFunctionCode(int exceptionFunctionCode) {
return exceptionFunctionCode & FC_MASK;
}
/**
*
* <p>
* FC01线 FC03
* FC02 FC04
*
* @param readFunctionCode FC01-04
* @return
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isWritable(int readFunctionCode) {
return readFunctionCode == FC_READ_COILS || readFunctionCode == FC_READ_HOLDING_REGISTERS;
}
/**
*
* <p>
* FC01线 FC05线
* FC03 FC06
* null
*
* @param readFunctionCode
* @return null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteSingleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_SINGLE_COIL;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_SINGLE_REGISTER;
default:
return null;
}
}
/**
*
* <p>
* FC01线 FC15线
* FC03 FC16
* null
*
* @param readFunctionCode
* @return null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static Integer getWriteMultipleFunctionCode(int readFunctionCode) {
switch (readFunctionCode) {
case FC_READ_COILS:
return FC_WRITE_MULTIPLE_COILS;
case FC_READ_HOLDING_REGISTERS:
return FC_WRITE_MULTIPLE_REGISTERS;
default:
return null;
}
}
// ==================== CRC16 工具 ====================
/**
* CRC-16/MODBUS
*
* @param data
* @param length
* @return CRC16
*/
public static int calculateCrc16(byte[] data, int length) {
int crc = 0xFFFF;
for (int i = 0; i < length; i++) {
crc ^= (data[i] & 0xFF);
for (int j = 0; j < 8; j++) {
if ((crc & 0x0001) != 0) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
/**
* CRC16
*
* @param data CRC
* @return
*/
public static boolean verifyCrc16(byte[] data) {
if (data.length < 3) {
return false;
}
int computed = calculateCrc16(data, data.length - 2);
int received = (data[data.length - 2] & 0xFF) | ((data[data.length - 1] & 0xFF) << 8);
return computed == received;
}
// ==================== 数据转换 ====================
/**
*
*
* @param rawValues 线
* @param point
* @return
*/
public static Object convertToPropertyValue(int[] rawValues, IotModbusPointRespDTO point) {
if (ArrayUtil.isEmpty(rawValues)) {
return null;
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
// 1. 根据原始数据类型解析原始数值
Number rawNumber = parseRawValue(rawValues, rawDataType, byteOrder);
if (rawNumber == null) {
return null;
}
// 2. 应用缩放因子:实际值 = 原始值 × scale
BigDecimal actualValue = new BigDecimal(rawNumber.toString()).multiply(scale);
// 3. 根据数据类型返回合适的 Java 类型
return formatValue(actualValue, rawDataType);
}
/**
*
*
* @param propertyValue
* @param point
* @return
*/
public static int[] convertToRawValues(Object propertyValue, IotModbusPointRespDTO point) {
if (propertyValue == null) {
return new int[0];
}
String rawDataType = point.getRawDataType();
String byteOrder = point.getByteOrder();
BigDecimal scale = ObjectUtil.defaultIfNull(point.getScale(), BigDecimal.ONE);
int registerCount = ObjectUtil.defaultIfNull(point.getRegisterCount(), 1);
// 1. 转换为 BigDecimal
BigDecimal actualValue = new BigDecimal(propertyValue.toString());
// 2. 应用缩放因子:原始值 = 实际值 ÷ scale
BigDecimal rawValue = actualValue.divide(scale, 0, RoundingMode.HALF_UP);
// 3. 根据原始数据类型编码为寄存器值
return encodeToRegisters(rawValue, rawDataType, byteOrder, registerCount);
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Number parseRawValue(int[] rawValues, String rawDataType, String byteOrder) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
switch (dataTypeEnum) {
case BOOLEAN:
return rawValues[0] != 0 ? 1 : 0;
case INT16:
return (short) rawValues[0];
case UINT16:
return rawValues[0] & 0xFFFF;
case INT32:
return parseInt32(rawValues, byteOrder);
case UINT32:
return parseUint32(rawValues, byteOrder);
case FLOAT:
return parseFloat(rawValues, byteOrder);
case DOUBLE:
return parseDouble(rawValues, byteOrder);
default:
log.warn("[parseRawValue][不支持的数据类型: {}]", rawDataType);
return rawValues[0];
}
}
private static int parseInt32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt();
}
private static long parseUint32(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return rawValues[0] & 0xFFFFFFFFL;
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getInt() & 0xFFFFFFFFL;
}
private static float parseFloat(int[] rawValues, String byteOrder) {
if (rawValues.length < 2) {
return (float) rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 2), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
}
private static double parseDouble(int[] rawValues, String byteOrder) {
if (rawValues.length < 4) {
return rawValues[0];
}
byte[] bytes = reorderBytes(registersToBytes(rawValues, 4), byteOrder);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getDouble();
}
private static byte[] registersToBytes(int[] registers, int count) {
byte[] bytes = new byte[count * 2];
for (int i = 0; i < Math.min(registers.length, count); i++) {
bytes[i * 2] = (byte) ((registers[i] >> 8) & 0xFF);
bytes[i * 2 + 1] = (byte) (registers[i] & 0xFF);
}
return bytes;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static byte[] reorderBytes(byte[] bytes, String byteOrder) {
IotModbusByteOrderEnum byteOrderEnum = IotModbusByteOrderEnum.getByOrder(byteOrder);
// null 或者大端序,不需要调整
if (ObjectUtils.equalsAny(byteOrderEnum, null, IotModbusByteOrderEnum.ABCD, IotModbusByteOrderEnum.AB)) {
return bytes;
}
// 其他字节序调整
byte[] result = new byte[bytes.length];
switch (byteOrderEnum) {
case BA: // 小端序:按每 2 字节一组交换16 位场景 [1,0]32 位场景 [1,0,3,2]
for (int i = 0; i + 1 < bytes.length; i += 2) {
result[i] = bytes[i + 1];
result[i + 1] = bytes[i];
}
break;
case CDAB: // 大端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[2];
result[1] = bytes[3];
result[2] = bytes[0];
result[3] = bytes[1];
}
break;
case DCBA: // 小端序32 位)
if (bytes.length >= 4) {
result[0] = bytes[3];
result[1] = bytes[2];
result[2] = bytes[1];
result[3] = bytes[0];
}
break;
case BADC: // 小端字交换32 位)
if (bytes.length >= 4) {
result[0] = bytes[1];
result[1] = bytes[0];
result[2] = bytes[3];
result[3] = bytes[2];
}
break;
default:
return bytes;
}
return result;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] encodeToRegisters(BigDecimal rawValue, String rawDataType, String byteOrder, int registerCount) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return new int[]{rawValue.intValue()};
}
switch (dataTypeEnum) {
case BOOLEAN:
return new int[]{rawValue.intValue() != 0 ? 1 : 0};
case INT16:
case UINT16:
return new int[]{rawValue.intValue() & 0xFFFF};
case INT32:
return encodeInt32(rawValue.intValue(), byteOrder);
case UINT32:
// 使用 longValue() 避免超过 Integer.MAX_VALUE 时溢出,
// 强转 int 保留低 32 位 bit pattern写入寄存器的字节是正确的无符号值
return encodeInt32((int) rawValue.longValue(), byteOrder);
case FLOAT:
return encodeFloat(rawValue.floatValue(), byteOrder);
case DOUBLE:
return encodeDouble(rawValue.doubleValue(), byteOrder);
default:
return new int[]{rawValue.intValue()};
}
}
private static int[] encodeInt32(int value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeFloat(float value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putFloat(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] encodeDouble(double value, String byteOrder) {
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putDouble(value).array();
bytes = reorderBytes(bytes, byteOrder);
return bytesToRegisters(bytes);
}
private static int[] bytesToRegisters(byte[] bytes) {
int[] registers = new int[bytes.length / 2];
for (int i = 0; i < registers.length; i++) {
registers[i] = ((bytes[i * 2] & 0xFF) << 8) | (bytes[i * 2 + 1] & 0xFF);
}
return registers;
}
@SuppressWarnings("EnhancedSwitchMigration")
private static Object formatValue(BigDecimal value, String rawDataType) {
IotModbusRawDataTypeEnum dataTypeEnum = IotModbusRawDataTypeEnum.getByType(rawDataType);
if (dataTypeEnum == null) {
return value;
}
switch (dataTypeEnum) {
case BOOLEAN:
return value.intValue() != 0;
case INT16:
case INT32:
return value.intValue();
case UINT16:
case UINT32:
return value.longValue();
case FLOAT:
return value.floatValue();
case DOUBLE:
return value.doubleValue();
default:
return value;
}
}
// ==================== 帧值提取 ====================
/**
* FC01-04
*
* @param frame Modbus
* @return int[] null
*/
@SuppressWarnings("EnhancedSwitchMigration")
public static int[] extractValues(IotModbusFrame frame) {
if (frame == null || frame.isException()) {
return null;
}
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return null;
}
int functionCode = frame.getFunctionCode();
switch (functionCode) {
case FC_READ_COILS:
case FC_READ_DISCRETE_INPUTS:
return extractCoilValues(pdu);
case FC_READ_HOLDING_REGISTERS:
case FC_READ_INPUT_REGISTERS:
return extractRegisterValues(pdu);
default:
log.warn("[extractValues][不支持的功能码: {}]", functionCode);
return null;
}
}
private static int[] extractCoilValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int bitCount = byteCount * 8;
int[] values = new int[bitCount];
for (int i = 0; i < bitCount && (1 + i / 8) < pdu.length; i++) {
values[i] = ((pdu[1 + i / 8] >> (i % 8)) & 0x01);
}
return values;
}
private static int[] extractRegisterValues(byte[] pdu) {
if (pdu.length < 2) {
return null;
}
int byteCount = pdu[0] & 0xFF;
int registerCount = byteCount / 2;
int[] values = new int[registerCount];
for (int i = 0; i < registerCount && (1 + i * 2 + 1) < pdu.length; i++) {
values[i] = ((pdu[1 + i * 2] & 0xFF) << 8) | (pdu[1 + i * 2 + 1] & 0xFF);
}
return values;
}
/**
* registerCount PDU byteCount
*
* @param frame Modbus
* @return registerCount -1
*/
public static int extractRegisterCountFromResponse(IotModbusFrame frame) {
byte[] pdu = frame.getPdu();
if (pdu == null || pdu.length < 1) {
return -1;
}
int byteCount = pdu[0] & 0xFF;
int fc = frame.getFunctionCode();
// FC03/04 寄存器读响应registerCount = byteCount / 2
if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
return byteCount / 2;
}
// FC01/02 线圈/离散输入读响应:按 bit 打包有余位,无法精确反推,返回 -1 跳过校验
return -1;
}
// ==================== 点位查找 ====================
/**
*
*
* @param config Modbus
* @param identifier
* @return null
*/
public static IotModbusPointRespDTO findPoint(IotModbusDeviceConfigRespDTO config, String identifier) {
if (config == null || StrUtil.isBlank(identifier)) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> identifier.equals(p.getIdentifier()));
}
/**
* ID
*
* @param config Modbus
* @param pointId ID
* @return null
*/
public static IotModbusPointRespDTO findPointById(IotModbusDeviceConfigRespDTO config, Long pointId) {
if (config == null || pointId == null) {
return null;
}
return CollUtil.findOne(config.getPoints(), p -> p.getId().equals(pointId));
}
}

View File

@ -0,0 +1,195 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import com.ghgande.j2mod.modbus.io.ModbusTCPTransaction;
import com.ghgande.j2mod.modbus.msg.*;
import com.ghgande.j2mod.modbus.procimg.InputRegister;
import com.ghgande.j2mod.modbus.procimg.Register;
import com.ghgande.j2mod.modbus.procimg.SimpleRegister;
import com.ghgande.j2mod.modbus.util.BitVector;
import io.vertx.core.Future;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils.*;
/**
* IoT Modbus TCP
* <p>
* j2mod Modbus TCP
* 1. Modbus /
* 2. {@link IotModbusTcpClientConnectionManager.ModbusConnection}
* 3.
*
* @author
*/
@UtilityClass
@Slf4j
public class IotModbusTcpClientUtils {
/**
* Modbus
*
* @param connection Modbus
* @param slaveId
* @param point
* @return int
*/
public static Future<int[]> read(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createReadRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount());
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
// 3. 解析响应
ModbusResponse response = transaction.getResponse();
return extractValues(response, point.getFunctionCode());
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 读取失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
* Modbus
*
* @param connection Modbus
* @param slaveId
* @param point
* @param values
* @return
*/
public static Future<Boolean> write(IotModbusTcpClientConnectionManager.ModbusConnection connection,
Integer slaveId,
IotModbusPointRespDTO point,
int[] values) {
return connection.executeBlocking(tcpConnection -> {
try {
// 1. 创建请求
ModbusRequest request = createWriteRequest(point.getFunctionCode(),
point.getRegisterAddress(), point.getRegisterCount(), values);
if (request == null) {
throw new RuntimeException("功能码 " + point.getFunctionCode() + " 不支持写操作");
}
request.setUnitID(slaveId);
// 2. 执行事务(请求)
ModbusTCPTransaction transaction = new ModbusTCPTransaction(tcpConnection);
transaction.setRequest(request);
transaction.execute();
return true;
} catch (Exception e) {
throw new RuntimeException(String.format("Modbus 写入失败 [slaveId=%d, identifier=%s, address=%d]",
slaveId, point.getIdentifier(), point.getRegisterAddress()), e);
}
});
}
/**
*
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createReadRequest(Integer functionCode, Integer address, Integer count) {
switch (functionCode) {
case FC_READ_COILS:
return new ReadCoilsRequest(address, count);
case FC_READ_DISCRETE_INPUTS:
return new ReadInputDiscretesRequest(address, count);
case FC_READ_HOLDING_REGISTERS:
return new ReadMultipleRegistersRequest(address, count);
case FC_READ_INPUT_REGISTERS:
return new ReadInputRegistersRequest(address, count);
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
*
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static ModbusRequest createWriteRequest(Integer functionCode, Integer address, Integer count, int[] values) {
switch (functionCode) {
case FC_READ_COILS: // 写线圈(使用功能码 5 或 15
if (count == 1) {
return new WriteCoilRequest(address, values[0] != 0);
} else {
BitVector bv = new BitVector(count);
for (int i = 0; i < Math.min(values.length, count); i++) {
bv.setBit(i, values[i] != 0);
}
return new WriteMultipleCoilsRequest(address, bv);
}
case FC_READ_HOLDING_REGISTERS: // 写保持寄存器(使用功能码 6 或 16
if (count == 1) {
return new WriteSingleRegisterRequest(address, new SimpleRegister(values[0]));
} else {
Register[] registers = new SimpleRegister[count];
for (int i = 0; i < count; i++) {
registers[i] = new SimpleRegister(i < values.length ? values[i] : 0);
}
return new WriteMultipleRegistersRequest(address, registers);
}
case FC_READ_DISCRETE_INPUTS: // 只读
case FC_READ_INPUT_REGISTERS: // 只读
return null;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
/**
*
*/
@SuppressWarnings("EnhancedSwitchMigration")
private static int[] extractValues(ModbusResponse response, Integer functionCode) {
switch (functionCode) {
case FC_READ_COILS:
ReadCoilsResponse coilsResponse = (ReadCoilsResponse) response;
int bitCount = coilsResponse.getBitCount();
int[] coilValues = new int[bitCount];
for (int i = 0; i < bitCount; i++) {
coilValues[i] = coilsResponse.getCoilStatus(i) ? 1 : 0;
}
return coilValues;
case FC_READ_DISCRETE_INPUTS:
ReadInputDiscretesResponse discretesResponse = (ReadInputDiscretesResponse) response;
int discreteCount = discretesResponse.getBitCount();
int[] discreteValues = new int[discreteCount];
for (int i = 0; i < discreteCount; i++) {
discreteValues[i] = discretesResponse.getDiscreteStatus(i) ? 1 : 0;
}
return discreteValues;
case FC_READ_HOLDING_REGISTERS:
ReadMultipleRegistersResponse holdingResponse = (ReadMultipleRegistersResponse) response;
InputRegister[] holdingRegisters = holdingResponse.getRegisters();
int[] holdingValues = new int[holdingRegisters.length];
for (int i = 0; i < holdingRegisters.length; i++) {
holdingValues[i] = holdingRegisters[i].getValue();
}
return holdingValues;
case FC_READ_INPUT_REGISTERS:
ReadInputRegistersResponse inputResponse = (ReadInputRegistersResponse) response;
InputRegister[] inputRegisters = inputResponse.getRegisters();
int[] inputValues = new int[inputRegisters.length];
for (int i = 0; i < inputRegisters.length; i++) {
inputValues[i] = inputRegisters[i].getValue();
}
return inputValues;
default:
throw new IllegalArgumentException("不支持的功能码: " + functionCode);
}
}
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* IoT Modbus TCP Client
*
* @author
*/
@Data
public class IotModbusTcpClientConfig {
/**
*
*/
@NotNull(message = "配置刷新间隔不能为空")
@Min(value = 1, message = "配置刷新间隔不能小于 1 秒")
private Integer configRefreshInterval = 30;
}

View File

@ -0,0 +1,218 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient;
import cn.hutool.core.lang.Assert;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolProperties;
import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream.IotModbusTcpClientDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream.IotModbusTcpClientUpstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientPollScheduler;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* IoT Modbus TCP Client Modbus
*
* @author
*/
@Slf4j
public class IotModbusTcpClientProtocol implements IotProtocol {
/**
*
*/
private final ProtocolProperties properties;
/**
* ID
*/
@Getter
private final String serverId;
/**
*
*/
@Getter
private volatile boolean running = false;
/**
* Vert.x
*/
private final Vertx vertx;
/**
* ID
*/
private Long configRefreshTimerId;
/**
*
*/
private final IotModbusTcpClientConnectionManager connectionManager;
/**
*
*/
private IotModbusTcpClientDownstreamSubscriber downstreamSubscriber;
private final IotModbusTcpClientConfigCacheService configCacheService;
private final IotModbusTcpClientPollScheduler pollScheduler;
public IotModbusTcpClientProtocol(ProtocolProperties properties) {
IotModbusTcpClientConfig modbusTcpClientConfig = properties.getModbusTcpClient();
Assert.notNull(modbusTcpClientConfig, "Modbus TCP Client 协议配置modbusTcpClient不能为空");
this.properties = properties;
this.serverId = IotDeviceMessageUtils.generateServerId(properties.getPort());
// 初始化 Vertx
this.vertx = Vertx.vertx();
// 初始化 Manager
RedissonClient redissonClient = SpringUtil.getBean(RedissonClient.class);
IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
IotDeviceMessageService messageService = SpringUtil.getBean(IotDeviceMessageService.class);
this.configCacheService = new IotModbusTcpClientConfigCacheService(deviceApi);
this.connectionManager = new IotModbusTcpClientConnectionManager(redissonClient, vertx,
messageService, configCacheService, serverId);
// 初始化 Handler
IotModbusTcpClientUpstreamHandler upstreamHandler = new IotModbusTcpClientUpstreamHandler(messageService, serverId);
// 初始化轮询调度器
this.pollScheduler = new IotModbusTcpClientPollScheduler(vertx, connectionManager, upstreamHandler, configCacheService);
}
@Override
public String getId() {
return properties.getId();
}
@Override
public IotProtocolTypeEnum getType() {
return IotProtocolTypeEnum.MODBUS_TCP_CLIENT;
}
@Override
public void start() {
if (running) {
log.warn("[start][IoT Modbus TCP Client 协议 {} 已经在运行中]", getId());
return;
}
try {
// 1.1 首次加载配置
refreshConfig();
// 1.2 启动配置刷新定时器
int refreshInterval = properties.getModbusTcpClient().getConfigRefreshInterval();
configRefreshTimerId = vertx.setPeriodic(
TimeUnit.SECONDS.toMillis(refreshInterval),
id -> refreshConfig()
);
running = true;
log.info("[start][IoT Modbus TCP Client 协议 {} 启动成功serverId={}]", getId(), serverId);
// 2. 启动下行消息订阅者
IotMessageBus messageBus = SpringUtil.getBean(IotMessageBus.class);
IotModbusTcpClientDownstreamHandler downstreamHandler = new IotModbusTcpClientDownstreamHandler(connectionManager,
configCacheService);
this.downstreamSubscriber = new IotModbusTcpClientDownstreamSubscriber(this, downstreamHandler, messageBus);
this.downstreamSubscriber.start();
} catch (Exception e) {
log.error("[start][IoT Modbus TCP Client 协议 {} 启动失败]", getId(), e);
stop0();
throw e;
}
}
@Override
public void stop() {
if (!running) {
return;
}
stop0();
}
private void stop0() {
// 1. 停止下行消息订阅者
if (downstreamSubscriber != null) {
try {
downstreamSubscriber.stop();
log.info("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者已停止]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} 下行消息订阅者停止失败]", getId(), e);
}
downstreamSubscriber = null;
}
// 2.1 取消配置刷新定时器
if (configRefreshTimerId != null) {
vertx.cancelTimer(configRefreshTimerId);
configRefreshTimerId = null;
}
// 2.2 停止轮询调度器
pollScheduler.stopAll();
// 2.3 关闭所有连接
connectionManager.closeAll();
// 3. 关闭 Vert.x 实例
if (vertx != null) {
try {
vertx.close().result();
log.info("[stop][IoT Modbus TCP Client 协议 {} Vertx 已关闭]", getId());
} catch (Exception e) {
log.error("[stop][IoT Modbus TCP Client 协议 {} Vertx 关闭失败]", getId(), e);
}
}
running = false;
log.info("[stop][IoT Modbus TCP Client 协议 {} 已停止]", getId());
}
/**
*
*/
private synchronized void refreshConfig() {
try {
// 1. 从 biz 拉取最新配置API 失败时返回 null
List<IotModbusDeviceConfigRespDTO> configs = configCacheService.refreshConfig();
if (configs == null) {
log.warn("[refreshConfig][API 失败,跳过本轮刷新]");
return;
}
log.debug("[refreshConfig][获取到 {} 个 Modbus 设备配置]", configs.size());
// 2. 更新连接和轮询任务
for (IotModbusDeviceConfigRespDTO config : configs) {
try {
// 2.1 确保连接存在
connectionManager.ensureConnection(config);
// 2.2 更新轮询任务
pollScheduler.updatePolling(config);
} catch (Exception e) {
log.error("[refreshConfig][处理设备配置失败, deviceId={}]", config.getDeviceId(), e);
}
}
// 3. 清理已删除设备的资源
Set<Long> removedDeviceIds = configCacheService.cleanupRemovedDevices(configs);
for (Long deviceId : removedDeviceIds) {
pollScheduler.stopPolling(deviceId);
connectionManager.removeDevice(deviceId);
}
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
}
}
}

View File

@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusTcpClientUtils;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConfigCacheService;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager.IotModbusTcpClientConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP Client
* <p>
*
* 1. thing.service.property.set
* 2. Modbus TCP
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientDownstreamHandler {
private final IotModbusTcpClientConnectionManager connectionManager;
private final IotModbusTcpClientConfigCacheService configCacheService;
/**
*
*/
@SuppressWarnings({"unchecked", "DuplicatedCode"})
public void handle(IotDeviceMessage message) {
// 1.1 检查是否是属性设置消息
if (ObjUtil.equals(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), message.getMethod())) {
return;
}
if (ObjUtil.notEqual(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), message.getMethod())) {
log.warn("[handle][忽略非属性设置消息: {}]", message.getMethod());
return;
}
// 1.2 获取设备配置
IotModbusDeviceConfigRespDTO config = configCacheService.getConfig(message.getDeviceId());
if (config == null) {
log.warn("[handle][设备 {} 没有 Modbus 配置]", message.getDeviceId());
return;
}
// 2. 解析属性值并写入
Object params = message.getParams();
if (!(params instanceof Map)) {
log.warn("[handle][params 不是 Map 类型: {}]", params);
return;
}
Map<String, Object> propertyMap = (Map<String, Object>) params;
for (Map.Entry<String, Object> entry : propertyMap.entrySet()) {
String identifier = entry.getKey();
Object value = entry.getValue();
// 2.1 查找对应的点位配置
IotModbusPointRespDTO point = IotModbusCommonUtils.findPoint(config, identifier);
if (point == null) {
log.warn("[handle][设备 {} 没有点位配置: {}]", message.getDeviceId(), identifier);
continue;
}
// 2.2 检查是否支持写操作
if (!IotModbusCommonUtils.isWritable(point.getFunctionCode())) {
log.warn("[handle][点位 {} 不支持写操作, 功能码={}]", identifier, point.getFunctionCode());
continue;
}
// 2.3 执行写入
writeProperty(config, point, value);
}
}
/**
*
*/
private void writeProperty(IotModbusDeviceConfigRespDTO config, IotModbusPointRespDTO point, Object value) {
// 1.1 获取连接
IotModbusTcpClientConnectionManager.ModbusConnection connection = connectionManager.getConnection(config.getDeviceId());
if (connection == null) {
log.warn("[writeProperty][设备 {} 没有连接]", config.getDeviceId());
return;
}
// 1.2 获取 slave ID
Integer slaveId = connectionManager.getSlaveId(config.getDeviceId());
if (slaveId == null) {
log.warn("[writeProperty][设备 {} 没有 slaveId]", config.getDeviceId());
return;
}
// 2.1 转换属性值为原始值
int[] rawValues = IotModbusCommonUtils.convertToRawValues(value, point);
// 2.2 执行 Modbus 写入
IotModbusTcpClientUtils.write(connection, slaveId, point, rawValues)
.onSuccess(success -> log.info("[writeProperty][写入成功, deviceId={}, identifier={}, value={}]",
config.getDeviceId(), point.getIdentifier(), value))
.onFailure(e -> log.error("[writeProperty][写入失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e));
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.downstream;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.AbstractIotProtocolDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.IotModbusTcpClientProtocol;
import lombok.extern.slf4j.Slf4j;
/**
* IoT Modbus TCP 线
*
* @author
*/
@Slf4j
public class IotModbusTcpClientDownstreamSubscriber extends AbstractIotProtocolDownstreamSubscriber {
private final IotModbusTcpClientDownstreamHandler downstreamHandler;
public IotModbusTcpClientDownstreamSubscriber(IotModbusTcpClientProtocol protocol,
IotModbusTcpClientDownstreamHandler downstreamHandler,
IotMessageBus messageBus) {
super(protocol, messageBus);
this.downstreamHandler = downstreamHandler;
}
@Override
protected void handleMessage(IotDeviceMessage message) {
downstreamHandler.handle(message);
}
}

View File

@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.handler.upstream;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusPointRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.gateway.protocol.modbus.common.utils.IotModbusCommonUtils;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* IoT Modbus TCP
*
* @author
*/
@Slf4j
public class IotModbusTcpClientUpstreamHandler {
private final IotDeviceMessageService messageService;
private final String serverId;
public IotModbusTcpClientUpstreamHandler(IotDeviceMessageService messageService,
String serverId) {
this.messageService = messageService;
this.serverId = serverId;
}
/**
* Modbus
*
* @param config
* @param point
* @param rawValue int
*/
public void handleReadResult(IotModbusDeviceConfigRespDTO config,
IotModbusPointRespDTO point,
int[] rawValue) {
try {
// 1.1 转换原始值为物模型属性值(点位翻译)
Object convertedValue = IotModbusCommonUtils.convertToPropertyValue(rawValue, point);
log.debug("[handleReadResult][设备={}, 属性={}, 原始值={}, 转换值={}]",
config.getDeviceId(), point.getIdentifier(), rawValue, convertedValue);
// 1.2 构造属性上报消息
Map<String, Object> params = MapUtil.of(point.getIdentifier(), convertedValue);
IotDeviceMessage message = IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params);
// 2. 发送到消息总线
messageService.sendDeviceMessage(message, config.getProductKey(),
config.getDeviceName(), serverId);
} catch (Exception e) {
log.error("[handleReadResult][处理读取结果失败, deviceId={}, identifier={}]",
config.getDeviceId(), point.getIdentifier(), e);
}
}
}

View File

@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.modbus.tcpclient.manager;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigListReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotModbusDeviceConfigRespDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.core.enums.modbus.IotModbusModeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
* IoT Modbus TCP Client
*
* @author
*/
@RequiredArgsConstructor
@Slf4j
public class IotModbusTcpClientConfigCacheService {
private final IotDeviceCommonApi deviceApi;
/**
* deviceId ->
*/
private final Map<Long, IotModbusDeviceConfigRespDTO> configCache = new ConcurrentHashMap<>();
/**
* ID
*
* @see #cleanupRemovedDevices(List)
*/
private final Set<Long> knownDeviceIds = ConcurrentHashMap.newKeySet();
/**
*
*
* @return API null cleanup
*/
public List<IotModbusDeviceConfigRespDTO> refreshConfig() {
try {
// 1. 从远程获取配置
CommonResult<List<IotModbusDeviceConfigRespDTO>> result = deviceApi.getModbusDeviceConfigList(
new IotModbusDeviceConfigListReqDTO().setStatus(CommonStatusEnum.ENABLE.getStatus())
.setMode(IotModbusModeEnum.POLLING.getMode()).setProtocolType(IotProtocolTypeEnum.MODBUS_TCP_CLIENT.getType()));
result.checkError();
List<IotModbusDeviceConfigRespDTO> configs = result.getData();
// 2. 更新缓存(注意:不在这里更新 knownDeviceIds由 cleanupRemovedDevices 统一管理)
for (IotModbusDeviceConfigRespDTO config : configs) {
configCache.put(config.getDeviceId(), config);
}
return configs;
} catch (Exception e) {
log.error("[refreshConfig][刷新配置失败]", e);
return null;
}
}
/**
*
*
* @param deviceId ID
* @return
*/
public IotModbusDeviceConfigRespDTO getConfig(Long deviceId) {
return configCache.get(deviceId);
}
/**
* ID ID
*
* @param currentConfigs
* @return ID
*/
public Set<Long> cleanupRemovedDevices(List<IotModbusDeviceConfigRespDTO> currentConfigs) {
// 1.1 获取当前有效的设备 ID
Set<Long> currentDeviceIds = convertSet(currentConfigs, IotModbusDeviceConfigRespDTO::getDeviceId);
// 1.2 找出已删除的设备(基于旧的 knownDeviceIds
Set<Long> removedDeviceIds = new HashSet<>(knownDeviceIds);
removedDeviceIds.removeAll(currentDeviceIds);
// 2. 清理已删除设备的缓存
for (Long deviceId : removedDeviceIds) {
log.info("[cleanupRemovedDevices][清理已删除设备: {}]", deviceId);
configCache.remove(deviceId);
}
// 3. 更新已知设备 ID 集合为当前有效的设备 ID
knownDeviceIds.clear();
knownDeviceIds.addAll(currentDeviceIds);
return removedDeviceIds;
}
}

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