diff --git a/db/mysql/module/maku-module-iot.sql b/db/mysql/module/maku-module-iot.sql new file mode 100644 index 0000000000000000000000000000000000000000..cd3eb8ca207af487663a1a6113f8811288b80386 --- /dev/null +++ b/db/mysql/module/maku-module-iot.sql @@ -0,0 +1,141 @@ +-- 表结构 +CREATE TABLE `iot_device` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `code` varchar(255) NOT NULL COMMENT '编码', + `name` varchar(255) NOT NULL COMMENT '名称', + `type` int NOT NULL COMMENT '设备类型,1.手持设备,2.柜体,3传感设备', + `uid` varchar(255) NOT NULL COMMENT '唯一标识码', + `secret` varchar(255) DEFAULT NULL COMMENT '设备密钥', + `app_version` varchar(255) DEFAULT NULL COMMENT 'App版本号', + `battery_percent` varchar(10) DEFAULT NULL COMMENT '电池电量百分比', + `temperature` varchar(10) DEFAULT NULL COMMENT '温度', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态,0禁用,1启用', + `running_status` int NOT NULL DEFAULT '0' COMMENT '运行状态,0.离线状态 1.在线状态 2.正常待机 3.用户使用中 4.OTA升级中', + `up_time` datetime DEFAULT NULL COMMENT '上线时间', + `down_time` datetime DEFAULT NULL COMMENT '下线时间', + `tenant_id` bigint DEFAULT NULL COMMENT '租户ID', + `creator` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updater` bigint DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `version` int DEFAULT NULL COMMENT '版本号', + `deleted` tinyint DEFAULT NULL COMMENT '删除标识 0:正常 1:已删除', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备表'; + +CREATE TABLE `iot_device_event_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `device_id` bigint NOT NULL COMMENT '设备id', + `event_type` tinyint NOT NULL COMMENT '事件类型', + `event_uid` varchar(50) DEFAULT NULL COMMENT '事件标识id', + `event_payload` varchar(1000) DEFAULT NULL COMMENT '事件数据', + `event_time` datetime DEFAULT NULL COMMENT '事件时间', + `tenant_id` bigint DEFAULT NULL COMMENT '租户ID', + `version` int DEFAULT NULL COMMENT '版本号', + `deleted` tinyint DEFAULT NULL COMMENT '删除标识 0:正常 1:已删除', + `creator` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updater` bigint DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备事件日志'; + +CREATE TABLE `iot_device_service_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `device_id` bigint NOT NULL COMMENT '设备id', + `service_type` tinyint NOT NULL COMMENT '服务类型', + `service_uid` varchar(50) DEFAULT NULL COMMENT '服务标识id', + `service_payload` varchar(1000) DEFAULT NULL COMMENT '服务数据', + `service_time` datetime DEFAULT NULL COMMENT '服务时间', + `tenant_id` bigint DEFAULT NULL COMMENT '租户ID', + `version` int DEFAULT NULL COMMENT '版本号', + `deleted` tinyint DEFAULT NULL COMMENT '删除标识 0:正常 1:已删除', + `creator` bigint DEFAULT NULL COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `updater` bigint DEFAULT NULL COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备服务日志'; + + +-- 菜单,权限 +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) VALUES ( NULL, '物联网平台', NULL, NULL, 0, 0, 'icon-printer-fill', 6, 0, 0, 10000,now(), 10000, now()); + +SET @pid = @@identity; + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES ((SELECT @pid) , '设备列表', 'iot/device/index', NULL, 0, 0, 'icon-menu', 0, 0, 0, 10000, now(), 10000, now()); + +SET @menuId = @@identity; + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '查看', '', 'iot:device:page', 1, 0, '', 0, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '新增', '', 'iot:device:save', 1, 0, '', 1, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '修改', '', 'iot:device:update,iot:device:info', 1, 0, '', 2, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '删除', '', 'iot:device:delete', 1, 0, '', 3, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '下发指令', '', 'iot:device:send', 1, 0, '', 4, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '上报数据', '', 'iot:device:report', 1, 0, '', 5, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '设备事件日志', '', 'iot:device_event_log:page', 1, 0, '', 5, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_menu (pid, name, url, authority, type, open_style, icon, sort, version, deleted, creator, create_time, updater, update_time) +VALUES (@menuId, '设备服务日志', '', 'iot:device_service_log:page', 1, 0, '', 5, 0, 0, 10000, now(), 10000, now()); + +-- 字典数据 +INSERT INTO sys_dict_type (dict_type,dict_name,remark,sort,tenant_id,version,deleted,creator,create_time,updater,update_time )VALUE( 'device_type', '设备类型', '设备类型', 0, 10000, 0, 0, 10000, now(), 10000, now() ); +SET @typeId = @@identity; +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '手持设备', '1', 'primary', '', 0, 10000, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '柜体', '2', 'primary', '', 1, 10000, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '传感设备', '3', 'primary', '', 2, 10000, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_dict_type (dict_type,dict_name,remark,sort,tenant_id,version,deleted,creator,create_time,updater,update_time )VALUES( 'device_running_status', '设备运行状态', '设备运行状态:离线|在线|待机|使用中|OTA升级中', 0, 10000, 0, 0, 10000, now(), 10000, now() ); +SET @typeId = @@identity; +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '离线状态', '0', 'danger', NULL, 0, 10000, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '在线状态', '1', 'success', NULL, 1, 10000, 0, 0, 10000, now(), 10000, now()); + + +INSERT INTO sys_dict_type (dict_type,dict_name,remark,sort,tenant_id,version,deleted,creator,create_time,updater,update_time )VALUES( 'device_command', '设备指令', '设备服务具备的功能', 0, 10000, 0, 0, 10000, now(), 10000, now() ); +SET @typeId = @@identity; +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '远程锁定', 'LOCK', NULL, NULL, 0, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '远程解锁', 'UNLOCK', NULL, NULL, 1, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '登录', 'SIGN_ON', NULL, NULL, 2, NULL, 0, 1, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '登出', 'SIGN_OFF', NULL, NULL, 3, NULL, 0, 1, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), 'OTA升级', 'OTA_UPGRADE', NULL, NULL, 4, NULL, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_dict_type (dict_type,dict_name,remark,sort,tenant_id,version,deleted,creator,create_time,updater,update_time )VALUES( 'device_property', '设备属性', '设备通用属性:运行状态|APP版本|电池电量百分比|温度', 0, 10000, 0, 0, 10000, now(), 10000, now() ); +SET @typeId = @@identity; +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '运行状态', 'RUNNING_STATUS', NULL, NULL, 0, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), 'APP版本', 'APP_VERSION', NULL, NULL, 1, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '电池电量百分比', 'BATTERY_PERCENT', NULL, NULL, 2, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '温度', 'TEMPERATURE', NULL, NULL, 3, NULL, 0, 0, 10000, now(), 10000, now()); + +INSERT INTO sys_dict_type (dict_type,dict_name,remark,sort,tenant_id,version,deleted,creator,create_time,updater,update_time )VALUES( 'device_event_type', '事件类型', '事件日志类型', 0, 10000, 0, 0, 10000, now(), 10000, now() ); +SET @typeId = @@identity; +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '下线', 'OFFLINE', 'danger', NULL, 1, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '上线', 'ONLINE', 'primary', NULL, 2, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '登录', 'SIGN_ON', 'primary', NULL, 3, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '退出登录', 'SIGN_OFF', 'danger', NULL, 4, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), 'OTA升级', 'OTA_UPGRADE', 'primary', NULL, 5, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '设备远程锁定', 'LOCK', 'primary', NULL, 6, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '设备远程解锁', 'UNLOCK', 'primary', NULL,7, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), 'APP版本信息', 'APP_VERSION_REPORT', 'primary', NULL, 8, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '电池电量', 'BATTERY_PERCENT_REPORT', 'primary', NULL, 9, NULL, 0, 0, 10000, now(), 10000, now()); +INSERT INTO sys_dict_data (dict_type_id, dict_label, dict_value, label_class, remark, sort, tenant_id, version, deleted, creator, create_time, updater, update_time) VALUES ((SELECT @typeId), '温度', 'TEMPERATURE_REPORT', 'primary', NULL, 0, NULL, 10, 0, 10000, now(), 10000, now()); + + + + + + + diff --git a/deploy/docker-compose-emqx.yml b/deploy/docker-compose-emqx.yml new file mode 100644 index 0000000000000000000000000000000000000000..af84a448f852558291f46a9feffe920c980925ac --- /dev/null +++ b/deploy/docker-compose-emqx.yml @@ -0,0 +1,51 @@ +version: '3.9' +# 通过 Docker Compose 构建 EMQX 集群 +services: + maku-emqx1: + image: emqx:5.7.1 + container_name: maku-emqx1 + environment: + - "EMQX_NODE_NAME=emqx@node1.emqx.io" + - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" + - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" + healthcheck: + test: ["CMD", "/opt/emqx/bin/emqx", "ctl", "status"] + interval: 5s + timeout: 25s + retries: 5 + networks: + emqx-bridge: + aliases: + - node1.emqx.io + ports: + - 1883:1883 + - 8083:8083 + - 8084:8084 + - 8883:8883 + - 18083:18083 + # 如果需要持久 Docker 容器 ,请将以下目录挂载到容器外部,这样即使容器被删除数据也不会丢失 + volumes: + - /work/www/maku-boot/emqx/data:/opt/emqx/data + - /work/www/maku-boot/emqx/log:/opt/emqx/log +# maku-emqx2: +# image: emqx:5.7.1 +# container_name: maku-emqx2 +# environment: +# - "EMQX_NODE_NAME=emqx@node2.emqx.io" +# - "EMQX_CLUSTER__DISCOVERY_STRATEGY=static" +# - "EMQX_CLUSTER__STATIC__SEEDS=[emqx@node1.emqx.io,emqx@node2.emqx.io]" +# healthcheck: +# test: ["CMD", "/opt/emqx/bin/emqx", "ctl", "status"] +# interval: 5s +# timeout: 25s +# retries: 5 +# networks: +# emqx-bridge: +# aliases: +# - node2.emqx.io +# volumes: +# - $PWD/emqx2_data:/opt/emqx/data + +networks: + emqx-bridge: + driver: bridge \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/pom.xml b/maku-boot-module/maku-module-iot/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d38864a0b6c611e6fc6b3e2a1d7e8e12951980a9 --- /dev/null +++ b/maku-boot-module/maku-module-iot/pom.xml @@ -0,0 +1,54 @@ + + + net.maku + maku-boot-module + ${revision} + + 4.0.0 + maku-module-iot + jar + + + + net.maku + maku-framework + ${revision} + + + org.springframework.integration + spring-integration-mqtt + + + + + junit + junit + 4.13.2 + test + + + + + org.mockito + mockito-core + 4.0.0 + test + + + + org.mockito + mockito-junit-jupiter + 4.0.0 + test + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceController.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceController.java new file mode 100644 index 0000000000000000000000000000000000000000..1433db12670848a4ebe5aa9559df844010e3ad38 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceController.java @@ -0,0 +1,110 @@ +package net.maku.iot.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.common.utils.Result; +import net.maku.iot.convert.IotDeviceConvert; +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.query.IotDeviceQuery; +import net.maku.iot.service.IotDeviceService; +import net.maku.iot.vo.DeviceCommandVO; +import net.maku.iot.vo.DeviceReportAttributeDataVO; +import net.maku.iot.vo.IotDeviceVO; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@RestController +@RequestMapping("iot/device") +@Tag(name = "设备表") +@AllArgsConstructor +public class IotDeviceController { + private final IotDeviceService iotDeviceService; + + @GetMapping("/page") + @Operation(summary = "分页") + @PreAuthorize("hasAuthority('iot:device:page')") + public Result> page(@ParameterObject @Valid IotDeviceQuery query) { + PageResult page = iotDeviceService.page(query); + + return Result.ok(page); + } + + @GetMapping("/{id}") + @Operation(summary = "信息") + @PreAuthorize("hasAuthority('iot:device:info')") + public Result get(@PathVariable("id") Long id) { + IotDeviceEntity entity = iotDeviceService.getById(id); + + return Result.ok(IotDeviceConvert.INSTANCE.convert(entity)); + } + + @PostMapping + @Operation(summary = "保存") + @PreAuthorize("hasAuthority('iot:device:save')") + public Result save(@RequestBody IotDeviceVO vo) { + iotDeviceService.save(vo); + + return Result.ok(); + } + + @PutMapping + @Operation(summary = "修改") + @PreAuthorize("hasAuthority('iot:device:update')") + public Result update(@RequestBody @Valid IotDeviceVO vo) { + iotDeviceService.update(vo); + + return Result.ok(); + } + + @DeleteMapping + @Operation(summary = "删除") + @PreAuthorize("hasAuthority('iot:device:delete')") + public Result delete(@RequestBody List idList) { + iotDeviceService.delete(idList); + + return Result.ok(); + } + + @PostMapping("/asyncSendCommand") + @Operation(summary = "下发指令-不等待设备回复") + @PreAuthorize("hasAuthority('iot:device:send')") + public Result asyncSendCommand(@RequestBody DeviceCommandVO vo) { + iotDeviceService.asyncSendCommand(vo); + return Result.ok(); + } + + @PostMapping("/syncSendCommand") + @Operation(summary = "下发指令-等待设备回复") + @PreAuthorize("hasAuthority('iot:device:send')") + public Result syncSendCommand(@RequestBody DeviceCommandVO vo) { + iotDeviceService.syncSendCommand(vo); + return Result.ok(); + } + + @PostMapping("/syncSendCommand/debug") + @Operation(summary = "下发指令-等待设备回复-调试") + @PreAuthorize("hasAuthority('iot:device:send')") + public Result syncSendCommandDebug(@RequestBody DeviceCommandVO vo) { + return Result.ok(iotDeviceService.syncSendCommandDebug(vo).getResponsePayload()); + } + + @PostMapping("/simulateDeviceReportAttributeData") + @Operation(summary = "模拟设备属性数据上报") + @PreAuthorize("hasAuthority('iot:device:report')") + public Result simulateDeviceReportAttributeData(@RequestBody DeviceReportAttributeDataVO vo) { + iotDeviceService.simulateDeviceReportAttributeData(vo); + return Result.ok(); + } + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceEventLogController.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceEventLogController.java new file mode 100644 index 0000000000000000000000000000000000000000..dfb04f4ac4f3067461893df2649a12465eaa4ae1 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceEventLogController.java @@ -0,0 +1,76 @@ +package net.maku.iot.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.common.utils.Result; +import net.maku.iot.convert.IotDeviceEventLogConvert; +import net.maku.iot.entity.IotDeviceEventLogEntity; +import net.maku.iot.query.IotDeviceEventLogQuery; +import net.maku.iot.service.IotDeviceEventLogService; +import net.maku.iot.vo.IotDeviceEventLogVO; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@RestController +@RequestMapping("iot/device_event_log") +@Tag(name = "设备事件日志") +@AllArgsConstructor +public class IotDeviceEventLogController { + private final IotDeviceEventLogService iotDeviceEventLogService; + + @GetMapping("page") + @Operation(summary = "分页") + @PreAuthorize("hasAuthority('iot:device_event_log:page')") + public Result> page(@ParameterObject @Valid IotDeviceEventLogQuery query) { + PageResult page = iotDeviceEventLogService.page(query); + + return Result.ok(page); + } + + @GetMapping("{id}") + @Operation(summary = "信息") + @PreAuthorize("hasAuthority('iot:device_event_log:info')") + public Result get(@PathVariable("id") Long id) { + IotDeviceEventLogEntity entity = iotDeviceEventLogService.getById(id); + + return Result.ok(IotDeviceEventLogConvert.INSTANCE.convert(entity)); + } + + @PostMapping + @Operation(summary = "保存") + @PreAuthorize("hasAuthority('iot:device_event_log:save')") + public Result save(@RequestBody IotDeviceEventLogVO vo) { + iotDeviceEventLogService.save(vo); + + return Result.ok(); + } + + @PutMapping + @Operation(summary = "修改") + @PreAuthorize("hasAuthority('iot:device_event_log:update')") + public Result update(@RequestBody @Valid IotDeviceEventLogVO vo) { + iotDeviceEventLogService.update(vo); + + return Result.ok(); + } + + @DeleteMapping + @Operation(summary = "删除") + @PreAuthorize("hasAuthority('iot:device_event_log:delete')") + public Result delete(@RequestBody List idList) { + iotDeviceEventLogService.delete(idList); + + return Result.ok(); + } +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceServiceLogController.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceServiceLogController.java new file mode 100644 index 0000000000000000000000000000000000000000..6c76075277b0c7818fa3f30131e687b3571cb11d --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/controller/IotDeviceServiceLogController.java @@ -0,0 +1,76 @@ +package net.maku.iot.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.common.utils.Result; +import net.maku.iot.convert.IotDeviceServiceLogConvert; +import net.maku.iot.entity.IotDeviceServiceLogEntity; +import net.maku.iot.query.IotDeviceServiceLogQuery; +import net.maku.iot.service.IotDeviceServiceLogService; +import net.maku.iot.vo.IotDeviceServiceLogVO; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@RestController +@RequestMapping("iot/device_service_log") +@Tag(name = "设备服务日志") +@AllArgsConstructor +public class IotDeviceServiceLogController { + private final IotDeviceServiceLogService iotDeviceServiceLogService; + + @GetMapping("page") + @Operation(summary = "分页") + @PreAuthorize("hasAuthority('iot:device_service_log:page')") + public Result> page(@ParameterObject @Valid IotDeviceServiceLogQuery query) { + PageResult page = iotDeviceServiceLogService.page(query); + + return Result.ok(page); + } + + @GetMapping("{id}") + @Operation(summary = "信息") + @PreAuthorize("hasAuthority('iot:device_service_log:info')") + public Result get(@PathVariable("id") Long id) { + IotDeviceServiceLogEntity entity = iotDeviceServiceLogService.getById(id); + + return Result.ok(IotDeviceServiceLogConvert.INSTANCE.convert(entity)); + } + + @PostMapping + @Operation(summary = "保存") + @PreAuthorize("hasAuthority('iot:device_service_log:save')") + public Result save(@RequestBody IotDeviceServiceLogVO vo) { + iotDeviceServiceLogService.save(vo); + + return Result.ok(); + } + + @PutMapping + @Operation(summary = "修改") + @PreAuthorize("hasAuthority('iot:device_service_log:update')") + public Result update(@RequestBody @Valid IotDeviceServiceLogVO vo) { + iotDeviceServiceLogService.update(vo); + + return Result.ok(); + } + + @DeleteMapping + @Operation(summary = "删除") + @PreAuthorize("hasAuthority('iot:device_service_log:delete')") + public Result delete(@RequestBody List idList) { + iotDeviceServiceLogService.delete(idList); + + return Result.ok(); + } +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceConvert.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceConvert.java new file mode 100644 index 0000000000000000000000000000000000000000..c332ae6e79be51c63260a90f6a593b424b3c3ce8 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceConvert.java @@ -0,0 +1,25 @@ +package net.maku.iot.convert; + +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.vo.IotDeviceVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceConvert { + IotDeviceConvert INSTANCE = Mappers.getMapper(IotDeviceConvert.class); + + IotDeviceEntity convert(IotDeviceVO vo); + + IotDeviceVO convert(IotDeviceEntity entity); + + List convertList(List list); + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceEventLogConvert.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceEventLogConvert.java new file mode 100644 index 0000000000000000000000000000000000000000..68a1c9d638fca2207c7c4584cddcaa0927aef8f1 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceEventLogConvert.java @@ -0,0 +1,25 @@ +package net.maku.iot.convert; + +import net.maku.iot.entity.IotDeviceEventLogEntity; +import net.maku.iot.vo.IotDeviceEventLogVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceEventLogConvert { + IotDeviceEventLogConvert INSTANCE = Mappers.getMapper(IotDeviceEventLogConvert.class); + + IotDeviceEventLogEntity convert(IotDeviceEventLogVO vo); + + IotDeviceEventLogVO convert(IotDeviceEventLogEntity entity); + + List convertList(List list); + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceServiceLogConvert.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceServiceLogConvert.java new file mode 100644 index 0000000000000000000000000000000000000000..3b11ea711d8d6e13b08966c665468f18cda43cb8 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/convert/IotDeviceServiceLogConvert.java @@ -0,0 +1,25 @@ +package net.maku.iot.convert; + +import net.maku.iot.entity.IotDeviceServiceLogEntity; +import net.maku.iot.vo.IotDeviceServiceLogVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceServiceLogConvert { + IotDeviceServiceLogConvert INSTANCE = Mappers.getMapper(IotDeviceServiceLogConvert.class); + + IotDeviceServiceLogEntity convert(IotDeviceServiceLogVO vo); + + IotDeviceServiceLogVO convert(IotDeviceServiceLogEntity entity); + + List convertList(List list); + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceDao.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceDao.java new file mode 100644 index 0000000000000000000000000000000000000000..be01471304787470bf9f6271f5ad1ba3ad952b24 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceDao.java @@ -0,0 +1,15 @@ +package net.maku.iot.dao; + +import net.maku.framework.mybatis.dao.BaseDao; +import net.maku.iot.entity.IotDeviceEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceDao extends BaseDao { + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceEventLogDao.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceEventLogDao.java new file mode 100644 index 0000000000000000000000000000000000000000..4adc63b98ef4f6b59a1fd11ac94da81174828410 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceEventLogDao.java @@ -0,0 +1,15 @@ +package net.maku.iot.dao; + +import net.maku.framework.mybatis.dao.BaseDao; +import net.maku.iot.entity.IotDeviceEventLogEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceEventLogDao extends BaseDao { + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceServiceLogDao.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceServiceLogDao.java new file mode 100644 index 0000000000000000000000000000000000000000..509be3849e9e2da1c775d3e9c990ccd487f40847 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dao/IotDeviceServiceLogDao.java @@ -0,0 +1,15 @@ +package net.maku.iot.dao; + +import net.maku.framework.mybatis.dao.BaseDao; +import net.maku.iot.entity.IotDeviceServiceLogEntity; +import org.apache.ibatis.annotations.Mapper; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@Mapper +public interface IotDeviceServiceLogDao extends BaseDao { + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dto/DeviceClientDTO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dto/DeviceClientDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..667949e5b557d134cc42d2fec132436dbde7e99e --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/dto/DeviceClientDTO.java @@ -0,0 +1,88 @@ +package net.maku.iot.dto; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.enums.DeviceTypeEnum; + +import java.util.List; + +/** + * 设备客户端信息 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Builder +public class DeviceClientDTO { + /** + * 格式 : deviceType_tenantId_deviceId_uid + */ + private static final String CLIENT_ID_TEMPLATE = "{}_{}_{}_{}"; + private static final String INVALID_CLIENT_ID = "无效的设备clientId:{}"; + /** + * 租户id + */ + private Long tenantId; + + /** + * 设备id + */ + private Long deviceId; + + /** + * 设备类型 + */ + private String deviceType; + + /** + * 设备唯一标识 + */ + private String uid; + + /** + * 生成clientId + * + * @return + */ + public String buildClientId() { + if (StrUtil.isBlank(uid)) { + return null; + } + return StrUtil.format(CLIENT_ID_TEMPLATE, deviceType, tenantId, deviceId, uid); + } + + /** + * 从clientId解析设备client信息 + * + * @param clientId + * @return + */ + public static DeviceClientDTO parse(String clientId) { + List clientIdParts = StrUtil.split(clientId, "_"); + Assert.isTrue(clientIdParts.size() > 3, INVALID_CLIENT_ID, clientId); + return DeviceClientDTO.builder() + .deviceType(clientIdParts.get(0)) + .tenantId(Long.valueOf(clientIdParts.get(1))) + .deviceId(Long.valueOf(clientIdParts.get(2))) + .uid(clientIdParts.get(3)) + .build(); + } + + /** + * 从Device创建deviceClient + * + * @param device + * @return + */ + public static DeviceClientDTO from(IotDeviceEntity device) { + return DeviceClientDTO.builder() + .deviceType(DeviceTypeEnum.parse(device.getType().toString()).name().toLowerCase()) + .tenantId(device.getTenantId() == null ? 0 : device.getTenantId()) + .deviceId(device.getId()) + .uid(device.getUid()) + .build(); + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEntity.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..9f0340991eb42d6d3725302017c3e5cc91775290 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEntity.java @@ -0,0 +1,86 @@ +package net.maku.iot.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.mybatis.entity.BaseEntity; + +import java.time.LocalDateTime; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@EqualsAndHashCode(callSuper = false) +@Data +@TableName("iot_device") +public class IotDeviceEntity extends BaseEntity { + + /** + * 编码 + */ + private String code; + + /** + * 名称 + */ + private String name; + + /** + * 设备类型,1.手持设备,2.柜体,3传感设备 + */ + private Integer type; + + /** + * 唯一标识码 + */ + private String uid; + + /** + * 设备密钥 + */ + private String secret; + + /** + * App版本号 + */ + private String appVersion; + + /** + * 电池电量百分比 + */ + private String batteryPercent; + + /** + * 温度 + */ + private String temperature; + + /** + * 状态,0禁用,1正常 + */ + private Integer status; + + /** + * 运行状态,0.离线状态 1.在线状态 2.正常待机 3.用户使用中 4.OTA升级中 + */ + private Integer runningStatus; + + /** + * 上线时间 + */ + private LocalDateTime upTime; + + /** + * 下线时间 + */ + private LocalDateTime downTime; + + /** + * 租户ID + */ + private Long tenantId; + + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEventLogEntity.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEventLogEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..2b37b4908b949f99c582e80cbe10471a76574e8a --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceEventLogEntity.java @@ -0,0 +1,51 @@ +package net.maku.iot.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.mybatis.entity.BaseEntity; + +import java.time.LocalDateTime; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@EqualsAndHashCode(callSuper = false) +@Data +@TableName("iot_device_event_log") +public class IotDeviceEventLogEntity extends BaseEntity { + + /** + * 设备id + */ + private Long deviceId; + + /** + * 事件类型 + */ + private Integer eventType; + + /** + * 事件标识id + */ + private String eventUid; + + /** + * 事件数据 + */ + private String eventPayload; + + /** + * 事件时间 + */ + private LocalDateTime eventTime; + + /** + * 租户ID + */ + private Long tenantId; + + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceServiceLogEntity.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceServiceLogEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..4b20796890b5965e6e0e5ebabb2f1324d32bde5a --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/entity/IotDeviceServiceLogEntity.java @@ -0,0 +1,51 @@ +package net.maku.iot.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.mybatis.entity.BaseEntity; + +import java.time.LocalDateTime; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@EqualsAndHashCode(callSuper = false) +@Data +@TableName("iot_device_service_log") +public class IotDeviceServiceLogEntity extends BaseEntity { + + /** + * 设备id + */ + private Long deviceId; + + /** + * 服务类型 + */ + private Integer serviceType; + + /** + * 服务标识id + */ + private String serviceUid; + + /** + * 服务数据 + */ + private String servicePayload; + + /** + * 服务时间 + */ + private LocalDateTime serviceTime; + + /** + * 租户ID + */ + private Long tenantId; + + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceCommandEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceCommandEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..4f726806e89ed6f02d6ce87e0a1356f5d2c5213d --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceCommandEnum.java @@ -0,0 +1,78 @@ +package net.maku.iot.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 服务端下发的指令枚举类 + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DeviceCommandEnum { + + /** + * 远程锁定 + */ + LOCK(1, "远程锁定", DeviceEventTypeEnum.LOCK), + + /** + * 远程解锁 + */ + UNLOCK(2, "远程解锁", DeviceEventTypeEnum.UNLOCK), + + /** + * OTA升级 + */ + OTA_UPGRADE(3, "OTA升级", DeviceEventTypeEnum.OTA_UPGRADE); + + /** + * 类型值 + */ + private final Integer value; + + /** + * 显示名称 + */ + private final String title; + + + /** + * 对应的设备事件日志类型 + */ + private final DeviceEventTypeEnum eventType; + + /** + * 解析指定的valueName + * + * @param valueName + * @return DeviceCommandEnum + * @throws IllegalArgumentException + */ + public static DeviceCommandEnum parse(String valueName) { + return Arrays.stream(DeviceCommandEnum.values()) + .filter(d -> d.name().equalsIgnoreCase(valueName)).findFirst() + .orElseThrow(() -> new IllegalArgumentException( + StrUtil.format("无效的DeviceCommandEnum值:{}", valueName))); + } + + /** + * 根据 value值 获取对应枚举 + * + * @param value + * @return DeviceCommandEnum + */ + public static DeviceCommandEnum getEnum(Integer value) { + for (DeviceCommandEnum commandEnum : DeviceCommandEnum.values()) { + if (commandEnum.getValue().equals(value)) { + return commandEnum; + } + } + return null; + } +} + diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceEventTypeEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceEventTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..f3578e7c441c5f4e7469b2bd46ce1a4df8cc1987 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceEventTypeEnum.java @@ -0,0 +1,105 @@ +package net.maku.iot.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 设备事件类型枚举 + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DeviceEventTypeEnum { + /** + * 下线 + */ + OFFLINE(0, "下线事件"), + + /** + * 上线 + */ + ONLINE(1, "上线事件"), + + /** + * 登录 + */ + SIGN_ON(2, "登录事件"), + + /** + * 退出登录 + */ + SIGN_OFF(3, "退出登录事件"), + /** + * OTA升级 + */ + OTA_UPGRADE(4, "OTA升级事件"), + + /** + * 设备远程锁定 + */ + LOCK(5, "设备远程锁定"), + + /** + * 设备远程解锁 + */ + UNLOCK(6, "设备远程解锁"), + + /** + * APP版本信息上报 + */ + APP_VERSION_REPORT(7, "APP版本信息上报"), + + /** + * 电池电量上报 + */ + BATTERY_PERCENT_REPORT(8, "电池电量上报"), + + /** + * 温度上报 + */ + TEMPERATURE_REPORT(9, "温度上报"); + + /** + * 类型值 + */ + private final Integer value; + + /** + * 类型名称 + */ + private final String title; + + + /** + * 解析指定的valueName + * + * @param valueName + * @return DeviceEventTypeEnum + * @throws IllegalArgumentException + */ + public static DeviceEventTypeEnum parse(String valueName) { + return Arrays.stream(DeviceEventTypeEnum.values()) + .filter(d -> d.name().equalsIgnoreCase(valueName)).findFirst() + .orElseThrow(() -> new IllegalArgumentException( + StrUtil.format("无效的DeviceEventTypeEnum值:{}", valueName))); + } + + /** + * 根据 value值 获取对应枚举 + * + * @param value + * @return DeviceCommandEnum + */ + public static DeviceEventTypeEnum getEnum(Integer value) { + for (DeviceEventTypeEnum commandEnum : DeviceEventTypeEnum.values()) { + if (commandEnum.getValue().equals(value)) { + return commandEnum; + } + } + return null; + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DevicePropertyEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DevicePropertyEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..74d000f1b5bc361b6c3084d687021d96a0256ec8 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DevicePropertyEnum.java @@ -0,0 +1,40 @@ +package net.maku.iot.enums; + + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 设备属性枚举 + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DevicePropertyEnum { + /** + * 运行状态 + */ + RUNNING_STATUS(1), + + /** + * APP版本 + */ + APP_VERSION(2), + + /** + * 电池电量百分比 + */ + BATTERY_PERCENT(3), + + /** + * 温度 + */ + TEMPERATURE(4); + + /** + * 类型值 + */ + private final Integer value; + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceRunningStatusEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceRunningStatusEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..c3c2a68bb9afbb3caab7cb934013cee75977ebb9 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceRunningStatusEnum.java @@ -0,0 +1,58 @@ +package net.maku.iot.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 设备运行状态枚举 + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DeviceRunningStatusEnum { + /** + * 离线状态 + */ + OFFLINE(0, "离线", DeviceEventTypeEnum.OFFLINE), + + /** + * 在线状态 + */ + ONLINE(1, "在线", DeviceEventTypeEnum.ONLINE); + + /** + * 状态值 + */ + private final Integer value; + + /** + * 状态显示名称 + */ + private final String title; + + /** + * 设备事件类型 + */ + private final DeviceEventTypeEnum eventType; + + + /** + * 解析指定的valueName + * + * @param valueName + * @return DeviceRunningStatusEnum + * @throws IllegalArgumentException + */ + public static DeviceRunningStatusEnum parse(String valueName) { + return Arrays.stream(DeviceRunningStatusEnum.values()) + .filter(d -> d.getValue().toString().equals(valueName) + || d.name().equalsIgnoreCase(valueName)).findFirst() + .orElseThrow(() -> new IllegalArgumentException( + StrUtil.format("无效的DeviceRunningStatusEnum值:{}", valueName))); + } + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceServiceEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceServiceEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..46c91337f0ffb9028bd4700830e316bd3ab6566f --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceServiceEnum.java @@ -0,0 +1,18 @@ +package net.maku.iot.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DeviceServiceEnum { + + COMMAND_ID("命令ID"); + + /** + * 类型值 + */ + private final String value; + + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTopicEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTopicEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..1c1fbcfe2fcca830acd1c28d5b412bb0f85e78cc --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTopicEnum.java @@ -0,0 +1,141 @@ +package net.maku.iot.enums; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.maku.iot.dto.DeviceClientDTO; + +import java.util.Arrays; +import java.util.List; + + +/** + * 设备消息主题类型枚举 + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DeviceTopicEnum { + /** + * 设备指令下发主题 + */ + COMMAND("command"), + + /** + * 设备信息上报主题(指令响应反馈) + */ + COMMAND_RESPONSE("command_response"), + + /** + * 设备信息上报主题(属性相关) + */ + PROPERTY("property"); + + /** + * 设备信息上报主题前缀 + */ + public static final String TOPIC_PREFIX = "/maku/device"; + + + /** + * MQTT 主题模板,参数:前缀/clientid(deviceType_tenantId_deviceId_uid)/设备主题 + */ + private static final String TOPIC_TEMPLATE = "{}/{}/{}"; + + /** + * 设备主题通配符模板,参数:前缀/+/设备主题 + */ + private static final String TOPIC_WILDCARD_TEMPLATE = "{}/+/{}"; + + /** + * MQTT 主题 + */ + private final String topic; + + /** + * 获取设备主题通配符 + * + * @return 主题通配符 + */ + public String getWildcard() { + return StrUtil.format(TOPIC_WILDCARD_TEMPLATE, TOPIC_PREFIX, getTopic()); + } + + /** + * 构建完整路径的设备主题 + * + * @param deviceClient 设备客户端信息 + * @return 完整路径的设备主题 + */ + public String buildTopic(DeviceClientDTO deviceClient) { + return StrUtil.format(TOPIC_TEMPLATE, TOPIC_PREFIX, deviceClient.buildClientId(), getTopic()); + } + + /** + * 解析指定的主题字符串为 DeviceTopicEnum 枚举,若无效则抛出 IllegalArgumentException 异常 + * + * @param topic 主题字符串 + * @return 对应的 DeviceTopicEnum 枚举 + * @throws IllegalArgumentException 当主题字符串无效时抛出 + */ + public static DeviceTopicEnum parse(String topic) { + String topicSuffix = StrUtil.startWith(topic, TOPIC_PREFIX) ? StrUtil.subAfter(topic, "/", true) : topic; + return Arrays.stream(DeviceTopicEnum.values()) + .filter(d -> d.name().equalsIgnoreCase(topicSuffix)).findFirst() + .orElseThrow(() -> new IllegalArgumentException(StrUtil.format("无效的 DeviceTopicEnum 值:{}", topic))); + } + + /** + * 判断指定的主题是否以给定前缀开头 + * + * @param topic 主题字符串 + * @param prefix 前缀字符串 + * @return 若主题以给定前缀开头则返回 true,否则返回 false + */ + public static boolean startsWith(String topic, String prefix) { + try { + DeviceTopicEnum deviceTopic = parse(topic); + return deviceTopic.getTopic().startsWith(prefix); + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * 解析主题并获取其完整信息(设备信息及主题) + * + * @param topic 主题字符串 + * @return 包含设备信息及主题的 DeviceTopicContext 对象 + */ + public static DeviceTopicContext parseContext(String topic) { + String topicSuffix = StrUtil.subAfter(topic, TOPIC_PREFIX, false); + List parts = StrUtil.split(topicSuffix, "/", true, true); + Assert.isTrue(parts.size() == 2, "无效的设备主题:{}", topic); + + DeviceClientDTO deviceClient = DeviceClientDTO.parse(parts.get(0)); + DeviceTopicEnum deviceTopic = DeviceTopicEnum.parse(parts.get(1)); + + return DeviceTopicContext.builder() + .client(deviceClient) + .topic(deviceTopic) + .build(); + } + + @Data + @Builder + public static class DeviceTopicContext { + /** + * 设备客户端信息 + */ + private DeviceClientDTO client; + + /** + * 设备主题 + */ + private DeviceTopicEnum topic; + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTypeEnum.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..17cfef6f88818d01cfe27f100248c0c21804fb07 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/enums/DeviceTypeEnum.java @@ -0,0 +1,52 @@ +package net.maku.iot.enums; + +import cn.hutool.core.util.StrUtil; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 设备类型枚举,新增的设备类型枚举命名不允许使用"_" + * + * @author LSF maku_lsf@163.com + */ +@Getter +@RequiredArgsConstructor +public enum DeviceTypeEnum { + /** + * 手持设备 + */ + HANDSET(1), + + /** + * 柜体 + */ + CABINET(2), + + /** + * 传感设备 + */ + SENSOR(3); + + /** + * 类型值 + */ + private final Integer value; + + /** + * 解析指定的valueName + * + * @param valueName + * @return DeviceTypeEnum + * @throws IllegalArgumentException + */ + public static DeviceTypeEnum parse(String valueName) { + return Arrays.stream(DeviceTypeEnum.values()) + .filter(d -> d.getValue().toString().equals(valueName) + || d.name().equalsIgnoreCase(valueName)).findFirst() + .orElseThrow(() -> new IllegalArgumentException( + StrUtil.format("无效的DeviceTypeEnum值:{}", valueName))); + } + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/MqttGateway.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/MqttGateway.java new file mode 100644 index 0000000000000000000000000000000000000000..a3114a1c4a62ae86938e06bdcc0192353200bfd8 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/MqttGateway.java @@ -0,0 +1,42 @@ +package net.maku.iot.mqtt; + +import net.maku.iot.mqtt.config.MqttConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.integration.annotation.MessagingGateway; +import org.springframework.integration.mqtt.support.MqttHeaders; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.stereotype.Component; + +/** + * MQTT网关 + * + * @author LSF maku_lsf@163.com + */ +@Component +@MessagingGateway(defaultRequestChannel = MqttConfig.OUTBOUND_CHANNEL) +public class MqttGateway { + + @Autowired + private MqttConfig mqttConfig; + + + public void sendToMqtt(String payload) { + mqttConfig.mqttOutboundHandler().handleMessage(MessageBuilder.withPayload(payload).build()); + } + + + public void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload) { + mqttConfig.mqttOutboundHandler().handleMessage(MessageBuilder.withPayload(payload).setHeader(MqttHeaders.TOPIC, topic).build()); + } + + + public void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload) { + mqttConfig.mqttOutboundHandler().handleMessage(MessageBuilder.withPayload(payload).setHeader(MqttHeaders.TOPIC, topic).setHeader(MqttHeaders.QOS, qos).build()); + } + + + public void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.RETAINED) boolean retained, String payload) { + mqttConfig.mqttOutboundHandler().handleMessage(MessageBuilder.withPayload(payload).setHeader(MqttHeaders.TOPIC, topic).setHeader(MqttHeaders.RETAINED, retained).build()); + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/config/MqttConfig.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/config/MqttConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ff261b647956d89dcab14c9d9be9a15969886eb --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/config/MqttConfig.java @@ -0,0 +1,164 @@ +package net.maku.iot.mqtt.config; + +import jakarta.annotation.PostConstruct; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.maku.iot.enums.DeviceTopicEnum; +import net.maku.iot.mqtt.factory.MqttMessageHandlerFactory; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.IntegrationComponentScan; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; +import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageHandler; +import org.springframework.stereotype.Component; + +/** + * MQTT 配置类,用于设置和管理 MQTT 连接和消息处理。 + * + * @author LSF maku_lsf@163.com + */ +@Component +@ConfigurationProperties(prefix = "spring.mqtt") +@Slf4j +@Configuration +@IntegrationComponentScan +@RequiredArgsConstructor +@Data +public class MqttConfig { + + public static final String OUTBOUND_CHANNEL = "mqttOutboundChannel"; + public static final String INPUT_CHANNEL = "mqttInputChannel"; + + // MQTT 用户名 + private String username; + + // MQTT 密码 + private String password; + + // MQTT 服务器 URL + private String hostUrl; + + // 客户端 ID + private String clientId; + + // 默认主题 + private String defaultTopic; + + // 处理 MQTT 消息的工厂 + private final MqttMessageHandlerFactory mqttMessageHandlerFactory; + + @PostConstruct + public void init() { + log.info("MQTT 主机: {} 客户端ID: {} 默认主题:{}", this.hostUrl, this.clientId, this.defaultTopic); + } + + /** + * 配置并返回一个 MqttPahoClientFactory 实例,用于创建 MQTT 客户端连接。 + * + * @return MqttPahoClientFactory + */ + @Bean + public MqttPahoClientFactory mqttClientFactory() { + // 设置连接选项,包括服务器 URI、用户名和密码。 + final MqttConnectOptions options = new MqttConnectOptions(); + options.setServerURIs(new String[]{hostUrl}); + options.setUserName(username); + options.setPassword(password.toCharArray()); + final DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); + factory.setConnectionOptions(options); + return factory; + } + + /** + * 创建一个用于发送 MQTT 消息的 MessageChannel。 + * + * @return MessageChannel + */ + @Bean(OUTBOUND_CHANNEL) + public MessageChannel mqttOutboundChannel() { + return new DirectChannel(); + } + + /** + * 配置用于发送 MQTT 消息的 MessageHandler。 + * + * @return MessageHandler + */ + @Bean + @ServiceActivator(inputChannel = OUTBOUND_CHANNEL) + public MessageHandler mqttOutboundHandler() { + // 使用 MqttPahoMessageHandler 创建一个新的 MQTT 客户端连接,用于发布消息。 + final MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId + "_pub", mqttClientFactory()); + handler.setDefaultQos(1); + handler.setDefaultRetained(false); + handler.setDefaultTopic(defaultTopic); + handler.setAsync(true); + return handler; + } + + /** + * 创建用于接收 MQTT 消息的 MessageChannel。 + * + * @return MessageChannel + */ + @Bean + public MessageChannel mqttInputChannel() { + return new DirectChannel(); + } + + /** + * 配置 客户端,订阅的主题, + * PROPERTY:设备属性上报主题, + * COMMAND_RESPONSE:下发指令执行结果主题 + * + * @return MqttPahoMessageDrivenChannelAdapter + */ + @Bean + @ConditionalOnProperty(name = "spring.mqtt.mqttEnabled", havingValue = "true") + public MqttPahoMessageDrivenChannelAdapter mqttInboundAdapter() { + final MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter( + clientId + "_sub", + mqttClientFactory(), DeviceTopicEnum.PROPERTY.getWildcard(), + DeviceTopicEnum.COMMAND_RESPONSE.getWildcard() + ); + adapter.setCompletionTimeout(15000); + adapter.setConverter(new DefaultPahoMessageConverter()); + adapter.setQos(1); + adapter.setOutputChannel(mqttInputChannel()); + return adapter; + } + + /** + * 通过通道获取数据并处理消息。 + * + * @return MessageHandler + */ + @Bean + @ServiceActivator(inputChannel = INPUT_CHANNEL) + public MessageHandler mqttMessageHandler() { + return message -> { + String topic = (String) message.getHeaders().get("mqtt_receivedTopic"); + if (topic != null) { + mqttMessageHandlerFactory.getHandlersForTopic(topic).forEach(handler -> { + if (log.isDebugEnabled()) { + log.debug("主题: {}, 消息内容: {}", topic, message.getPayload()); + } + handler.handle(topic, message.getPayload().toString()); + }); + } else { + log.warn("接收到主题为null的消息。"); + } + }; + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandDTO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..e304a294e9b88ec0dd0fb8b64009a6ba7bc4ae6f --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandDTO.java @@ -0,0 +1,32 @@ +package net.maku.iot.mqtt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.iot.enums.DeviceCommandEnum; + +/** + * 设备命令对象 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备命令对象") +public class DeviceCommandDTO { + /** + * 命令类型 + */ + @Schema(description = "命令类型", required = true) + private DeviceCommandEnum command; + + /** + * 命令id + */ + @Schema(description = "命令id", required = true) + private String id; + + /** + * 命令内容 + */ + @Schema(description = "命令内容") + private String payload; +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandResponseDTO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..48e0f5261b3e471390de44412c20b5953fdc5e4c --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DeviceCommandResponseDTO.java @@ -0,0 +1,45 @@ +package net.maku.iot.mqtt.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.iot.enums.DeviceCommandEnum; + +/** + * 设备命令响应DTO + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备命令响应DTO") +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceCommandResponseDTO { + /** + * 命令类型 + */ + @Schema(description = "命令类型", required = true) + private DeviceCommandEnum command; + + /** + * 命令ID + */ + @Schema(description = "命令ID", required = true) + private String commandId; + + /** + * 命令是否完成(默认true:命令已完成;false:命令未完成,后续命令完成将再次发送响应消息,服务端将继续等待该命令完成的响应) + */ + @Schema(description = "命令是否完成(默认true:命令已完成;false:命令未完成,后续命令完成将再次发送响应消息,服务端将继续等待该命令完成的响应)") + private boolean isCompleted = true; + + /** + * 响应状态码,0成功,其它数值异常,根据业务需要自定义 + */ + @Schema(description = "响应状态码,0成功,其它数值异常,根据业务需要自定义") + private Integer statusCode = 0; + /** + * 命令响应结果 + */ + @Schema(description = "命令响应结果") + private String responsePayload; +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DevicePropertyDTO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DevicePropertyDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..d75b32904441da81c03e12fbfdca11dd2d41f800 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/dto/DevicePropertyDTO.java @@ -0,0 +1,26 @@ +package net.maku.iot.mqtt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.iot.enums.DevicePropertyEnum; + +/** + * 设备属性对象 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备属性对象") +public class DevicePropertyDTO { + /** + * 设备属性类型 + */ + @Schema(description = "设备属性类型") + private DevicePropertyEnum propertyType; + + /** + * 属性数据 + */ + @Schema(description = "状态数据,不同状态类型需传入相应的状态数据", required = true) + private String payload; +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DeviceCommandResponseHandlerFactory.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DeviceCommandResponseHandlerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..742584d9bddb64345d03caa2f448d84d8946c8dd --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DeviceCommandResponseHandlerFactory.java @@ -0,0 +1,41 @@ +package net.maku.iot.mqtt.factory; + +import lombok.RequiredArgsConstructor; +import net.maku.iot.mqtt.handler.DeviceCommandResponseHandler; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 设备命令响应处理器工厂,自动获取所有实现的handler实例 + * + * @author LSF maku_lsf@163.com + */ +@Component +@RequiredArgsConstructor +public class DeviceCommandResponseHandlerFactory { + private final ApplicationContext applicationContext; + + /** + * 所有设备命令响应handlers + */ + private List handlers; + + /** + * 获取设备命令响应handlers + * + * @return + */ + public List getHandlers() { + if (handlers != null) { + return handlers; + } + handlers = Collections.unmodifiableList( + new ArrayList<>(applicationContext.getBeansOfType( + DeviceCommandResponseHandler.class).values())); + return handlers; + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DevicePropertyChangeHandlerFactory.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DevicePropertyChangeHandlerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e0d6bb427e1bd149cd78bab73f8540a6bdcbdef5 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/DevicePropertyChangeHandlerFactory.java @@ -0,0 +1,41 @@ +package net.maku.iot.mqtt.factory; + +import lombok.RequiredArgsConstructor; +import net.maku.iot.mqtt.handler.DevicePropertyChangeHandler; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 设备运行状态变化处理器工厂,自动获取所有实现的handler实例 + * + * @author LSF maku_lsf@163.com + */ +@Component +@RequiredArgsConstructor +public class DevicePropertyChangeHandlerFactory { + private final ApplicationContext applicationContext; + + /** + * 所有设备运行属性变化handlers + */ + private List handlers; + + /** + * 获取设备运行状态变化handlers + * + * @return + */ + public List getHandlers() { + if (handlers != null) { + return handlers; + } + handlers = Collections.unmodifiableList( + new ArrayList<>(applicationContext.getBeansOfType( + DevicePropertyChangeHandler.class).values())); + return handlers; + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/MqttMessageHandlerFactory.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/MqttMessageHandlerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..368cf546cc33001cdef7aab5943dcee9a11546d6 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/factory/MqttMessageHandlerFactory.java @@ -0,0 +1,47 @@ +package net.maku.iot.mqtt.factory; + +import lombok.RequiredArgsConstructor; +import net.maku.iot.mqtt.handler.MqttMessageHandler; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * MQTT消息处理器工厂,自动获取所有实现的处理器实例 + * + * @author LSF maku_lsf@163.com + */ +@Component +@RequiredArgsConstructor +public class MqttMessageHandlerFactory { + private final ApplicationContext applicationContext; + + /** + * 所有消息处理器 + */ + private List messageHandlers; + + private List loadHandlers() { + if (messageHandlers != null) { + return messageHandlers; + } + messageHandlers = new ArrayList<>(applicationContext.getBeansOfType(MqttMessageHandler.class).values()); + return messageHandlers; + } + + /** + * 获取与主题对应的处理器 + * + * @param topic 主题 + * @return 处理器列表 + */ + public List getHandlersForTopic(String topic) { + return Collections.unmodifiableList(loadHandlers().stream() + .filter(handler -> handler.supports(topic)) + .collect(Collectors.toList())); + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseHandler.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..2c04a66da5c544ba6983adb932ae87d85834686b --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseHandler.java @@ -0,0 +1,19 @@ +package net.maku.iot.mqtt.handler; + + +import net.maku.iot.mqtt.dto.DeviceCommandResponseDTO; + +/** + * 设备命令响应处理器 + * + * @author LSF maku_lsf@163.com + */ +public interface DeviceCommandResponseHandler { + /** + * 设备命令响应处理 + * + * @param topic + * @param commandResponse + */ + void handle(String topic, DeviceCommandResponseDTO commandResponse); +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseMqttMessageHandler.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseMqttMessageHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..775f84a03e3d9a4b8a8a236b77aeab807c6413bb --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DeviceCommandResponseMqttMessageHandler.java @@ -0,0 +1,73 @@ +package net.maku.iot.mqtt.handler; + +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.maku.framework.common.utils.JsonUtils; +import net.maku.iot.enums.DeviceTopicEnum; +import net.maku.iot.mqtt.dto.DeviceCommandResponseDTO; +import net.maku.iot.mqtt.factory.DeviceCommandResponseHandlerFactory; +import net.maku.iot.mqtt.service.DeviceMqttService; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * 设备命令响应处理器 + * + * @author LSF maku_lsf@163.com + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class DeviceCommandResponseMqttMessageHandler implements MqttMessageHandler { + private final DeviceCommandResponseHandlerFactory deviceCommandResponseHandlerFactory; + + + private final DeviceMqttService deviceMqttService; + + @Override + public boolean supports(String topic) { + return DeviceTopicEnum.startsWith(topic, DeviceTopicEnum.COMMAND_RESPONSE.getTopic()); + } + + @Override + public void handle(String topic, String message) { + DeviceCommandResponseDTO commanddResponseDTO = parseCommandReplyMessage(topic, message); + Optional.ofNullable(commanddResponseDTO.getCommand()) + .orElseThrow(() -> new IllegalArgumentException(StrUtil.format("缺失指令类型! 主题:'{}',消息:{}", topic, message))); + Optional.ofNullable(commanddResponseDTO.getCommandId()) + .orElseThrow(() -> new IllegalArgumentException(StrUtil.format("缺失指令ID! 主题:'{}',消息:{}", topic, message))); + + Optional.ofNullable(commanddResponseDTO) + .ifPresent(responseDTO -> { + // 调用设备命令执行器的命令响应处理逻辑 + try { + deviceMqttService.commandReplied(topic, responseDTO); + } catch (Exception e) { + log.error(StrUtil.format("调用设备命令执行器响应处理方法出错,topic:{}, message:{}", topic, message), e); + } + // 调用自定义命令响应处理器 + try { + deviceCommandResponseHandlerFactory.getHandlers().forEach(h -> h.handle(topic, responseDTO)); + } catch (Exception e) { + log.error(StrUtil.format("调用设备命令响应响应处理器出错,topic:{}, message:{}", topic, message), e); + } + }); + } + + private DeviceCommandResponseDTO parseCommandReplyMessage(String topic, String message) { + try { + DeviceCommandResponseDTO commandResponse = JsonUtils.parseObject(message, DeviceCommandResponseDTO.class); + if (StrUtil.isBlank(commandResponse.getCommandId())) { + log.error(StrUtil.format("主题'{}'的消息,缺失指令ID", topic)); + return null; + } + return commandResponse; + + } catch (Exception e) { + log.error(StrUtil.format("将主题'{}'的消息解析为设备命令响应对象失败", topic), e); + return null; + } + } +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyChangeHandler.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyChangeHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..739f266b447e05154a41d4db26eddb5c13c66375 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyChangeHandler.java @@ -0,0 +1,19 @@ +package net.maku.iot.mqtt.handler; + + +import net.maku.iot.mqtt.dto.DevicePropertyDTO; + +/** + * 设备属性变化处理器 + * + * @author LSF maku_lsf@163.com + */ +public interface DevicePropertyChangeHandler { + /** + * 设备属性状态变化处理 + * + * @param topic + * @param deviceStatus + */ + void handle(String topic, DevicePropertyDTO deviceStatus); +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyMqttMessageHandler.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyMqttMessageHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..f47e205aea71f856b7777f4410df79f03b633e69 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/DevicePropertyMqttMessageHandler.java @@ -0,0 +1,49 @@ +package net.maku.iot.mqtt.handler; + +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.maku.framework.common.utils.JsonUtils; +import net.maku.iot.enums.DeviceTopicEnum; +import net.maku.iot.mqtt.dto.DevicePropertyDTO; +import net.maku.iot.mqtt.factory.DevicePropertyChangeHandlerFactory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +/** + * 设备属性上报消息处理器 + * + * @author LSF maku_lsf@163.com + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class DevicePropertyMqttMessageHandler implements MqttMessageHandler { + + private final DevicePropertyChangeHandlerFactory statusChangeHandlerFactory; + + @Override + public boolean supports(String topic) { + return DeviceTopicEnum.startsWith(topic, DeviceTopicEnum.PROPERTY.getTopic()); + } + + @Override + public void handle(String topic, String message) { + DevicePropertyDTO devicePropertyDTO = parseStatusMessage(topic, message); + Optional.ofNullable(devicePropertyDTO) + .ifPresent(deviceProperty -> statusChangeHandlerFactory.getHandlers() + .forEach(h -> h.handle(topic, deviceProperty))); + } + + private DevicePropertyDTO parseStatusMessage(String topic, String message) { + try { + return JsonUtils.parseObject(message, DevicePropertyDTO.class); + } catch (Exception e) { + log.error(StrUtil.format("将主题'{}'的消息解析为设备运行状态对象失败", topic), e); + return null; + } + } + + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/MqttMessageHandler.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/MqttMessageHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..33307ea332eba25cfe0160619bd14b22d3d83a86 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/handler/MqttMessageHandler.java @@ -0,0 +1,24 @@ +package net.maku.iot.mqtt.handler; + +/** + * MQTT订阅消息处理接口 + * + * @author LSF maku_lsf@163.com + */ +public interface MqttMessageHandler { + /** + * 是否支持处理指定的topic + * + * @param topic + * @return + */ + boolean supports(String topic); + + /** + * mqtt消息处理接口 + * + * @param topic + * @param message + */ + void handle(String topic, String message); +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/service/DeviceMqttService.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/service/DeviceMqttService.java new file mode 100644 index 0000000000000000000000000000000000000000..e6f1973cc05ecf64f7a8fd6cd0f902d50bf4e9ef --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/mqtt/service/DeviceMqttService.java @@ -0,0 +1,227 @@ +package net.maku.iot.mqtt.service; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.maku.framework.common.exception.ServerException; +import net.maku.iot.dto.DeviceClientDTO; +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.enums.DeviceCommandEnum; +import net.maku.iot.enums.DeviceServiceEnum; +import net.maku.iot.enums.DeviceTopicEnum; +import net.maku.iot.mqtt.MqttGateway; +import net.maku.iot.mqtt.dto.DeviceCommandDTO; +import net.maku.iot.mqtt.dto.DeviceCommandResponseDTO; +import net.maku.iot.service.IotDeviceServiceLogService; +import net.maku.iot.utils.MqttUtils; +import org.springframework.stereotype.Component; + +import java.util.UUID; +import java.util.concurrent.*; + +/** + * 设备命令发送服务 + **/ +@Slf4j +@Component +@RequiredArgsConstructor +public class DeviceMqttService { + private final MqttUtils mqttUtils; + private final MqttGateway mqttGateway; + private final IotDeviceServiceLogService iotDeviceEventLogService; + + /** + * 命令等待exchanger缓存,key: command id + */ + private ConcurrentMap> commandExchangers = new ConcurrentHashMap<>(); + + /** + * 异步发送命令,返回命令id + * + * @param device + * @param command + * @param payload + * @return + */ + public String asyncSendCommand(IotDeviceEntity device, DeviceCommandEnum command, String payload) { + return asyncSendCommand(device, command, payload, Boolean.FALSE); + } + + + /** + * 异步发送命令,返回命令id + * + * @param device + * @param command + * @param payload + * @param retained + * @return + */ + public String asyncSendCommand(IotDeviceEntity device, DeviceCommandEnum command, String payload, boolean retained) { + // 构建命令对象 + String commandId = StrUtil.replaceChars(UUID.randomUUID().toString(), "-", ""); + DeviceCommandDTO commandDTO = new DeviceCommandDTO(); + commandDTO.setCommand(command); + commandDTO.setId(commandId); + commandDTO.setPayload(payload); + String commandTopic = DeviceTopicEnum.COMMAND.buildTopic(DeviceClientDTO.from(device)); + + // 发送命令到设备命令主题 + try { + mqttGateway.sendToMqtt(commandTopic, retained, JSONUtil.toJsonStr(commandDTO)); + } catch (Exception e) { + log.error(e.getMessage()); + throw new ServerException(StrUtil.format("发送'{}'命令:{} 到设备:{}-{}, Topic:{} 失败", + command.getTitle(), commandId, device.getCode(), device.getName(), commandTopic)); + } + log.info("发送'{}'命令:{} 到设备:{}-{}, Topic:{} 成功", command.getTitle(), commandId, device.getCode(), device.getName(), commandTopic); + iotDeviceEventLogService.createAndSaveDeviceServiceLog(device.getId(), device.getTenantId(), command, commandId, payload); + return commandId; + } + + + /** + * 同步发送命令并返回响应结果 + * + * @param device + * @param command + * @param payload + * @return + */ + public DeviceCommandResponseDTO syncSendCommand(IotDeviceEntity device, DeviceCommandEnum command, String payload) { + return syncSendCommand(device, command, payload, Boolean.FALSE); + } + + /** + * 发送命令并返回响应结果 + * + * @param device + * @param command + * @param payload + * @param retained + * @return + */ + public DeviceCommandResponseDTO syncSendCommand(IotDeviceEntity device, DeviceCommandEnum command, String payload, boolean retained) { + // 构建并发送命令 + String commandId = asyncSendCommand(device, command, payload, retained); + // 等待返回结果 + return waitCommandResponse(command, commandId); + } + + + /** + * 发送命令并返回响应结果,模拟设备响应 + * + * @param device + * @param command + * @param payload + * @return + */ + public DeviceCommandResponseDTO syncSendCommandDebug(IotDeviceEntity device, DeviceCommandEnum command, String payload) { + // 构建并发送命令 + String commandId = asyncSendCommand(device, command, payload); + + // 2秒后模拟设备响应 + new Thread(() -> { + try { + //模拟设备正常响应 + Thread.sleep(2000); + //模拟设备超时响应 + //Thread.sleep(15000); + DeviceCommandResponseDTO simulateResponseDto = new DeviceCommandResponseDTO(); + simulateResponseDto.setCommandId(commandId); + simulateResponseDto.setResponsePayload(command.getTitle() + ",设备执行成功!"); + simulateResponseDto.setCommand(command); + simulateDeviceCommandResponseAttributeData(device, JSONUtil.toJsonStr(simulateResponseDto)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("模拟设备响应线程被中断", e); + } + }).start(); + + // 等待设备响应 + return waitCommandResponse(command, commandId); + } + + + /** + * 订阅设备命令响应主题并等待获取返回结果 + * + * @param command + * @param commandId + * @return + */ + private DeviceCommandResponseDTO waitCommandResponse(DeviceCommandEnum command, String commandId) { + // 创建命令响应等待exchanger + Exchanger commandExchanger = new Exchanger<>(); + commandExchangers.put(commandId, commandExchanger); + + try { + Object result = commandExchanger.exchange("", 10, TimeUnit.SECONDS); + return (DeviceCommandResponseDTO) result; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ServerException(StrUtil.format("{} <{}>,{} 命令中断", + DeviceServiceEnum.COMMAND_ID.getValue(), commandId, command.getTitle()), e); + } catch (TimeoutException e) { + throw new ServerException(StrUtil.format("{} <{}>,{} 命令超时", + DeviceServiceEnum.COMMAND_ID.getValue(), commandId, command.getTitle()), e); + } finally { + // 移除命令响应等待exchanger + commandExchangers.remove(commandId); + } + } + + /** + * 设备命令响应处理 + * + * @param topic + * @param commandResponse + */ + public void commandReplied(String topic, DeviceCommandResponseDTO commandResponse) { + Exchanger exchanger = commandExchangers.remove(commandResponse.getCommandId()); + if (exchanger != null) { + if (commandResponse.isCompleted()) { + try { + // 将响应交换到等待线程 + exchanger.exchange(commandResponse, 15, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("将主题'{}'的命令响应交换到等待线程失败,中断异常: {}", topic, e.getMessage()); + } catch (TimeoutException e) { + log.error("将主题'{}'的命令响应交换到等待线程失败,超时异常: {}", topic, e.getMessage()); + } + } else { + log.warn("命令ID为'{}'的响应未完成,未通知等待线程", commandResponse.getCommandId()); + } + } else { + log.warn("找不到命令ID为'{}'的响应交换器", commandResponse.getCommandId()); + } + } + + public void simulateDeviceReportAttributeData(IotDeviceEntity device, String payload) { + // 封装 设备属性上报的 topic + String commandTopic = DeviceTopicEnum.PROPERTY.buildTopic(DeviceClientDTO.from(device)); + try { + mqttGateway.sendToMqtt(commandTopic, payload); + } catch (Exception e) { + log.error(e.getMessage()); + throw new ServerException(StrUtil.format("模拟设备:{}-{},模拟属性上报失败! Topic:{} ", + device.getCode(), device.getName(), commandTopic)); + } + } + + public void simulateDeviceCommandResponseAttributeData(IotDeviceEntity device, String payload) { + // 封装 设备命令执行结果的 topic + String commandTopic = DeviceTopicEnum.COMMAND_RESPONSE.buildTopic(DeviceClientDTO.from(device)); + try { + mqttGateway.sendToMqtt(commandTopic, payload); + } catch (Exception e) { + log.error(e.getMessage()); + throw new ServerException(StrUtil.format("模拟设备:{}-{},模拟发送命令执行结果失败! Topic:{} ", + device.getCode(), device.getName(), commandTopic)); + } + } + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceEventLogQuery.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceEventLogQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..e309cae26f2a3949786e83ff6de9b97753434916 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceEventLogQuery.java @@ -0,0 +1,23 @@ +package net.maku.iot.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.common.query.Query; + +/** + * 设备事件日志查询 + * + * @author LSF maku_lsf@163.com + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description = "设备事件日志查询") +public class IotDeviceEventLogQuery extends Query { + + @Schema(description = "指令") + private String eventTypeEnum; + + @Schema(description = "设备id") + private Long deviceId; +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceQuery.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..05ce2bdb80ffbd90d25a0deb28655221128e69f0 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceQuery.java @@ -0,0 +1,26 @@ +package net.maku.iot.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.common.query.Query; + +/** + * 设备表查询 + * + * @author LSF maku_lsf@163.com + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description = "设备表查询") +public class IotDeviceQuery extends Query { + + @Schema(description = "状态,0禁用,1正常") + private Integer status; + + @Schema(description = "运行状态,0.离线状态 1.在线状态 2.正常待机 3.用户使用中 4.OTA升级中") + private Integer runningStatus; + + @Schema(description = "设备类型,1.手持设备,2.柜体,3传感设备") + private Integer type; +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceServiceLogQuery.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceServiceLogQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..982596d10ff1b92a887d540cebe764bd5aed3688 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/query/IotDeviceServiceLogQuery.java @@ -0,0 +1,22 @@ +package net.maku.iot.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import net.maku.framework.common.query.Query; + +/** + * 设备服务日志查询 + * + * @author LSF maku_lsf@163.com + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description = "设备服务日志查询") +public class IotDeviceServiceLogQuery extends Query { + @Schema(description = "指令") + private String deviceCommandEnum; + + @Schema(description = "设备id") + private Long deviceId; +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceEventLogService.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceEventLogService.java new file mode 100644 index 0000000000000000000000000000000000000000..0cf1b348fc8eacd31ff0b02c80fbb8497c279f69 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceEventLogService.java @@ -0,0 +1,52 @@ +package net.maku.iot.service; + +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.BaseService; +import net.maku.iot.entity.IotDeviceEventLogEntity; +import net.maku.iot.enums.DeviceEventTypeEnum; +import net.maku.iot.query.IotDeviceEventLogQuery; +import net.maku.iot.vo.IotDeviceEventLogVO; + +import java.util.List; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +public interface IotDeviceEventLogService extends BaseService { + + PageResult page(IotDeviceEventLogQuery query); + + void save(IotDeviceEventLogVO vo); + + void update(IotDeviceEventLogVO vo); + + void delete(List idList); + + /** + * 创建设备事件 + * + * @param deviceId 设备ID + * @param tenantId 租户ID + * @param eventType 事件类型 + * @param eventUid 事件UID + * @param payload 事件数据 + * @return 设备事件 + */ + IotDeviceEventLogEntity createDeviceEvent(Long deviceId, Long tenantId, DeviceEventTypeEnum eventType, + String eventUid, Object payload); + + /** + * 创建设备事件并保存 + * + * @param deviceId 设备ID + * @param tenantId 租户ID + * @param eventType 事件类型 + * @param eventUid 事件UID + * @param payload 事件数据 + * @return 设备事件 + */ + void createAndSaveDeviceEvent(Long deviceId, Long tenantId, DeviceEventTypeEnum eventType, + String eventUid, Object payload); +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceService.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceService.java new file mode 100644 index 0000000000000000000000000000000000000000..eafbde48598b38c28d0072af3a1cb490586f1ecd --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceService.java @@ -0,0 +1,60 @@ +package net.maku.iot.service; + +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.BaseService; +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.mqtt.dto.DeviceCommandResponseDTO; +import net.maku.iot.query.IotDeviceQuery; +import net.maku.iot.vo.DeviceCommandResponseAttributeDataVO; +import net.maku.iot.vo.DeviceCommandVO; +import net.maku.iot.vo.DeviceReportAttributeDataVO; +import net.maku.iot.vo.IotDeviceVO; + +import java.util.List; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +public interface IotDeviceService extends BaseService { + + PageResult page(IotDeviceQuery query); + + void save(IotDeviceVO vo); + + void update(IotDeviceVO vo); + + void delete(List idList); + + /** + * 对设备下发指令-同步响应模式 + * + * @param vo + */ + DeviceCommandResponseDTO syncSendCommand(DeviceCommandVO vo); + + /** + * 对设备下发指令-同步响应模式-调试 + * + * @param vo + */ + DeviceCommandResponseDTO syncSendCommandDebug(DeviceCommandVO vo); + + /** + * 对设备下发指令-异步响应模式 + * + * @param vo + */ + void asyncSendCommand(DeviceCommandVO vo); + + /** + * 模拟设备属性数据上报 + */ + void simulateDeviceReportAttributeData(DeviceReportAttributeDataVO vo); + + /** + * 模拟设备服务指令响应数据 + */ + void simulateDeviceCommandResponseAttributeData(DeviceCommandResponseAttributeDataVO vo); +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceServiceLogService.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceServiceLogService.java new file mode 100644 index 0000000000000000000000000000000000000000..352e57328becbd74ef89a6d5635148f80055a994 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/IotDeviceServiceLogService.java @@ -0,0 +1,52 @@ +package net.maku.iot.service; + +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.BaseService; +import net.maku.iot.entity.IotDeviceServiceLogEntity; +import net.maku.iot.enums.DeviceCommandEnum; +import net.maku.iot.query.IotDeviceServiceLogQuery; +import net.maku.iot.vo.IotDeviceServiceLogVO; + +import java.util.List; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +public interface IotDeviceServiceLogService extends BaseService { + + PageResult page(IotDeviceServiceLogQuery query); + + void save(IotDeviceServiceLogVO vo); + + void update(IotDeviceServiceLogVO vo); + + void delete(List idList); + + /** + * 创建设备服务日志 + * + * @param deviceId 设备ID + * @param tenantId 租户ID + * @param command 服务类型 + * @param eventUid 事件UID + * @param payload 事件数据 + * @return 设备事件 + */ + IotDeviceServiceLogEntity createDeviceServiceLog(Long deviceId, Long tenantId, DeviceCommandEnum command, + String eventUid, Object payload); + + /** + * 创建设备服务日志并保存 + * + * @param deviceId 设备ID + * @param tenantId 租户ID + * @param command 服务类型 + * @param eventUid 事件UID + * @param payload 事件数据 + * @return 设备事件 + */ + void createAndSaveDeviceServiceLog(Long deviceId, Long tenantId, DeviceCommandEnum command, + String eventUid, Object payload); +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceEventLogServiceImpl.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceEventLogServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..713ef934006018d62d80b752d6b7a1df367e27a5 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceEventLogServiceImpl.java @@ -0,0 +1,95 @@ +package net.maku.iot.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.AllArgsConstructor; +import net.maku.framework.common.utils.JsonUtils; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.impl.BaseServiceImpl; +import net.maku.iot.convert.IotDeviceEventLogConvert; +import net.maku.iot.dao.IotDeviceEventLogDao; +import net.maku.iot.entity.IotDeviceEventLogEntity; +import net.maku.iot.enums.DeviceEventTypeEnum; +import net.maku.iot.query.IotDeviceEventLogQuery; +import net.maku.iot.service.IotDeviceEventLogService; +import net.maku.iot.vo.IotDeviceEventLogVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@Service +@AllArgsConstructor +public class IotDeviceEventLogServiceImpl extends BaseServiceImpl implements IotDeviceEventLogService { + + @Override + public PageResult page(IotDeviceEventLogQuery query) { + IPage page = baseMapper.selectPage(getPage(query), getWrapper(query)); + List vos = IotDeviceEventLogConvert.INSTANCE.convertList(page.getRecords()); + vos.forEach(vo -> { + vo.setEventTypeEnum(DeviceEventTypeEnum.getEnum(vo.getEventType())); + }); + return new PageResult<>(vos, page.getTotal()); + } + + private LambdaQueryWrapper getWrapper(IotDeviceEventLogQuery query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StrUtil.isNotBlank(query.getEventTypeEnum())) { + wrapper.eq(IotDeviceEventLogEntity::getEventType, DeviceEventTypeEnum.parse(query.getEventTypeEnum()).getValue()); + } + wrapper.eq(query.getDeviceId() != null, IotDeviceEventLogEntity::getDeviceId, query.getDeviceId()); + wrapper.orderByDesc(IotDeviceEventLogEntity::getEventTime); + return wrapper; + } + + @Override + public void save(IotDeviceEventLogVO vo) { + IotDeviceEventLogEntity entity = IotDeviceEventLogConvert.INSTANCE.convert(vo); + + baseMapper.insert(entity); + } + + @Override + public void update(IotDeviceEventLogVO vo) { + IotDeviceEventLogEntity entity = IotDeviceEventLogConvert.INSTANCE.convert(vo); + + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(List idList) { + removeByIds(idList); + } + + @Override + public IotDeviceEventLogEntity createDeviceEvent(Long deviceId, Long tenantId, DeviceEventTypeEnum eventType, String eventUid, Object payload) { + IotDeviceEventLogEntity deviceEvent = new IotDeviceEventLogEntity(); + deviceEvent.setDeviceId(deviceId); + deviceEvent.setTenantId(tenantId); + deviceEvent.setEventType(eventType.getValue()); + deviceEvent.setEventUid(eventUid); + if (payload != null) { + deviceEvent.setEventPayload((payload instanceof String) + ? (String) payload + : JsonUtils.toJsonString(payload)); + } + deviceEvent.setEventTime(LocalDateTime.now()); + return deviceEvent; + + } + + @Override + public void createAndSaveDeviceEvent(Long deviceId, Long tenantId, DeviceEventTypeEnum eventType, String eventUid, Object payload) { + save(createDeviceEvent(deviceId, tenantId, eventType, eventUid, payload)); + } + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceImpl.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d7bebfa59c6bca70a45fd519f4bb704963e97c70 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceImpl.java @@ -0,0 +1,297 @@ +package net.maku.iot.service.impl; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.maku.framework.common.exception.ServerException; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.impl.BaseServiceImpl; +import net.maku.iot.convert.IotDeviceConvert; +import net.maku.iot.dao.IotDeviceDao; +import net.maku.iot.entity.IotDeviceEntity; +import net.maku.iot.enums.*; +import net.maku.iot.mqtt.dto.DeviceCommandResponseDTO; +import net.maku.iot.mqtt.dto.DevicePropertyDTO; +import net.maku.iot.mqtt.handler.DeviceCommandResponseHandler; +import net.maku.iot.mqtt.handler.DevicePropertyChangeHandler; +import net.maku.iot.mqtt.service.DeviceMqttService; +import net.maku.iot.query.IotDeviceQuery; +import net.maku.iot.service.IotDeviceEventLogService; +import net.maku.iot.service.IotDeviceService; +import net.maku.iot.vo.DeviceCommandResponseAttributeDataVO; +import net.maku.iot.vo.DeviceCommandVO; +import net.maku.iot.vo.DeviceReportAttributeDataVO; +import net.maku.iot.vo.IotDeviceVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@Service +@Slf4j +@AllArgsConstructor +public class IotDeviceServiceImpl extends BaseServiceImpl + implements IotDeviceService, DevicePropertyChangeHandler, DeviceCommandResponseHandler { + + //todo 后续版本更改为根据物模型自动选择不同的通信层Service + private final DeviceMqttService mqttService; + private final IotDeviceEventLogService deviceEventLogService; + + @Override + public PageResult page(IotDeviceQuery query) { + IPage page = baseMapper.selectPage(getPage(query), getWrapper(query)); + + return new PageResult<>(IotDeviceConvert.INSTANCE.convertList(page.getRecords()), page.getTotal()); + } + + private LambdaQueryWrapper getWrapper(IotDeviceQuery query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + wrapper.eq(query.getStatus() != null, IotDeviceEntity::getStatus, query.getStatus()); + wrapper.eq(query.getType() != null, IotDeviceEntity::getType, query.getType()); + wrapper.eq(query.getRunningStatus() != null, IotDeviceEntity::getRunningStatus, query.getRunningStatus()); + return wrapper; + } + + @Override + public void save(IotDeviceVO vo) { + IotDeviceEntity entity = IotDeviceConvert.INSTANCE.convert(vo); + + baseMapper.insert(entity); + } + + @Override + public void update(IotDeviceVO vo) { + IotDeviceEntity entity = IotDeviceConvert.INSTANCE.convert(vo); + + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(List idList) { + removeByIds(idList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public DeviceCommandResponseDTO syncSendCommand(DeviceCommandVO vo) { + + IotDeviceEntity device = getById(vo.getDeviceId()); + Assert.notNull(device, "未注册的设备:{}", vo.getDeviceId()); + + DeviceCommandEnum commandEnum = DeviceCommandEnum.parse(vo.getCommand()); + + try { + return mqttService.syncSendCommand(getById(vo.getDeviceId()), commandEnum, vo.getPayload()); + } catch (ServerException e) { + if (DeviceCommandEnum.parse(vo.getCommand()).getEventType() != null + && StrUtil.contains(e.getMessage(), DeviceServiceEnum.COMMAND_ID.getValue())) { + //指令异常事件记录 + String commandId = e.getMessage().substring(e.getMessage().indexOf("<") + 1, e.getMessage().indexOf(">")); + deviceEventLogService.createAndSaveDeviceEvent( + vo.getDeviceId(), device.getTenantId(), + commandEnum.getEventType(), commandId, vo.getPayload()); + } + throw e; + } + } + + @Override + public DeviceCommandResponseDTO syncSendCommandDebug(DeviceCommandVO vo) { + IotDeviceEntity device = getById(vo.getDeviceId()); + DeviceCommandEnum commandEnum = DeviceCommandEnum.parse(vo.getCommand()); + return mqttService.syncSendCommandDebug(device, commandEnum, vo.getPayload()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void asyncSendCommand(DeviceCommandVO vo) { + mqttService.asyncSendCommand(getById(vo.getDeviceId()), DeviceCommandEnum.parse(vo.getCommand()), vo.getPayload()); + } + + @Override + public void simulateDeviceReportAttributeData(DeviceReportAttributeDataVO vo) { + mqttService.simulateDeviceReportAttributeData(getById(vo.getDeviceId()), JSONUtil.toJsonStr(vo)); + } + + @Override + public void simulateDeviceCommandResponseAttributeData(DeviceCommandResponseAttributeDataVO vo) { + mqttService.simulateDeviceCommandResponseAttributeData(getById(vo.getDeviceId()), JSONUtil.toJsonStr(vo)); + } + + /** + * 设备状态上报处理 + * + * @param topic + * @param deviceProperty + */ + @Override + public void handle(String topic, DevicePropertyDTO deviceProperty) { + DeviceTopicEnum.DeviceTopicContext topicContext = null; + try { + topicContext = DeviceTopicEnum.parseContext(topic); + } catch (Exception e) { + log.warn("无效设备主题:{},忽略设备状态上报消息:{}", topic, deviceProperty); + return; + } + if (log.isDebugEnabled()) { + log.warn("处理设备状态上报消息,Topic:{}, TopicContext:{}", topic, topicContext); + } + Long deviceId = topicContext.getClient().getDeviceId(); + IotDeviceEntity device = super.getById(deviceId); + if (device == null) { + log.warn("无效设备id:{},忽略设备状态上报消息:{}", deviceId, deviceProperty); + return; + } + switch (deviceProperty.getPropertyType()) { + case RUNNING_STATUS: + handleRunningStatus(device, deviceProperty, topicContext); + break; + case APP_VERSION: + handleAppVersion(device, deviceProperty, topicContext); + break; + case BATTERY_PERCENT: + handleBatteryPercent(device, deviceProperty, topicContext); + break; + case TEMPERATURE: + handleTemperature(device, deviceProperty, topicContext); + break; + default: + log.warn("未知设备属性类型:{},忽略设备状态上报消息:{}", deviceProperty.getPropertyType(), deviceProperty); + break; + } + } + + private void handleRunningStatus(IotDeviceEntity device, DevicePropertyDTO deviceProperty, DeviceTopicEnum.DeviceTopicContext topicContext) { + DeviceRunningStatusEnum oldStatus = DeviceRunningStatusEnum.parse(device.getRunningStatus().toString()); + DeviceRunningStatusEnum newStatus = DeviceRunningStatusEnum.parse(deviceProperty.getPayload()); + if (newStatus.equals(oldStatus)) { + return; + } + device.setRunningStatus(newStatus.getValue()); + if (DeviceRunningStatusEnum.ONLINE.equals(newStatus)) { + device.setUpTime(LocalDateTime.now()); + } else if (DeviceRunningStatusEnum.OFFLINE.equals(newStatus)) { + device.setDownTime(LocalDateTime.now()); + } + device.setUpdateTime(LocalDateTime.now()); + getBaseMapper().updateById(device); + + if (newStatus.getEventType() != null) { + deviceEventLogService.createAndSaveDeviceEvent(device.getId(), topicContext.getClient().getTenantId(), + newStatus.getEventType(), null, StrUtil.format("设备上报运行状态 : {}", newStatus.getTitle())); + } + + if (log.isInfoEnabled()) { + log.info("租户:{},设备:{}-{},运行状态:{}-{} -> {}-{}", topicContext.getClient().getTenantId(), device.getId(), device.getName(), oldStatus.getValue(), oldStatus.getTitle(), newStatus.getValue(), newStatus.getTitle()); + } + } + + private void handleAppVersion(IotDeviceEntity device, DevicePropertyDTO deviceStatus, DeviceTopicEnum.DeviceTopicContext topicContext) { + if (CharSequenceUtil.equals(device.getAppVersion(), deviceStatus.getPayload())) { + return; + } + device.setAppVersion(deviceStatus.getPayload()); + device.setUpdateTime(LocalDateTime.now()); + getBaseMapper().updateById(device); + + deviceEventLogService.createAndSaveDeviceEvent(device.getId(), topicContext.getClient().getTenantId(), + DeviceEventTypeEnum.APP_VERSION_REPORT, null, + StrUtil.format(" {}:{}", DeviceEventTypeEnum.APP_VERSION_REPORT.getTitle(), deviceStatus.getPayload())); + + if (log.isInfoEnabled()) { + log.info("租户:{},设备:{}-{},App版本:{} -> {}", topicContext.getClient().getTenantId(), device.getId(), device.getName(), device.getAppVersion(), deviceStatus.getPayload()); + } + } + + private void handleBatteryPercent(IotDeviceEntity device, DevicePropertyDTO deviceStatus, DeviceTopicEnum.DeviceTopicContext topicContext) { + String batteryPercent = deviceStatus.getPayload(); + if (batteryPercent.equals(device.getBatteryPercent())) { + return; + } + device.setBatteryPercent(batteryPercent); + device.setUpdateTime(LocalDateTime.now()); + getBaseMapper().updateById(device); + + deviceEventLogService.createAndSaveDeviceEvent(device.getId(), topicContext.getClient().getTenantId(), + DeviceEventTypeEnum.BATTERY_PERCENT_REPORT, null, + StrUtil.format(" {}:{}", DeviceEventTypeEnum.BATTERY_PERCENT_REPORT.getTitle(), deviceStatus.getPayload())); + + if (log.isInfoEnabled()) { + log.info("租户:{},设备:{}-{},电池电量百分比:{} -> {}", topicContext.getClient().getTenantId(), device.getId(), device.getName(), device.getBatteryPercent(), batteryPercent); + } + } + + private void handleTemperature(IotDeviceEntity device, DevicePropertyDTO deviceStatus, DeviceTopicEnum.DeviceTopicContext topicContext) { + String temperature = deviceStatus.getPayload(); + if (temperature.equals(device.getTemperature())) { + return; + } + device.setTemperature(temperature); + device.setUpdateTime(LocalDateTime.now()); + getBaseMapper().updateById(device); + + deviceEventLogService.createAndSaveDeviceEvent(device.getId(), topicContext.getClient().getTenantId(), + DeviceEventTypeEnum.TEMPERATURE_REPORT, null, + StrUtil.format(" {}:{}", DeviceEventTypeEnum.TEMPERATURE_REPORT.getTitle(), deviceStatus.getPayload())); + + if (log.isInfoEnabled()) { + log.info("租户:{},设备:{}-{},温度:{} -> {}", topicContext.getClient().getTenantId(), device.getId(), device.getName(), device.getTemperature(), temperature); + } + } + + /** + * 设备命令响应处理 + * + * @param topic + * @param commandResponse + */ + @Override + public void handle(String topic, DeviceCommandResponseDTO commandResponse) { + DeviceTopicEnum.DeviceTopicContext topicContext = null; + try { + topicContext = DeviceTopicEnum.parseContext(topic); + } catch (Exception e) { + log.warn("无效设备主题:{},忽略设备命令响应消息:{}", topic, commandResponse); + return; + } + if (log.isDebugEnabled()) { + log.warn("处理设备设备命令响应消息,Topic:{}, TopicContext:{}", topic, topicContext); + } + + Long deviceId = topicContext.getClient().getDeviceId(); + IotDeviceEntity device = super.getById(deviceId); + if (device == null) { + log.warn("无效设备id:{},忽略设备命令响应消息:{}", deviceId, commandResponse); + return; + } + switch (commandResponse.getCommand()) { + case LOCK, UNLOCK, OTA_UPGRADE: { + //记录处理设备事件 + deviceEventLogService.createAndSaveDeviceEvent(device.getId(), + topicContext.getClient().getTenantId(), + commandResponse.getCommand().getEventType(), + commandResponse.getCommandId(), commandResponse.getResponsePayload()); + break; + } + default: + log.warn("未知设备命令类型:{},忽略设备命令响应消息:{}", commandResponse.getCommand(), commandResponse); + break; + + } + + + } +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceLogServiceImpl.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceLogServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..158b8b29c64c981415626562decebcbb00ec40f5 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/service/impl/IotDeviceServiceLogServiceImpl.java @@ -0,0 +1,94 @@ +package net.maku.iot.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.AllArgsConstructor; +import net.maku.framework.common.utils.JsonUtils; +import net.maku.framework.common.utils.PageResult; +import net.maku.framework.mybatis.service.impl.BaseServiceImpl; +import net.maku.iot.convert.IotDeviceServiceLogConvert; +import net.maku.iot.dao.IotDeviceServiceLogDao; +import net.maku.iot.entity.IotDeviceServiceLogEntity; +import net.maku.iot.enums.DeviceCommandEnum; +import net.maku.iot.query.IotDeviceServiceLogQuery; +import net.maku.iot.service.IotDeviceServiceLogService; +import net.maku.iot.vo.IotDeviceServiceLogVO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@Service +@AllArgsConstructor +public class IotDeviceServiceLogServiceImpl extends BaseServiceImpl implements IotDeviceServiceLogService { + + @Override + public PageResult page(IotDeviceServiceLogQuery query) { + IPage page = baseMapper.selectPage(getPage(query), getWrapper(query)); + List vos = IotDeviceServiceLogConvert.INSTANCE.convertList(page.getRecords()); + vos.forEach(vo -> { + vo.setDeviceCommandEnum(DeviceCommandEnum.getEnum(vo.getServiceType())); + }); + return new PageResult<>(vos, page.getTotal()); + } + + private LambdaQueryWrapper getWrapper(IotDeviceServiceLogQuery query) { + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); + if (StrUtil.isNotBlank(query.getDeviceCommandEnum())) { + wrapper.eq(IotDeviceServiceLogEntity::getServiceType, DeviceCommandEnum.parse(query.getDeviceCommandEnum()).getValue()); + } + wrapper.eq(query.getDeviceId() != null, IotDeviceServiceLogEntity::getDeviceId, query.getDeviceId()); + wrapper.orderByDesc(IotDeviceServiceLogEntity::getServiceTime); + return wrapper; + } + + @Override + public void save(IotDeviceServiceLogVO vo) { + IotDeviceServiceLogEntity entity = IotDeviceServiceLogConvert.INSTANCE.convert(vo); + + baseMapper.insert(entity); + } + + @Override + public void update(IotDeviceServiceLogVO vo) { + IotDeviceServiceLogEntity entity = IotDeviceServiceLogConvert.INSTANCE.convert(vo); + + updateById(entity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(List idList) { + removeByIds(idList); + } + + @Override + public IotDeviceServiceLogEntity createDeviceServiceLog(Long deviceId, Long tenantId, DeviceCommandEnum serviceCommand, String eventUid, Object payload) { + IotDeviceServiceLogEntity deviceServiceLog = new IotDeviceServiceLogEntity(); + deviceServiceLog.setDeviceId(deviceId); + deviceServiceLog.setTenantId(tenantId); + deviceServiceLog.setServiceType(serviceCommand.getValue()); + deviceServiceLog.setServiceUid(eventUid); + if (payload != null) { + deviceServiceLog.setServicePayload((payload instanceof String) + ? (String) payload + : JsonUtils.toJsonString(payload)); + } + deviceServiceLog.setServiceTime(LocalDateTime.now()); + return deviceServiceLog; + } + + @Override + public void createAndSaveDeviceServiceLog(Long deviceId, Long tenantId, DeviceCommandEnum serviceCommand, String eventUid, Object payload) { + save(createDeviceServiceLog(deviceId, tenantId, serviceCommand, eventUid, payload)); + } + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/utils/MqttUtils.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/utils/MqttUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8e3f0cead45f59cd1630b2bb9c73049c9afee5ce --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/utils/MqttUtils.java @@ -0,0 +1,44 @@ +package net.maku.iot.utils; + +import lombok.RequiredArgsConstructor; +import org.springframework.integration.mqtt.inbound.AbstractMqttMessageDrivenChannelAdapter; +import org.springframework.stereotype.Component; + +/** + * Mqtt工具类 + * + * @author LSF maku_lsf@163.com + */ +@Component +@RequiredArgsConstructor +public class MqttUtils { + private final AbstractMqttMessageDrivenChannelAdapter mqttMessageAdapter; + + /** + * 动态订阅主题,默认使用qos 1 + * + * @param topics + */ + public void addTopic(String... topics) { + mqttMessageAdapter.addTopic(topics); + } + + /** + * 动态订阅主题 + * + * @param topic + * @param qos + */ + public void addTopic(String topic, int qos) { + mqttMessageAdapter.addTopic(topic, qos); + } + + /** + * 动态取消主题订阅 + * + * @param topics + */ + public void removeTopic(String... topics) { + mqttMessageAdapter.removeTopic(topics); + } +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandResponseAttributeDataVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandResponseAttributeDataVO.java new file mode 100644 index 0000000000000000000000000000000000000000..c5a5e2d3b9993aa77585bebc9019caddbfa74fc8 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandResponseAttributeDataVO.java @@ -0,0 +1,39 @@ +package net.maku.iot.vo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.iot.enums.DeviceCommandEnum; + +/** + * 设备命令响应数据 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备命令响应VO") +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceCommandResponseAttributeDataVO { + private static final long serialVersionUID = 1L; + + @Schema(description = "设备ID") + private Long deviceId; + + @Schema(description = "命令类型", required = true) + private DeviceCommandEnum command; + + @Schema(description = "命令ID", required = true) + private String commandId; + + @Schema(description = "命令是否完成(默认true:命令已完成;false:命令未完成,后续命令完成将再次发送响应消息,服务端将继续等待该命令完成的响应)") + private boolean isCompleted = true; + + @Schema(description = "响应状态码,0成功,其它数值异常,根据业务需要自定义") + private Integer statusCode = 0; + + @Schema(description = "响应状态消息") + private String statusMessage; + + @Schema(description = "命令响应结果") + private String responsePayload; +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandVO.java new file mode 100644 index 0000000000000000000000000000000000000000..cf27081c065bb66c0ca786245a5e8a8e6295f18d --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceCommandVO.java @@ -0,0 +1,24 @@ +package net.maku.iot.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 设备指令 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备指令VO") +public class DeviceCommandVO { + private static final long serialVersionUID = 1L; + + @Schema(description = "设备ID") + private Long deviceId; + + @Schema(description = "指令") + private String command; + + @Schema(description = "指令内容") + private String payload; +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceReportAttributeDataVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceReportAttributeDataVO.java new file mode 100644 index 0000000000000000000000000000000000000000..19ed341493140aaf405ef24803d0f43712ae086a --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/DeviceReportAttributeDataVO.java @@ -0,0 +1,25 @@ +package net.maku.iot.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 设备上报属性数据 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备上报属性数据VO") +public class DeviceReportAttributeDataVO { + private static final long serialVersionUID = 1L; + + @Schema(description = "设备ID") + private Long deviceId; + + @Schema(description = "设备属性类型") + private String propertyType; + + @Schema(description = "属性数据") + private String payload; + +} diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceEventLogVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceEventLogVO.java new file mode 100644 index 0000000000000000000000000000000000000000..339c9361601f3fa8fc4bd06df67de8e9ed12d10b --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceEventLogVO.java @@ -0,0 +1,38 @@ +package net.maku.iot.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.framework.common.utils.DateUtils; +import net.maku.iot.enums.DeviceEventTypeEnum; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设备事件日志 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备事件日志") +public class IotDeviceEventLogVO implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "事件类型") + private Integer eventType; + + @Schema(description = "事件") + private DeviceEventTypeEnum eventTypeEnum; + + @Schema(description = "事件标识id") + private String eventUid; + + @Schema(description = "事件数据") + private String eventPayload; + + @Schema(description = "事件时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime eventTime; + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceServiceLogVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceServiceLogVO.java new file mode 100644 index 0000000000000000000000000000000000000000..f30ba09ae2bf6242c9fbdb5601e94606b5b2549b --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceServiceLogVO.java @@ -0,0 +1,41 @@ +package net.maku.iot.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.framework.common.utils.DateUtils; +import net.maku.iot.enums.DeviceCommandEnum; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设备服务日志 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备服务日志") +public class IotDeviceServiceLogVO implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "服务类型") + private Integer serviceType; + + @Schema(description = "指令") + private DeviceCommandEnum deviceCommandEnum; + + @Schema(description = "服务标识id") + private String serviceUid; + + @Schema(description = "服务数据") + private String servicePayload; + + @Schema(description = "服务时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime serviceTime; + + @Schema(description = "创建时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceVO.java b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceVO.java new file mode 100644 index 0000000000000000000000000000000000000000..7e71b6ec19bf56af7f0989dc854ad8c7a9fa117c --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/java/net/maku/iot/vo/IotDeviceVO.java @@ -0,0 +1,80 @@ +package net.maku.iot.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import net.maku.framework.common.utils.DateUtils; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 设备表 + * + * @author LSF maku_lsf@163.com + */ +@Data +@Schema(description = "设备表") +public class IotDeviceVO implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "ID") + private Long id; + + @Schema(description = "编码") + private String code; + + @Schema(description = "名称") + private String name; + + @Schema(description = "设备类型,1.手持设备,2.柜体,3传感设备") + private Integer type; + + @Schema(description = "唯一标识码") + private String uid; + + @Schema(description = "设备密钥") + private String secret; + + @Schema(description = "App版本号") + private String appVersion; + + @Schema(description = "电池电量百分比") + private String batteryPercent; + + @Schema(description = "温度") + private String temperature; + + @Schema(description = "状态,0禁用,1正常") + private Integer status; + + @Schema(description = "运行状态") + private Integer runningStatus; + + @Schema(description = "上线时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime upTime; + + @Schema(description = "下线时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime downTime; + + @Schema(description = "租户ID") + private Long tenantId; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime createTime; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新时间") + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) + private LocalDateTime updateTime; + + +} \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceDao.xml b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceDao.xml new file mode 100644 index 0000000000000000000000000000000000000000..6ca09fc70100b25eda36c897411c2cf763ad63d4 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceDao.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceEventLogDao.xml b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceEventLogDao.xml new file mode 100644 index 0000000000000000000000000000000000000000..ab0706f52dffa8bc6050d3e87807e9ff16544992 --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceEventLogDao.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceServiceLogDao.xml b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceServiceLogDao.xml new file mode 100644 index 0000000000000000000000000000000000000000..00a7a6262cd1896d0269a207bf77322e0478faed --- /dev/null +++ b/maku-boot-module/maku-module-iot/src/main/resources/mapper/iot/IotDeviceServiceLogDao.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/maku-boot-module/pom.xml b/maku-boot-module/pom.xml index 87572b052cb37a44ce9144d5c30f0f0b27e33d54..cf597e29566beb08a092a6a9a030a6ffa1ab5730 100644 --- a/maku-boot-module/pom.xml +++ b/maku-boot-module/pom.xml @@ -13,6 +13,7 @@ maku-module-generator maku-module-monitor maku-module-member + maku-module-iot \ No newline at end of file diff --git a/maku-server/pom.xml b/maku-server/pom.xml index aed71c0a15037adb4a725c6eff7e069065c62c7f..40a9cb08ff883692c4b99d87ccb1a93b0003dc93 100644 --- a/maku-server/pom.xml +++ b/maku-server/pom.xml @@ -40,6 +40,11 @@ maku-module-monitor ${revision} + + net.maku + maku-module-iot + ${revision} + org.springframework.boot spring-boot-starter-test diff --git a/maku-server/src/main/resources/application-dev.yml b/maku-server/src/main/resources/application-dev.yml index db332d6fc577410834ff88e2534f3c4773157734..aba760484a2267df11b25ad631a3be7b1a4bffe3 100644 --- a/maku-server/src/main/resources/application-dev.yml +++ b/maku-server/src/main/resources/application-dev.yml @@ -33,3 +33,11 @@ spring: # url: jdbc:postgresql://192.168.3.19:5432/postgres # username: postgres # password: 123456 + mqtt: + hostUrl: tcp://localhost:1883 + username: maku + password: maku + clientId: maku_boot_service_dev + defaultTopic: topic_default + #true 开启监听 false 关闭监听 用于监设备上报的信息 + mqttEnabled: true \ No newline at end of file diff --git a/maku-server/src/main/resources/application-prod.yml b/maku-server/src/main/resources/application-prod.yml index 6716b6cfc729cb4cc2603f6c4acbb36db21965df..aba760484a2267df11b25ad631a3be7b1a4bffe3 100644 --- a/maku-server/src/main/resources/application-prod.yml +++ b/maku-server/src/main/resources/application-prod.yml @@ -32,4 +32,12 @@ spring: # driver-class-name: org.postgresql.Driver # url: jdbc:postgresql://192.168.3.19:5432/postgres # username: postgres - # password: 123456 \ No newline at end of file + # password: 123456 + mqtt: + hostUrl: tcp://localhost:1883 + username: maku + password: maku + clientId: maku_boot_service_dev + defaultTopic: topic_default + #true 开启监听 false 关闭监听 用于监设备上报的信息 + mqttEnabled: true \ No newline at end of file diff --git a/maku-server/src/main/resources/application-test.yml b/maku-server/src/main/resources/application-test.yml index 6716b6cfc729cb4cc2603f6c4acbb36db21965df..aba760484a2267df11b25ad631a3be7b1a4bffe3 100644 --- a/maku-server/src/main/resources/application-test.yml +++ b/maku-server/src/main/resources/application-test.yml @@ -32,4 +32,12 @@ spring: # driver-class-name: org.postgresql.Driver # url: jdbc:postgresql://192.168.3.19:5432/postgres # username: postgres - # password: 123456 \ No newline at end of file + # password: 123456 + mqtt: + hostUrl: tcp://localhost:1883 + username: maku + password: maku + clientId: maku_boot_service_dev + defaultTopic: topic_default + #true 开启监听 false 关闭监听 用于监设备上报的信息 + mqttEnabled: true \ No newline at end of file diff --git a/pom.xml b/pom.xml index f25066bda4f75fab026df1e8241e6828756e28e9..9deb4ede402dd74ccf58dadb423e83f5c8e76b12 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ 1.0.6 3.1.944 1.6.2 + 5.5.6 @@ -229,6 +230,11 @@ javax.mail ${mail.version} + + org.springframework.integration + spring-integration-mqtt + ${spring-integration-mqtt.version} +