Commit e46c0f34211e21f16734eff83ea8609e1355ddb9

Authored by 芋道源码
Committed by Gitee
2 parents f8ebdbdd d01a6e21

!1506 iot:最新的 tcp、udp、coap、mqtt、emqx 等同步

Merge pull request !1506 from 芋道源码/feature/iot
Showing 142 changed files with 12854 additions and 3140 deletions

Too many changes.

To preserve performance only 100 of 142 files are displayed.

yudao-dependencies/pom.xml
@@ -68,6 +68,8 @@ @@ -68,6 +68,8 @@
68 <netty.version>4.2.9.Final</netty.version> 68 <netty.version>4.2.9.Final</netty.version>
69 <mqtt.version>1.2.5</mqtt.version> 69 <mqtt.version>1.2.5</mqtt.version>
70 <vertx.version>4.5.22</vertx.version> 70 <vertx.version>4.5.22</vertx.version>
  71 + <okhttp.version>4.12.0</okhttp.version>
  72 + <californium.version>3.12.0</californium.version>
71 <!-- 三方云服务相关 --> 73 <!-- 三方云服务相关 -->
72 <awssdk.version>2.40.15</awssdk.version> 74 <awssdk.version>2.40.15</awssdk.version>
73 <justauth.version>1.16.7</justauth.version> 75 <justauth.version>1.16.7</justauth.version>
@@ -555,6 +557,50 @@ @@ -555,6 +557,50 @@
555 <version>${jsoup.version}</version> 557 <version>${jsoup.version}</version>
556 </dependency> 558 </dependency>
557 559
  560 + <!-- Vert.x -->
  561 + <dependency>
  562 + <groupId>io.vertx</groupId>
  563 + <artifactId>vertx-core</artifactId>
  564 + <version>${vertx.version}</version>
  565 + </dependency>
  566 + <dependency>
  567 + <groupId>io.vertx</groupId>
  568 + <artifactId>vertx-web</artifactId>
  569 + <version>${vertx.version}</version>
  570 + </dependency>
  571 + <dependency>
  572 + <groupId>io.vertx</groupId>
  573 + <artifactId>vertx-mqtt</artifactId>
  574 + <version>${vertx.version}</version>
  575 + </dependency>
  576 +
  577 + <!-- MQTT -->
  578 + <dependency>
  579 + <groupId>org.eclipse.paho</groupId>
  580 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  581 + <version>${mqtt.version}</version>
  582 + </dependency>
  583 +
  584 + <!-- OkHttp -->
  585 + <dependency>
  586 + <groupId>com.squareup.okhttp3</groupId>
  587 + <artifactId>okhttp</artifactId>
  588 + <version>${okhttp.version}</version>
  589 + </dependency>
  590 + <dependency>
  591 + <groupId>com.squareup.okhttp3</groupId>
  592 + <artifactId>mockwebserver</artifactId>
  593 + <version>${okhttp.version}</version>
  594 + <scope>test</scope>
  595 + </dependency>
  596 +
  597 + <!-- CoAP - Eclipse Californium -->
  598 + <dependency>
  599 + <groupId>org.eclipse.californium</groupId>
  600 + <artifactId>californium-core</artifactId>
  601 + <version>${californium.version}</version>
  602 + </dependency>
  603 +
558 <!-- 三方云服务相关 --> 604 <!-- 三方云服务相关 -->
559 <dependency> 605 <dependency>
560 <groupId>software.amazon.awssdk</groupId> 606 <groupId>software.amazon.awssdk</groupId>
@@ -629,30 +675,6 @@ @@ -629,30 +675,6 @@
629 </exclusion> 675 </exclusion>
630 </exclusions> 676 </exclusions>
631 </dependency> 677 </dependency>
632 -  
633 - <!-- Vert.x -->  
634 - <dependency>  
635 - <groupId>io.vertx</groupId>  
636 - <artifactId>vertx-core</artifactId>  
637 - <version>${vertx.version}</version>  
638 - </dependency>  
639 - <dependency>  
640 - <groupId>io.vertx</groupId>  
641 - <artifactId>vertx-web</artifactId>  
642 - <version>${vertx.version}</version>  
643 - </dependency>  
644 - <dependency>  
645 - <groupId>io.vertx</groupId>  
646 - <artifactId>vertx-mqtt</artifactId>  
647 - <version>${vertx.version}</version>  
648 - </dependency>  
649 -  
650 - <!-- MQTT -->  
651 - <dependency>  
652 - <groupId>org.eclipse.paho</groupId>  
653 - <artifactId>org.eclipse.paho.client.mqttv3</artifactId>  
654 - <version>${mqtt.version}</version>  
655 - </dependency>  
656 </dependencies> 678 </dependencies>
657 </dependencyManagement> 679 </dependencyManagement>
658 680
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
7 import com.google.common.collect.Maps; 7 import com.google.common.collect.Maps;
8 import com.google.common.collect.Multimap; 8 import com.google.common.collect.Multimap;
9 9
  10 +import java.math.BigDecimal;
10 import java.util.ArrayList; 11 import java.util.ArrayList;
11 import java.util.Collection; 12 import java.util.Collection;
12 import java.util.List; 13 import java.util.List;
@@ -65,4 +66,47 @@ public class MapUtils { @@ -65,4 +66,47 @@ public class MapUtils {
65 return map; 66 return map;
66 } 67 }
67 68
  69 + /**
  70 + * 从 Map 中获取 BigDecimal 值
  71 + *
  72 + * @param map Map 数据源
  73 + * @param key 键名
  74 + * @return BigDecimal 值,解析失败或值为 null 时返回 null
  75 + */
  76 + public static BigDecimal getBigDecimal(Map<String, ?> map, String key) {
  77 + return getBigDecimal(map, key, null);
  78 + }
  79 +
  80 + /**
  81 + * 从 Map 中获取 BigDecimal 值
  82 + *
  83 + * @param map Map 数据源
  84 + * @param key 键名
  85 + * @param defaultValue 默认值
  86 + * @return BigDecimal 值,解析失败或值为 null 时返回默认值
  87 + */
  88 + public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) {
  89 + if (map == null) {
  90 + return defaultValue;
  91 + }
  92 + Object value = map.get(key);
  93 + if (value == null) {
  94 + return defaultValue;
  95 + }
  96 + if (value instanceof BigDecimal) {
  97 + return (BigDecimal) value;
  98 + }
  99 + if (value instanceof Number) {
  100 + return BigDecimal.valueOf(((Number) value).doubleValue());
  101 + }
  102 + if (value instanceof String) {
  103 + try {
  104 + return new BigDecimal((String) value);
  105 + } catch (NumberFormatException e) {
  106 + return defaultValue;
  107 + }
  108 + }
  109 + return defaultValue;
  110 + }
  111 +
68 } 112 }
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -229,4 +229,53 @@ public class JsonUtils { @@ -229,4 +229,53 @@ public class JsonUtils {
229 return JSONUtil.isTypeJSONObject(str); 229 return JSONUtil.isTypeJSONObject(str);
230 } 230 }
231 231
  232 + /**
  233 + * 将 Object 转换为目标类型
  234 + * <p>
  235 + * 避免先转 jsonString 再 parseObject 的性能损耗
  236 + *
  237 + * @param obj 源对象(可以是 Map、POJO 等)
  238 + * @param clazz 目标类型
  239 + * @return 转换后的对象
  240 + */
  241 + public static <T> T convertObject(Object obj, Class<T> clazz) {
  242 + if (obj == null) {
  243 + return null;
  244 + }
  245 + if (clazz.isInstance(obj)) {
  246 + return clazz.cast(obj);
  247 + }
  248 + return objectMapper.convertValue(obj, clazz);
  249 + }
  250 +
  251 + /**
  252 + * 将 Object 转换为目标类型(支持泛型)
  253 + *
  254 + * @param obj 源对象
  255 + * @param typeReference 目标类型引用
  256 + * @return 转换后的对象
  257 + */
  258 + public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
  259 + if (obj == null) {
  260 + return null;
  261 + }
  262 + return objectMapper.convertValue(obj, typeReference);
  263 + }
  264 +
  265 + /**
  266 + * 将 Object 转换为 List 类型
  267 + * <p>
  268 + * 避免先转 jsonString 再 parseArray 的性能损耗
  269 + *
  270 + * @param obj 源对象(可以是 List、数组等)
  271 + * @param clazz 目标元素类型
  272 + * @return 转换后的 List
  273 + */
  274 + public static <T> List<T> convertList(Object obj, Class<T> clazz) {
  275 + if (obj == null) {
  276 + return new ArrayList<>();
  277 + }
  278 + return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
  279 + }
  280 +
232 } 281 }
yudao-module-iot/yudao-module-iot-biz/pom.xml
@@ -73,6 +73,17 @@ @@ -73,6 +73,17 @@
73 <artifactId>yudao-spring-boot-starter-excel</artifactId> 73 <artifactId>yudao-spring-boot-starter-excel</artifactId>
74 </dependency> 74 </dependency>
75 75
  76 + <!-- OkHttp -->
  77 + <dependency>
  78 + <groupId>com.squareup.okhttp3</groupId>
  79 + <artifactId>okhttp</artifactId>
  80 + </dependency>
  81 + <dependency>
  82 + <groupId>com.squareup.okhttp3</groupId>
  83 + <artifactId>mockwebserver</artifactId>
  84 + <scope>test</scope>
  85 + </dependency>
  86 +
76 <!-- 消息队列相关 --> 87 <!-- 消息队列相关 -->
77 <!-- TODO @芋艿:临时打开 --> 88 <!-- TODO @芋艿:临时打开 -->
78 <dependency> 89 <dependency>
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
@@ -7,6 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; @@ -7,6 +7,10 @@ import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 7 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 15 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
12 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 16 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping; @@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.PostMapping;
19 import org.springframework.web.bind.annotation.RequestBody; 23 import org.springframework.web.bind.annotation.RequestBody;
20 import org.springframework.web.bind.annotation.RestController; 24 import org.springframework.web.bind.annotation.RestController;
21 25
  26 +import java.util.List;
  27 +
22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 28 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
23 29
24 /** 30 /**
@@ -57,4 +63,18 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { @@ -57,4 +63,18 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi {
57 })); 63 }));
58 } 64 }
59 65
  66 + @Override
  67 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register")
  68 + @PermitAll
  69 + public CommonResult<IotDeviceRegisterRespDTO> registerDevice(@RequestBody IotDeviceRegisterReqDTO reqDTO) {
  70 + return success(deviceService.registerDevice(reqDTO));
  71 + }
  72 +
  73 + @Override
  74 + @PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/register-sub")
  75 + @PermitAll
  76 + public CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(@RequestBody IotSubDeviceRegisterFullReqDTO reqDTO) {
  77 + return success(deviceService.registerSubDevices(reqDTO));
  78 + }
  79 +
60 } 80 }
61 \ No newline at end of file 81 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
1 package cn.iocoder.yudao.module.iot.controller.admin.device; 1 package cn.iocoder.yudao.module.iot.controller.admin.device;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; 4 import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
4 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 import cn.iocoder.yudao.framework.common.pojo.PageParam; 6 import cn.iocoder.yudao.framework.common.pojo.PageParam;
6 import cn.iocoder.yudao.framework.common.pojo.PageResult; 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; 10 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 11 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
11 -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum; 13 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
12 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  15 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
13 import io.swagger.v3.oas.annotations.Operation; 16 import io.swagger.v3.oas.annotations.Operation;
14 import io.swagger.v3.oas.annotations.Parameter; 17 import io.swagger.v3.oas.annotations.Parameter;
15 import io.swagger.v3.oas.annotations.Parameters; 18 import io.swagger.v3.oas.annotations.Parameters;
@@ -23,13 +26,12 @@ import org.springframework.web.bind.annotation.*; @@ -23,13 +26,12 @@ import org.springframework.web.bind.annotation.*;
23 import org.springframework.web.multipart.MultipartFile; 26 import org.springframework.web.multipart.MultipartFile;
24 27
25 import java.io.IOException; 28 import java.io.IOException;
26 -import java.util.Arrays;  
27 -import java.util.Collection;  
28 -import java.util.List; 29 +import java.util.*;
29 30
30 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; 31 import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
31 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 32 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
32 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 33 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  34 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
33 35
34 @Tag(name = "管理后台 - IoT 设备") 36 @Tag(name = "管理后台 - IoT 设备")
35 @RestController 37 @RestController
@@ -39,6 +41,8 @@ public class IotDeviceController { @@ -39,6 +41,8 @@ public class IotDeviceController {
39 41
40 @Resource 42 @Resource
41 private IotDeviceService deviceService; 43 private IotDeviceService deviceService;
  44 + @Resource
  45 + private IotProductService productService;
42 46
43 @PostMapping("/create") 47 @PostMapping("/create")
44 @Operation(summary = "创建设备") 48 @Operation(summary = "创建设备")
@@ -47,6 +51,7 @@ public class IotDeviceController { @@ -47,6 +51,7 @@ public class IotDeviceController {
47 return success(deviceService.createDevice(createReqVO)); 51 return success(deviceService.createDevice(createReqVO));
48 } 52 }
49 53
  54 +
50 @PutMapping("/update") 55 @PutMapping("/update")
51 @Operation(summary = "更新设备") 56 @Operation(summary = "更新设备")
52 @PreAuthorize("@ss.hasPermission('iot:device:update')") 57 @PreAuthorize("@ss.hasPermission('iot:device:update')")
@@ -55,7 +60,57 @@ public class IotDeviceController { @@ -55,7 +60,57 @@ public class IotDeviceController {
55 return success(true); 60 return success(true);
56 } 61 }
57 62
58 - // TODO @芋艿:参考阿里云:1)绑定网关;2)解绑网关 63 + @PutMapping("/bind-gateway")
  64 + @Operation(summary = "绑定子设备到网关")
  65 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  66 + public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) {
  67 + deviceService.bindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
  68 + return success(true);
  69 + }
  70 +
  71 + @PutMapping("/unbind-gateway")
  72 + @Operation(summary = "解绑子设备与网关")
  73 + @PreAuthorize("@ss.hasPermission('iot:device:update')")
  74 + public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
  75 + deviceService.unbindDeviceGateway(reqVO.getSubIds(), reqVO.getGatewayId());
  76 + return success(true);
  77 + }
  78 +
  79 + @GetMapping("/sub-device-list")
  80 + @Operation(summary = "获取网关的子设备列表")
  81 + @Parameter(name = "gatewayId", description = "网关设备编号", required = true, example = "1")
  82 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  83 + public CommonResult<List<IotDeviceRespVO>> getSubDeviceList(@RequestParam("gatewayId") Long gatewayId) {
  84 + List<IotDeviceDO> list = deviceService.getDeviceListByGatewayId(gatewayId);
  85 + if (CollUtil.isEmpty(list)) {
  86 + return success(Collections.emptyList());
  87 + }
  88 +
  89 + // 补充产品名称
  90 + Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId);
  91 + return success(convertList(list, device -> {
  92 + IotDeviceRespVO respVO = BeanUtils.toBean(device, IotDeviceRespVO.class);
  93 + MapUtils.findAndThen(productMap, device.getProductId(),
  94 + product -> respVO.setProductName(product.getName()));
  95 + return respVO;
  96 + }));
  97 + }
  98 +
  99 + @GetMapping("/unbound-sub-device-page")
  100 + @Operation(summary = "获取未绑定网关的子设备分页")
  101 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  102 + public CommonResult<PageResult<IotDeviceRespVO>> getUnboundSubDevicePage(@Valid IotDevicePageReqVO pageReqVO) {
  103 + PageResult<IotDeviceDO> pageResult = deviceService.getUnboundSubDevicePage(pageReqVO);
  104 + if (CollUtil.isEmpty(pageResult.getList())) {
  105 + return success(PageResult.empty());
  106 + }
  107 +
  108 + // 补充产品名称
  109 + Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId);
  110 + PageResult<IotDeviceRespVO> result = BeanUtils.toBean(pageResult, IotDeviceRespVO.class, device ->
  111 + MapUtils.findAndThen(productMap, device.getProductId(), product -> device.setProductName(product.getName())));
  112 + return success(result);
  113 + }
59 114
60 @PutMapping("/update-group") 115 @PutMapping("/update-group")
61 @Operation(summary = "更新设备分组") 116 @Operation(summary = "更新设备分组")
@@ -136,6 +191,26 @@ public class IotDeviceController { @@ -136,6 +191,26 @@ public class IotDeviceController {
136 .setProductId(device.getProductId()).setState(device.getState()))); 191 .setProductId(device.getProductId()).setState(device.getState())));
137 } 192 }
138 193
  194 + @GetMapping("/location-list")
  195 + @Operation(summary = "获取设备位置列表", description = "获取有经纬度信息的设备列表,用于地图展示")
  196 + @PreAuthorize("@ss.hasPermission('iot:device:query')")
  197 + public CommonResult<List<IotDeviceRespVO>> getDeviceLocationList() {
  198 + // 1. 获取有位置信息的设备列表
  199 + List<IotDeviceDO> devices = deviceService.getDeviceListByHasLocation();
  200 + if (CollUtil.isEmpty(devices)) {
  201 + return success(Collections.emptyList());
  202 + }
  203 +
  204 + // 2. 转换并返回
  205 + Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId);
  206 + return success(convertList(devices, device -> {
  207 + IotDeviceRespVO respVO = BeanUtils.toBean(device, IotDeviceRespVO.class);
  208 + MapUtils.findAndThen(productMap, device.getProductId(),
  209 + product -> respVO.setProductName(product.getName()));
  210 + return respVO;
  211 + }));
  212 + }
  213 +
139 @PostMapping("/import") 214 @PostMapping("/import")
140 @Operation(summary = "导入设备") 215 @Operation(summary = "导入设备")
141 @PreAuthorize("@ss.hasPermission('iot:device:import')") 216 @PreAuthorize("@ss.hasPermission('iot:device:import')")
@@ -153,10 +228,9 @@ public class IotDeviceController { @@ -153,10 +228,9 @@ public class IotDeviceController {
153 // 手动创建导出 demo 228 // 手动创建导出 demo
154 List<IotDeviceImportExcelVO> list = Arrays.asList( 229 List<IotDeviceImportExcelVO> list = Arrays.asList(
155 IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110") 230 IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110")
156 - .productKey("1de24640dfe").groupNames("灰度分组,生产分组")  
157 - .locationType(IotLocationTypeEnum.IP.getType()).build(), 231 + .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(),
158 IotDeviceImportExcelVO.builder().deviceName("biubiu").productKey("YzvHxd4r67sT4s2B") 232 IotDeviceImportExcelVO.builder().deviceName("biubiu").productKey("YzvHxd4r67sT4s2B")
159 - .groupNames("").locationType(IotLocationTypeEnum.MANUAL.getType()).build()); 233 + .groupNames("").build());
160 // 输出 234 // 输出
161 ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list); 235 ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list);
162 } 236 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceBindGatewayReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.Set;
  9 +
  10 +@Schema(description = "管理后台 - IoT 设备绑定网关 Request VO")
  11 +@Data
  12 +public class IotDeviceBindGatewayReqVO {
  13 +
  14 + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
  15 + @NotEmpty(message = "子设备编号列表不能为空")
  16 + private Set<Long> subIds;
  17 +
  18 + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  19 + @NotNull(message = "网关设备编号不能为空")
  20 + private Long gatewayId;
  21 +
  22 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java
1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2
3 import cn.idev.excel.annotation.ExcelProperty; 3 import cn.idev.excel.annotation.ExcelProperty;
4 -import cn.iocoder.yudao.framework.common.validation.InEnum;  
5 -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;  
6 import io.swagger.v3.oas.annotations.media.Schema; 4 import io.swagger.v3.oas.annotations.media.Schema;
7 import jakarta.validation.constraints.NotEmpty; 5 import jakarta.validation.constraints.NotEmpty;
8 -import jakarta.validation.constraints.NotNull;  
9 import lombok.AllArgsConstructor; 6 import lombok.AllArgsConstructor;
10 import lombok.Builder; 7 import lombok.Builder;
11 import lombok.Data; 8 import lombok.Data;
@@ -35,9 +32,4 @@ public class IotDeviceImportExcelVO { @@ -35,9 +32,4 @@ public class IotDeviceImportExcelVO {
35 @ExcelProperty("设备分组") 32 @ExcelProperty("设备分组")
36 private String groupNames; 33 private String groupNames;
37 34
38 - @ExcelProperty("上报方式(1:IP 定位, 2:设备上报,3:手动定位)")  
39 - @NotNull(message = "上报方式不能为空")  
40 - @InEnum(IotLocationTypeEnum.class)  
41 - private Integer locationType;  
42 -  
43 } 35 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
@@ -4,7 +4,6 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated; @@ -4,7 +4,6 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
4 import cn.idev.excel.annotation.ExcelProperty; 4 import cn.idev.excel.annotation.ExcelProperty;
5 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; 5 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
6 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; 6 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
7 -import cn.iocoder.yudao.module.iot.enums.DictTypeConstants;  
8 import io.swagger.v3.oas.annotations.media.Schema; 7 import io.swagger.v3.oas.annotations.media.Schema;
9 import lombok.Data; 8 import lombok.Data;
10 9
@@ -45,6 +44,9 @@ public class IotDeviceRespVO { @@ -45,6 +44,9 @@ public class IotDeviceRespVO {
45 @ExcelProperty("产品编号") 44 @ExcelProperty("产品编号")
46 private Long productId; 45 private Long productId;
47 46
  47 + @Schema(description = "产品名称", example = "温湿度传感器")
  48 + private String productName; // 只有部分接口返回,例如 getDeviceLocationList
  49 +
48 @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED) 50 @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED)
49 @ExcelProperty("产品 Key") 51 @ExcelProperty("产品 Key")
50 private String productKey; 52 private String productKey;
@@ -77,18 +79,9 @@ public class IotDeviceRespVO { @@ -77,18 +79,9 @@ public class IotDeviceRespVO {
77 @ExcelProperty("设备密钥") 79 @ExcelProperty("设备密钥")
78 private String deviceSecret; 80 private String deviceSecret;
79 81
80 - @Schema(description = "认证类型(如一机一密、动态注册)", example = "2")  
81 - @ExcelProperty("认证类型(如一机一密、动态注册)")  
82 - private String authType;  
83 -  
84 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") 82 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
85 private String config; 83 private String config;
86 84
87 - @Schema(description = "定位方式", example = "2")  
88 - @ExcelProperty(value = "定位方式", converter = DictConvert.class)  
89 - @DictFormat(DictTypeConstants.LOCATION_TYPE)  
90 - private Integer locationType;  
91 -  
92 @Schema(description = "设备位置的纬度", example = "45.000000") 85 @Schema(description = "设备位置的纬度", example = "45.000000")
93 private BigDecimal latitude; 86 private BigDecimal latitude;
94 87
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java
1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2
3 -import cn.iocoder.yudao.framework.common.validation.InEnum;  
4 -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;  
5 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.DecimalMax;
  5 +import jakarta.validation.constraints.DecimalMin;
6 import lombok.Data; 6 import lombok.Data;
7 7
8 import java.math.BigDecimal; 8 import java.math.BigDecimal;
@@ -39,14 +39,14 @@ public class IotDeviceSaveReqVO { @@ -39,14 +39,14 @@ public class IotDeviceSaveReqVO {
39 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") 39 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
40 private String config; 40 private String config;
41 41
42 - @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")  
43 - @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")  
44 - private Integer locationType;  
45 -  
46 - @Schema(description = "设备位置的纬度", example = "16380") 42 + @Schema(description = "设备位置的纬度", example = "39.915")
  43 + @DecimalMin(value = "-90", message = "纬度范围为 -90 到 90")
  44 + @DecimalMax(value = "90", message = "纬度范围为 -90 到 90")
47 private BigDecimal latitude; 45 private BigDecimal latitude;
48 46
49 - @Schema(description = "设备位置的经度", example = "16380") 47 + @Schema(description = "设备位置的经度", example = "116.404")
  48 + @DecimalMin(value = "-180", message = "经度范围为 -180 到 180")
  49 + @DecimalMax(value = "180", message = "经度范围为 -180 到 180")
50 private BigDecimal longitude; 50 private BigDecimal longitude;
51 51
52 } 52 }
53 \ No newline at end of file 53 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceUnbindGatewayReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.Set;
  9 +
  10 +@Schema(description = "管理后台 - IoT 设备解绑网关 Request VO")
  11 +@Data
  12 +public class IotDeviceUnbindGatewayReqVO {
  13 +
  14 + @Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
  15 + @NotEmpty(message = "子设备编号列表不能为空")
  16 + private Set<Long> subIds;
  17 +
  18 + @Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  19 + @NotNull(message = "网关设备编号不能为空")
  20 + private Long gatewayId;
  21 +
  22 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java
@@ -143,11 +143,13 @@ public class IotProductController { @@ -143,11 +143,13 @@ public class IotProductController {
143 143
144 @GetMapping("/simple-list") 144 @GetMapping("/simple-list")
145 @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项") 145 @Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
146 - public CommonResult<List<IotProductRespVO>> getProductSimpleList() {  
147 - List<IotProductDO> list = productService.getProductList();  
148 - return success(convertList(list, product -> // 只返回 id、name 字段 146 + @Parameter(name = "deviceType", description = "设备类型", example = "1")
  147 + public CommonResult<List<IotProductRespVO>> getProductSimpleList(
  148 + @RequestParam(value = "deviceType", required = false) Integer deviceType) {
  149 + List<IotProductDO> list = productService.getProductList(deviceType);
  150 + return success(convertList(list, product -> // 只返回 id、name、productKey 字段
149 new IotProductRespVO().setId(product.getId()).setName(product.getName()).setStatus(product.getStatus()) 151 new IotProductRespVO().setId(product.getId()).setName(product.getName()).setStatus(product.getStatus())
150 - .setDeviceType(product.getDeviceType()).setLocationType(product.getLocationType()))); 152 + .setDeviceType(product.getDeviceType()).setProductKey(product.getProductKey())));
151 } 153 }
152 154
153 } 155 }
154 \ No newline at end of file 156 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
@@ -27,6 +27,12 @@ public class IotProductRespVO { @@ -27,6 +27,12 @@ public class IotProductRespVO {
27 @ExcelProperty("产品标识") 27 @ExcelProperty("产品标识")
28 private String productKey; 28 private String productKey;
29 29
  30 + @Schema(description = "产品密钥", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private String productSecret;
  32 +
  33 + @Schema(description = "是否开启动态注册", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  34 + private Boolean registerEnabled;
  35 +
30 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 36 @Schema(description = "产品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
31 private Long categoryId; 37 private Long categoryId;
32 38
@@ -61,11 +67,6 @@ public class IotProductRespVO { @@ -61,11 +67,6 @@ public class IotProductRespVO {
61 @DictFormat(DictTypeConstants.NET_TYPE) 67 @DictFormat(DictTypeConstants.NET_TYPE)
62 private Integer netType; 68 private Integer netType;
63 69
64 - @Schema(description = "定位方式", example = "2")  
65 - @ExcelProperty(value = "定位方式", converter = DictConvert.class)  
66 - @DictFormat(DictTypeConstants.LOCATION_TYPE)  
67 - private Integer locationType;  
68 -  
69 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") 70 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
70 @ExcelProperty(value = "数据格式", converter = DictConvert.class) 71 @ExcelProperty(value = "数据格式", converter = DictConvert.class)
71 @DictFormat(DictTypeConstants.CODEC_TYPE) 72 @DictFormat(DictTypeConstants.CODEC_TYPE)
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; 1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
2 2
3 import cn.iocoder.yudao.framework.common.validation.InEnum; 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4 -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;  
5 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum; 4 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
6 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 5 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
7 import io.swagger.v3.oas.annotations.media.Schema; 6 import io.swagger.v3.oas.annotations.media.Schema;
@@ -45,12 +44,12 @@ public class IotProductSaveReqVO { @@ -45,12 +44,12 @@ public class IotProductSaveReqVO {
45 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}") 44 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
46 private Integer netType; 45 private Integer netType;
47 46
48 - @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")  
49 - @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")  
50 - private Integer locationType;  
51 -  
52 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") 47 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
53 @NotEmpty(message = "数据格式不能为空") 48 @NotEmpty(message = "数据格式不能为空")
54 private String codecType; 49 private String codecType;
55 50
  51 + @Schema(description = "是否开启动态注册", example = "false")
  52 + @NotNull(message = "是否开启动态注册不能为空")
  53 + private Boolean registerEnabled;
  54 +
56 } 55 }
57 \ No newline at end of file 56 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
@@ -123,19 +123,8 @@ public class IotDeviceDO extends TenantBaseDO { @@ -123,19 +123,8 @@ public class IotDeviceDO extends TenantBaseDO {
123 * 设备密钥,用于设备认证 123 * 设备密钥,用于设备认证
124 */ 124 */
125 private String deviceSecret; 125 private String deviceSecret;
126 - /**  
127 - * 认证类型(如一机一密、动态注册)  
128 - */  
129 - // TODO @haohao:是不是要枚举哈  
130 - private String authType;  
131 126
132 /** 127 /**
133 - * 定位方式  
134 - * <p>  
135 - * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}  
136 - */  
137 - private Integer locationType;  
138 - /**  
139 * 设备位置的纬度 128 * 设备位置的纬度
140 */ 129 */
141 private BigDecimal latitude; 130 private BigDecimal latitude;
@@ -143,16 +132,6 @@ public class IotDeviceDO extends TenantBaseDO { @@ -143,16 +132,6 @@ public class IotDeviceDO extends TenantBaseDO {
143 * 设备位置的经度 132 * 设备位置的经度
144 */ 133 */
145 private BigDecimal longitude; 134 private BigDecimal longitude;
146 - /**  
147 - * 地区编码  
148 - * <p>  
149 - * 关联 Area 的 id  
150 - */  
151 - private Integer areaId;  
152 - /**  
153 - * 设备详细地址  
154 - */  
155 - private String address;  
156 135
157 /** 136 /**
158 * 设备配置 137 * 设备配置
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
@@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO { @@ -33,6 +33,14 @@ public class IotProductDO extends TenantBaseDO {
33 */ 33 */
34 private String productKey; 34 private String productKey;
35 /** 35 /**
  36 + * 产品密钥,用于一型一密动态注册
  37 + */
  38 + private String productSecret;
  39 + /**
  40 + * 是否开启动态注册
  41 + */
  42 + private Boolean registerEnabled;
  43 + /**
36 * 产品分类编号 44 * 产品分类编号
37 * <p> 45 * <p>
38 * 关联 {@link IotProductCategoryDO#getId()} 46 * 关联 {@link IotProductCategoryDO#getId()}
@@ -70,12 +78,6 @@ public class IotProductDO extends TenantBaseDO { @@ -70,12 +78,6 @@ public class IotProductDO extends TenantBaseDO {
70 */ 78 */
71 private Integer netType; 79 private Integer netType;
72 /** 80 /**
73 - * 定位方式  
74 - * <p>  
75 - * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}  
76 - */  
77 - private Integer locationType;  
78 - /**  
79 * 数据格式(编解码器类型) 81 * 数据格式(编解码器类型)
80 * <p> 82 * <p>
81 * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE} 83 * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/device/IotDeviceMapper.java
@@ -6,7 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; @@ -6,7 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO; 7 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  9 +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
9 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 10 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  11 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
10 import jakarta.annotation.Nullable; 12 import jakarta.annotation.Nullable;
11 import org.apache.ibatis.annotations.Mapper; 13 import org.apache.ibatis.annotations.Mapper;
12 14
@@ -118,4 +120,56 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; { @@ -118,4 +120,56 @@ public interface IotDeviceMapper extends BaseMapperX&lt;IotDeviceDO&gt; {
118 )); 120 ));
119 } 121 }
120 122
  123 + /**
  124 + * 查询有位置信息的设备列表
  125 + *
  126 + * @return 设备列表
  127 + */
  128 + default List<IotDeviceDO> selectListByHasLocation() {
  129 + return selectList(new LambdaQueryWrapperX<IotDeviceDO>()
  130 + .isNotNull(IotDeviceDO::getLatitude)
  131 + .isNotNull(IotDeviceDO::getLongitude));
  132 + }
  133 +
  134 + // ========== 网关-子设备绑定相关 ==========
  135 +
  136 + /**
  137 + * 根据网关编号查询子设备列表
  138 + *
  139 + * @param gatewayId 网关设备编号
  140 + * @return 子设备列表
  141 + */
  142 + default List<IotDeviceDO> selectListByGatewayId(Long gatewayId) {
  143 + return selectList(IotDeviceDO::getGatewayId, gatewayId);
  144 + }
  145 +
  146 + /**
  147 + * 分页查询未绑定网关的子设备
  148 + *
  149 + * @param reqVO 分页查询参数
  150 + * @return 子设备分页
  151 + */
  152 + default PageResult<IotDeviceDO> selectUnboundSubDevicePage(IotDevicePageReqVO reqVO) {
  153 + return selectPage(reqVO, new LambdaQueryWrapperX<IotDeviceDO>()
  154 + .likeIfPresent(IotDeviceDO::getDeviceName, reqVO.getDeviceName())
  155 + .likeIfPresent(IotDeviceDO::getNickname, reqVO.getNickname())
  156 + .eqIfPresent(IotDeviceDO::getProductId, reqVO.getProductId())
  157 + // 仅查询子设备 + 未绑定网关
  158 + .eq(IotDeviceDO::getDeviceType, IotProductDeviceTypeEnum.GATEWAY_SUB.getType())
  159 + .isNull(IotDeviceDO::getGatewayId)
  160 + .orderByDesc(IotDeviceDO::getId));
  161 + }
  162 +
  163 + /**
  164 + * 批量更新设备的网关编号
  165 + *
  166 + * @param ids 设备编号列表
  167 + * @param gatewayId 网关设备编号(可以为 null,表示解绑)
  168 + */
  169 + default void updateGatewayIdBatch(Collection<Long> ids, Long gatewayId) {
  170 + update(null, new LambdaUpdateWrapper<IotDeviceDO>()
  171 + .set(IotDeviceDO::getGatewayId, gatewayId)
  172 + .in(IotDeviceDO::getId, ids));
  173 + }
  174 +
121 } 175 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/product/IotProductMapper.java
@@ -27,6 +27,12 @@ public interface IotProductMapper extends BaseMapperX&lt;IotProductDO&gt; { @@ -27,6 +27,12 @@ public interface IotProductMapper extends BaseMapperX&lt;IotProductDO&gt; {
27 .orderByDesc(IotProductDO::getId)); 27 .orderByDesc(IotProductDO::getId));
28 } 28 }
29 29
  30 + default List<IotProductDO> selectList(Integer deviceType) {
  31 + return selectList(new LambdaQueryWrapperX<IotProductDO>()
  32 + .eqIfPresent(IotProductDO::getDeviceType, deviceType)
  33 + .orderByDesc(IotProductDO::getId));
  34 + }
  35 +
30 default IotProductDO selectByProductKey(String productKey) { 36 default IotProductDO selectByProductKey(String productKey) {
31 return selectOne(new LambdaQueryWrapper<IotProductDO>() 37 return selectOne(new LambdaQueryWrapper<IotProductDO>()
32 .apply("LOWER(product_key) = {0}", productKey.toLowerCase())); 38 .apply("LOWER(product_key) = {0}", productKey.toLowerCase()));
@@ -37,5 +43,4 @@ public interface IotProductMapper extends BaseMapperX&lt;IotProductDO&gt; { @@ -37,5 +43,4 @@ public interface IotProductMapper extends BaseMapperX&lt;IotProductDO&gt; {
37 .geIfPresent(IotProductDO::getCreateTime, createTime)); 43 .geIfPresent(IotProductDO::getCreateTime, createTime));
38 } 44 }
39 45
40 -  
41 } 46 }
42 \ No newline at end of file 47 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
@@ -84,4 +84,12 @@ public interface RedisKeyConstants { @@ -84,4 +84,12 @@ public interface RedisKeyConstants {
84 */ 84 */
85 String SCENE_RULE_LIST = "iot:scene_rule_list"; 85 String SCENE_RULE_LIST = "iot:scene_rule_list";
86 86
  87 + /**
  88 + * WebSocket 连接分布式锁
  89 + * <p>
  90 + * KEY 格式:websocket_connect_lock:${serverUrl}
  91 + * 用于保证 WebSocket 重连操作的线程安全
  92 + */
  93 + String WEBSOCKET_CONNECT_LOCK = "iot:websocket_connect_lock:%s";
  94 +
87 } 95 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
@@ -26,13 +26,26 @@ public interface ErrorCodeConstants { @@ -26,13 +26,26 @@ public interface ErrorCodeConstants {
26 // ========== 设备 1-050-003-000 ============ 26 // ========== 设备 1-050-003-000 ============
27 ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在"); 27 ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
28 ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一"); 28 ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
29 - ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除"); 29 + ErrorCode DEVICE_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除");
30 ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在"); 30 ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
31 ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在"); 31 ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
32 ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备"); 32 ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
33 ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); 33 ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
34 ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); 34 ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
35 ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); 35 ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
  36 + ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
  37 + ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
  38 + // 拓扑管理相关错误码 1-050-003-100
  39 + ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
  40 + ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
  41 + ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
  42 + ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
  43 + // 设备注册相关错误码 1-050-003-200
  44 + ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
  45 + ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
  46 + ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
  47 + ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
  48 + ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
36 49
37 // ========== 产品分类 1-050-004-000 ========== 50 // ========== 产品分类 1-050-004-000 ==========
38 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); 51 ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java deleted
1 -package cn.iocoder.yudao.module.iot.enums.device;  
2 -  
3 -import lombok.Getter;  
4 -import lombok.RequiredArgsConstructor;  
5 -  
6 -// TODO @芋艿:需要添加对应的 DTO,以及上下行的链路,网关、网关服务、设备等  
7 -/**  
8 - * IoT 设备消息标识符枚举  
9 - */  
10 -@Deprecated  
11 -@Getter  
12 -@RequiredArgsConstructor  
13 -public enum IotDeviceMessageIdentifierEnum {  
14 -  
15 - PROPERTY_GET("get"), // 下行  
16 - PROPERTY_SET("set"), // 下行  
17 - PROPERTY_REPORT("report"), // 上行  
18 -  
19 - STATE_ONLINE("online"), // 上行  
20 - STATE_OFFLINE("offline"), // 上行  
21 -  
22 - CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景  
23 - CONFIG_SET("set"), // 下行  
24 -  
25 - SERVICE_INVOKE("${identifier}"), // 下行  
26 - SERVICE_REPLY_SUFFIX("_reply"), // 芋艿:TODO 芋艿:【讨论】上行 or 下行  
27 -  
28 - OTA_UPGRADE("upgrade"), // 下行  
29 - OTA_PULL("pull"), // 上行  
30 - OTA_PROGRESS("progress"), // 上行  
31 - OTA_REPORT("report"), // 上行  
32 -  
33 - REGISTER_REGISTER("register"), // 上行  
34 - REGISTER_REGISTER_SUB("register_sub"), // 上行  
35 - REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行  
36 -  
37 - TOPOLOGY_ADD("topology_add"), // 下行;  
38 - ;  
39 -  
40 - /**  
41 - * 标志符  
42 - */  
43 - private final String identifier;  
44 -  
45 -}  
46 \ No newline at end of file 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java deleted
1 -package cn.iocoder.yudao.module.iot.enums.device;  
2 -  
3 -import cn.iocoder.yudao.framework.common.core.ArrayValuable;  
4 -import lombok.Getter;  
5 -import lombok.RequiredArgsConstructor;  
6 -  
7 -import java.util.Arrays;  
8 -  
9 -/**  
10 - * IoT 设备消息类型枚举  
11 - */  
12 -@Deprecated  
13 -@Getter  
14 -@RequiredArgsConstructor  
15 -public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {  
16 -  
17 - STATE("state"), // 设备状态  
18 - PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
19 - EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
20 - SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
21 - CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置  
22 - OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级  
23 - REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册  
24 - TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑  
25 -  
26 - public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);  
27 -  
28 - /**  
29 - * 属性  
30 - */  
31 - private final String type;  
32 -  
33 - @Override  
34 - public String[] array() {  
35 - return ARRAYS;  
36 - }  
37 -  
38 -}  
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java deleted
1 -package cn.iocoder.yudao.module.iot.enums.product;  
2 -  
3 -import cn.iocoder.yudao.framework.common.core.ArrayValuable;  
4 -import lombok.AllArgsConstructor;  
5 -import lombok.Getter;  
6 -  
7 -import java.util.Arrays;  
8 -  
9 -/**  
10 - * IoT 定位方式枚举类  
11 - *  
12 - * @author alwayssuper  
13 - */  
14 -@AllArgsConstructor  
15 -@Getter  
16 -public enum IotLocationTypeEnum implements ArrayValuable<Integer> {  
17 -  
18 - IP(1, "IP 定位"),  
19 - DEVICE(2, "设备上报"),  
20 - MANUAL(3, "手动定位");  
21 -  
22 - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);  
23 -  
24 - /**  
25 - * 类型  
26 - */  
27 - private final Integer type;  
28 - /**  
29 - * 描述  
30 - */  
31 - private final String description;  
32 -  
33 - @Override  
34 - public Integer[] array() {  
35 - return ARRAYS;  
36 - }  
37 -  
38 -}  
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
@@ -3,11 +3,19 @@ package cn.iocoder.yudao.module.iot.service.device; @@ -3,11 +3,19 @@ package cn.iocoder.yudao.module.iot.service.device;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  8 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  9 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  11 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  12 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  13 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
8 import jakarta.validation.Valid; 15 import jakarta.validation.Valid;
9 16
10 import javax.annotation.Nullable; 17 import javax.annotation.Nullable;
  18 +import java.math.BigDecimal;
11 import java.time.LocalDateTime; 19 import java.time.LocalDateTime;
12 import java.util.Collection; 20 import java.util.Collection;
13 import java.util.List; 21 import java.util.List;
@@ -37,18 +45,6 @@ public interface IotDeviceService { @@ -37,18 +45,6 @@ public interface IotDeviceService {
37 */ 45 */
38 void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO); 46 void updateDevice(@Valid IotDeviceSaveReqVO updateReqVO);
39 47
40 - // TODO @芋艿:先这么实现。未来看情况,要不要自己实现  
41 -  
42 - /**  
43 - * 更新设备的所属网关  
44 - *  
45 - * @param id 编号  
46 - * @param gatewayId 网关设备 ID  
47 - */  
48 - default void updateDeviceGateway(Long id, Long gatewayId) {  
49 - updateDevice(new IotDeviceSaveReqVO().setId(id).setGatewayId(gatewayId));  
50 - }  
51 -  
52 /** 48 /**
53 * 更新设备状态 49 * 更新设备状态
54 * 50 *
@@ -271,4 +267,112 @@ public interface IotDeviceService { @@ -271,4 +267,112 @@ public interface IotDeviceService {
271 */ 267 */
272 void updateDeviceFirmware(Long deviceId, Long firmwareId); 268 void updateDeviceFirmware(Long deviceId, Long firmwareId);
273 269
  270 + /**
  271 + * 更新设备定位信息(GeoLocation 上报时调用)
  272 + *
  273 + * @param device 设备信息(用于清除缓存)
  274 + * @param longitude 经度
  275 + * @param latitude 纬度
  276 + */
  277 + void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude);
  278 +
  279 + /**
  280 + * 获得有位置信息的设备列表
  281 + *
  282 + * @return 设备列表
  283 + */
  284 + List<IotDeviceDO> getDeviceListByHasLocation();
  285 +
  286 + // ========== 网关-拓扑管理(后台操作) ==========
  287 +
  288 + /**
  289 + * 绑定子设备到网关
  290 + *
  291 + * @param subIds 子设备编号列表
  292 + * @param gatewayId 网关设备编号
  293 + */
  294 + void bindDeviceGateway(Collection<Long> subIds, Long gatewayId);
  295 +
  296 + /**
  297 + * 解绑子设备与网关
  298 + *
  299 + * @param subIds 子设备编号列表
  300 + * @param gatewayId 网关设备编号
  301 + */
  302 + void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId);
  303 +
  304 + /**
  305 + * 获取未绑定网关的子设备分页
  306 + *
  307 + * @param pageReqVO 分页查询参数(仅使用 productId、deviceName、nickname)
  308 + * @return 子设备分页
  309 + */
  310 + PageResult<IotDeviceDO> getUnboundSubDevicePage(IotDevicePageReqVO pageReqVO);
  311 +
  312 + /**
  313 + * 根据网关编号获取子设备列表
  314 + *
  315 + * @param gatewayId 网关设备编号
  316 + * @return 子设备列表
  317 + */
  318 + List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId);
  319 +
  320 + // ========== 网关-拓扑管理(设备上报) ==========
  321 +
  322 + /**
  323 + * 处理添加拓扑关系消息(网关设备上报)
  324 + *
  325 + * @param message 消息
  326 + * @param gatewayDevice 网关设备
  327 + * @return 成功添加的子设备列表
  328 + */
  329 + List<IotDeviceIdentity> handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  330 +
  331 + /**
  332 + * 处理删除拓扑关系消息(网关设备上报)
  333 + *
  334 + * @param message 消息
  335 + * @param gatewayDevice 网关设备
  336 + * @return 成功删除的子设备列表
  337 + */
  338 + List<IotDeviceIdentity> handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  339 +
  340 + /**
  341 + * 处理获取拓扑关系消息(网关设备上报)
  342 + *
  343 + * @param gatewayDevice 网关设备
  344 + * @return 拓扑关系响应
  345 + */
  346 + IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice);
  347 +
  348 + // ========== 设备动态注册 ==========
  349 +
  350 + /**
  351 + * 直连/网关设备动态注册
  352 + *
  353 + * @param reqDTO 动态注册请求
  354 + * @return 注册结果(包含 DeviceSecret)
  355 + */
  356 + IotDeviceRegisterRespDTO registerDevice(@Valid IotDeviceRegisterReqDTO reqDTO);
  357 +
  358 + /**
  359 + * 网关子设备动态注册
  360 + * <p>
  361 + * 与 {@link #handleSubDeviceRegisterMessage} 方法的区别:
  362 + * 该方法网关设备信息通过 reqDTO 参数传入,而 {@link #handleSubDeviceRegisterMessage} 方法通过 gatewayDevice 参数传入
  363 + *
  364 + * @param reqDTO 子设备注册请求(包含网关设备信息)
  365 + * @return 注册结果列表
  366 + */
  367 + List<IotSubDeviceRegisterRespDTO> registerSubDevices(@Valid IotSubDeviceRegisterFullReqDTO reqDTO);
  368 +
  369 + /**
  370 + * 处理子设备动态注册消息(网关设备上报)
  371 + *
  372 + * @param message 消息
  373 + * @param gatewayDevice 网关设备
  374 + * @return 注册结果列表
  375 + */
  376 + List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice);
  377 +
274 } 378 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.device; 1 package cn.iocoder.yudao.module.iot.service.device;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.core.util.IdUtil; 5 import cn.hutool.core.util.IdUtil;
5 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
6 import cn.hutool.core.util.StrUtil; 7 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.exception.ServiceException; 9 import cn.iocoder.yudao.framework.common.exception.ServiceException;
9 import cn.iocoder.yudao.framework.common.pojo.PageResult; 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
11 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; 13 import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
12 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 14 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
13 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 15 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
14 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*; 16 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
15 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 17 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  18 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  19 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
16 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 20 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  21 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  22 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  23 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  25 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  26 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  27 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  28 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoChangeReqDTO;
  29 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  30 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoGetRespDTO;
17 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 31 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
18 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 32 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
19 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO; 33 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceGroupDO;
@@ -21,6 +35,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -21,6 +35,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
21 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper; 35 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceMapper;
22 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; 36 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
23 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; 37 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
  38 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
24 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 39 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
25 import jakarta.annotation.Resource; 40 import jakarta.annotation.Resource;
26 import jakarta.validation.ConstraintViolationException; 41 import jakarta.validation.ConstraintViolationException;
@@ -34,12 +49,14 @@ import org.springframework.transaction.annotation.Transactional; @@ -34,12 +49,14 @@ import org.springframework.transaction.annotation.Transactional;
34 import org.springframework.validation.annotation.Validated; 49 import org.springframework.validation.annotation.Validated;
35 50
36 import javax.annotation.Nullable; 51 import javax.annotation.Nullable;
  52 +import java.math.BigDecimal;
37 import java.time.LocalDateTime; 53 import java.time.LocalDateTime;
38 import java.util.*; 54 import java.util.*;
39 55
40 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 56 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
41 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 57 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
42 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; 58 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  59 +import static java.util.Collections.singletonList;
43 60
44 /** 61 /**
45 * IoT 设备 Service 实现类 62 * IoT 设备 Service 实现类
@@ -60,9 +77,20 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -60,9 +77,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
60 @Resource 77 @Resource
61 @Lazy // 延迟加载,解决循环依赖 78 @Lazy // 延迟加载,解决循环依赖
62 private IotDeviceGroupService deviceGroupService; 79 private IotDeviceGroupService deviceGroupService;
  80 + @Resource
  81 + @Lazy // 延迟加载,解决循环依赖
  82 + private IotDeviceMessageService deviceMessageService;
  83 +
  84 + private IotDeviceServiceImpl getSelf() {
  85 + return SpringUtil.getBean(getClass());
  86 + }
63 87
64 @Override 88 @Override
65 public Long createDevice(IotDeviceSaveReqVO createReqVO) { 89 public Long createDevice(IotDeviceSaveReqVO createReqVO) {
  90 + return createDevice0(createReqVO).getId();
  91 + }
  92 +
  93 + private IotDeviceDO createDevice0(IotDeviceSaveReqVO createReqVO) {
66 // 1.1 校验产品是否存在 94 // 1.1 校验产品是否存在
67 IotProductDO product = productService.getProduct(createReqVO.getProductId()); 95 IotProductDO product = productService.getProduct(createReqVO.getProductId());
68 if (product == null) { 96 if (product == null) {
@@ -80,7 +108,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -80,7 +108,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
80 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class); 108 IotDeviceDO device = BeanUtils.toBean(createReqVO, IotDeviceDO.class);
81 initDevice(device, product); 109 initDevice(device, product);
82 deviceMapper.insert(device); 110 deviceMapper.insert(device);
83 - return device.getId(); 111 + return device;
84 } 112 }
85 113
86 private void validateCreateDeviceParam(String productKey, String deviceName, 114 private void validateCreateDeviceParam(String productKey, String deviceName,
@@ -116,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -116,11 +144,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
116 144
117 private void initDevice(IotDeviceDO device, IotProductDO product) { 145 private void initDevice(IotDeviceDO device, IotProductDO product) {
118 device.setProductId(product.getId()).setProductKey(product.getProductKey()) 146 device.setProductId(product.getId()).setProductKey(product.getProductKey())
119 - .setDeviceType(product.getDeviceType());  
120 - // 生成密钥  
121 - device.setDeviceSecret(generateDeviceSecret());  
122 - // 设置设备状态为未激活  
123 - device.setState(IotDeviceStateEnum.INACTIVE.getState()); 147 + .setDeviceType(product.getDeviceType())
  148 + .setDeviceSecret(generateDeviceSecret()) // 生成密钥
  149 + .setState(IotDeviceStateEnum.INACTIVE.getState()); // 默认未激活
  150 + }
  151 +
  152 + private String generateDeviceSecret() {
  153 + return IdUtil.fastSimpleUUID();
124 } 154 }
125 155
126 @Override 156 @Override
@@ -169,9 +199,10 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -169,9 +199,10 @@ public class IotDeviceServiceImpl implements IotDeviceService {
169 public void deleteDevice(Long id) { 199 public void deleteDevice(Long id) {
170 // 1.1 校验存在 200 // 1.1 校验存在
171 IotDeviceDO device = validateDeviceExists(id); 201 IotDeviceDO device = validateDeviceExists(id);
172 - // 1.2 如果是网关设备,检查是否有子设备  
173 - if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(id) > 0) {  
174 - throw exception(DEVICE_HAS_CHILDREN); 202 + // 1.2 如果是网关设备,检查是否有子设备绑定
  203 + if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType())
  204 + && deviceMapper.selectCountByGatewayId(id) > 0) {
  205 + throw exception(DEVICE_GATEWAY_HAS_SUB);
175 } 206 }
176 207
177 // 2. 删除设备 208 // 2. 删除设备
@@ -192,10 +223,11 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -192,10 +223,11 @@ public class IotDeviceServiceImpl implements IotDeviceService {
192 if (CollUtil.isEmpty(devices)) { 223 if (CollUtil.isEmpty(devices)) {
193 return; 224 return;
194 } 225 }
195 - // 1.2 校验网关设备是否存在 226 + // 1.2 如果是网关设备,检查是否有子设备绑定
196 for (IotDeviceDO device : devices) { 227 for (IotDeviceDO device : devices) {
197 - if (device.getGatewayId() != null && deviceMapper.selectCountByGatewayId(device.getId()) > 0) {  
198 - throw exception(DEVICE_HAS_CHILDREN); 228 + if (IotProductDeviceTypeEnum.isGateway(device.getDeviceType())
  229 + && deviceMapper.selectCountByGatewayId(device.getId()) > 0) {
  230 + throw exception(DEVICE_GATEWAY_HAS_SUB);
199 } 231 }
200 } 232 }
201 233
@@ -295,6 +327,37 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -295,6 +327,37 @@ public class IotDeviceServiceImpl implements IotDeviceService {
295 327
296 // 2. 清空对应缓存 328 // 2. 清空对应缓存
297 deleteDeviceCache(device); 329 deleteDeviceCache(device);
  330 +
  331 + // 3. 网关设备下线时,联动所有子设备下线
  332 + if (Objects.equals(state, IotDeviceStateEnum.OFFLINE.getState())
  333 + && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
  334 + handleGatewayOffline(device);
  335 + }
  336 + }
  337 +
  338 + /**
  339 + * 处理网关下线,联动所有子设备下线
  340 + *
  341 + * @param gatewayDevice 网关设备
  342 + */
  343 + private void handleGatewayOffline(IotDeviceDO gatewayDevice) {
  344 + List<IotDeviceDO> subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
  345 + if (CollUtil.isEmpty(subDevices)) {
  346 + return;
  347 + }
  348 + for (IotDeviceDO subDevice : subDevices) {
  349 + if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  350 + try {
  351 + updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  352 + log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]",
  353 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  354 + subDevice.getProductKey(), subDevice.getDeviceName());
  355 + } catch (Exception ex) {
  356 + log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]",
  357 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  358 + }
  359 + }
  360 + }
298 } 361 }
299 362
300 @Override 363 @Override
@@ -315,15 +378,6 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -315,15 +378,6 @@ public class IotDeviceServiceImpl implements IotDeviceService {
315 return deviceMapper.selectCountByGroupId(groupId); 378 return deviceMapper.selectCountByGroupId(groupId);
316 } 379 }
317 380
318 - /**  
319 - * 生成 deviceSecret  
320 - *  
321 - * @return 生成的 deviceSecret  
322 - */  
323 - private String generateDeviceSecret() {  
324 - return IdUtil.fastSimpleUUID();  
325 - }  
326 -  
327 @Override 381 @Override
328 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 382 @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
329 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) { 383 public IotDeviceImportRespVO importDevice(List<IotDeviceImportExcelVO> importDevices, boolean updateSupport) {
@@ -376,8 +430,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -376,8 +430,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
376 if (existDevice == null) { 430 if (existDevice == null) {
377 createDevice(new IotDeviceSaveReqVO() 431 createDevice(new IotDeviceSaveReqVO()
378 .setDeviceName(importDevice.getDeviceName()) 432 .setDeviceName(importDevice.getDeviceName())
379 - .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)  
380 - .setLocationType(importDevice.getLocationType())); 433 + .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds));
381 respVO.getCreateDeviceNames().add(importDevice.getDeviceName()); 434 respVO.getCreateDeviceNames().add(importDevice.getDeviceName());
382 return; 435 return;
383 } 436 }
@@ -386,7 +439,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -386,7 +439,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
386 throw exception(DEVICE_KEY_EXISTS); 439 throw exception(DEVICE_KEY_EXISTS);
387 } 440 }
388 updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId()) 441 updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId())
389 - .setGatewayId(gatewayId).setGroupIds(groupIds).setLocationType(importDevice.getLocationType())); 442 + .setGatewayId(gatewayId).setGroupIds(groupIds));
390 respVO.getUpdateDeviceNames().add(importDevice.getDeviceName()); 443 respVO.getUpdateDeviceNames().add(importDevice.getDeviceName());
391 } catch (ServiceException ex) { 444 } catch (ServiceException ex) {
392 respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage()); 445 respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage());
@@ -399,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -399,7 +452,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
399 public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) { 452 public IotDeviceAuthInfoRespVO getDeviceAuthInfo(Long id) {
400 IotDeviceDO device = validateDeviceExists(id); 453 IotDeviceDO device = validateDeviceExists(id);
401 // 使用 IotDeviceAuthUtils 生成认证信息 454 // 使用 IotDeviceAuthUtils 生成认证信息
402 - IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo( 455 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(
403 device.getProductKey(), device.getDeviceName(), device.getDeviceSecret()); 456 device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
404 return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class); 457 return BeanUtils.toBean(authInfo, IotDeviceAuthInfoRespVO.class);
405 } 458 }
@@ -447,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -447,7 +500,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
447 @Override 500 @Override
448 public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) { 501 public boolean authDevice(IotDeviceAuthReqDTO authReqDTO) {
449 // 1. 校验设备是否存在 502 // 1. 校验设备是否存在
450 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername()); 503 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(authReqDTO.getUsername());
451 if (deviceInfo == null) { 504 if (deviceInfo == null) {
452 log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername()); 505 log.error("[authDevice][认证失败,username({}) 格式不正确]", authReqDTO.getUsername());
453 return false; 506 return false;
@@ -461,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -461,7 +514,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
461 } 514 }
462 515
463 // 2. 校验密码 516 // 2. 校验密码
464 - IotDeviceAuthUtils.AuthInfo authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret()); 517 + IotDeviceAuthReqDTO authInfo = IotDeviceAuthUtils.getAuthInfo(productKey, deviceName, device.getDeviceSecret());
465 if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) { 518 if (ObjUtil.notEqual(authInfo.getPassword(), authReqDTO.getPassword())) {
466 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); 519 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
467 return false; 520 return false;
@@ -490,17 +543,388 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -490,17 +543,388 @@ public class IotDeviceServiceImpl implements IotDeviceService {
490 public void updateDeviceFirmware(Long deviceId, Long firmwareId) { 543 public void updateDeviceFirmware(Long deviceId, Long firmwareId) {
491 // 1. 校验设备是否存在 544 // 1. 校验设备是否存在
492 IotDeviceDO device = validateDeviceExists(deviceId); 545 IotDeviceDO device = validateDeviceExists(deviceId);
493 - 546 +
494 // 2. 更新设备固件版本 547 // 2. 更新设备固件版本
495 IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId); 548 IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId);
496 deviceMapper.updateById(updateObj); 549 deviceMapper.updateById(updateObj);
497 - 550 +
498 // 3. 清空对应缓存 551 // 3. 清空对应缓存
499 deleteDeviceCache(device); 552 deleteDeviceCache(device);
500 } 553 }
501 554
502 - private IotDeviceServiceImpl getSelf() {  
503 - return SpringUtil.getBean(getClass()); 555 + @Override
  556 + public void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude) {
  557 + // 1. 更新定位信息
  558 + deviceMapper.updateById(new IotDeviceDO().setId(device.getId())
  559 + .setLongitude(longitude).setLatitude(latitude));
  560 +
  561 + // 2. 清空对应缓存
  562 + deleteDeviceCache(device);
  563 + }
  564 +
  565 + @Override
  566 + public List<IotDeviceDO> getDeviceListByHasLocation() {
  567 + return deviceMapper.selectListByHasLocation();
  568 + }
  569 +
  570 + // ========== 网关-拓扑管理(后台操作) ==========
  571 +
  572 + @Override
  573 + @Transactional(rollbackFor = Exception.class)
  574 + public void bindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
  575 + if (CollUtil.isEmpty(subIds)) {
  576 + return;
  577 + }
  578 + // 1.1 校验网关设备存在且类型正确
  579 + validateGatewayDeviceExists(gatewayId);
  580 + // 1.2 校验每个设备是否可绑定
  581 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
  582 + for (IotDeviceDO device : devices) {
  583 + checkSubDeviceCanBind(device, gatewayId);
  584 + }
  585 +
  586 + // 2. 批量更新数据库
  587 + List<IotDeviceDO> updateList = convertList(devices, device ->
  588 + new IotDeviceDO().setId(device.getId()).setGatewayId(gatewayId));
  589 + deviceMapper.updateBatch(updateList);
  590 +
  591 + // 3. 清空对应缓存
  592 + deleteDeviceCache(devices);
  593 +
  594 + // 4. 下发网关设备拓扑变更通知(增加)
  595 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_CREATE, devices);
  596 + }
  597 +
  598 + private void checkSubDeviceCanBind(IotDeviceDO device, Long gatewayId) {
  599 + if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {
  600 + throw exception(DEVICE_NOT_GATEWAY_SUB, device.getProductKey(), device.getDeviceName());
  601 + }
  602 + // 已绑定到其他网关,拒绝绑定(需先解绑)
  603 + if (device.getGatewayId() != null && ObjUtil.notEqual(device.getGatewayId(), gatewayId)) {
  604 + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, device.getProductKey(), device.getDeviceName());
  605 + }
  606 + }
  607 +
  608 + @Override
  609 + @Transactional(rollbackFor = Exception.class)
  610 + public void unbindDeviceGateway(Collection<Long> subIds, Long gatewayId) {
  611 + // 1. 校验设备存在
  612 + if (CollUtil.isEmpty(subIds)) {
  613 + return;
  614 + }
  615 + List<IotDeviceDO> devices = deviceMapper.selectByIds(subIds);
  616 + devices.removeIf(device -> ObjUtil.notEqual(device.getGatewayId(), gatewayId));
  617 + if (CollUtil.isEmpty(devices)) {
  618 + return;
  619 + }
  620 +
  621 + // 2. 批量更新数据库(将 gatewayId 设置为 null)
  622 + deviceMapper.updateGatewayIdBatch(convertList(devices, IotDeviceDO::getId), null);
  623 +
  624 + // 3. 清空对应缓存
  625 + deleteDeviceCache(devices);
  626 +
  627 + // 4. 下发网关设备拓扑变更通知(删除)
  628 + sendTopoChangeNotify(gatewayId, IotDeviceTopoChangeReqDTO.STATUS_DELETE, devices);
  629 + }
  630 +
  631 + @Override
  632 + public PageResult<IotDeviceDO> getUnboundSubDevicePage(IotDevicePageReqVO pageReqVO) {
  633 + return deviceMapper.selectUnboundSubDevicePage(pageReqVO);
  634 + }
  635 +
  636 + @Override
  637 + public List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId) {
  638 + return deviceMapper.selectListByGatewayId(gatewayId);
  639 + }
  640 +
  641 + // ========== 网关-拓扑管理(设备上报) ==========
  642 +
  643 + @Override
  644 + public List<IotDeviceIdentity> handleTopoAddMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  645 + // 1.1 校验网关设备类型
  646 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  647 + throw exception(DEVICE_NOT_GATEWAY);
  648 + }
  649 + // 1.2 解析参数
  650 + IotDeviceTopoAddReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoAddReqDTO.class);
  651 + if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
  652 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  653 + }
  654 +
  655 + // 2. 遍历处理每个子设备
  656 + List<IotDeviceIdentity> addedSubDevices = new ArrayList<>();
  657 + for (IotDeviceAuthReqDTO subDeviceAuth : params.getSubDevices()) {
  658 + try {
  659 + IotDeviceDO subDevice = addDeviceTopo(gatewayDevice, subDeviceAuth);
  660 + addedSubDevices.add(new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  661 + } catch (Exception ex) {
  662 + log.warn("[handleTopoAddMessage][网关({}/{}) 添加子设备失败,message={}]",
  663 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), message, ex);
  664 + }
  665 + }
  666 +
  667 + // 3. 返回响应数据(包含成功添加的子设备列表)
  668 + return addedSubDevices;
  669 + }
  670 +
  671 + private IotDeviceDO addDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceAuthReqDTO subDeviceAuth) {
  672 + // 1.1 解析子设备信息
  673 + IotDeviceIdentity subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
  674 + if (subDeviceInfo == null) {
  675 + throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
  676 + }
  677 + // 1.2 校验子设备认证信息
  678 + if (!authDevice(subDeviceAuth)) {
  679 + throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED);
  680 + }
  681 + // 1.3 获取子设备
  682 + IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
  683 + if (subDevice == null) {
  684 + throw exception(DEVICE_NOT_EXISTS);
  685 + }
  686 + // 1.4 校验子设备类型
  687 + checkSubDeviceCanBind(subDevice, gatewayDevice.getId());
  688 +
  689 + // 2. 更新数据库
  690 + deviceMapper.updateById(new IotDeviceDO().setId(subDevice.getId()).setGatewayId(gatewayDevice.getId()));
  691 + log.info("[addDeviceTopo][网关({}/{}) 绑定子设备({}/{})]",
  692 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  693 + subDevice.getProductKey(), subDevice.getDeviceName());
  694 +
  695 + // 3. 清空对应缓存
  696 + deleteDeviceCache(subDevice);
  697 + return subDevice;
  698 + }
  699 +
  700 + @Override
  701 + public List<IotDeviceIdentity> handleTopoDeleteMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  702 + // 1.1 校验网关设备类型
  703 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  704 + throw exception(DEVICE_NOT_GATEWAY);
  705 + }
  706 + // 1.2 解析参数
  707 + IotDeviceTopoDeleteReqDTO params = JsonUtils.convertObject(message.getParams(), IotDeviceTopoDeleteReqDTO.class);
  708 + if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
  709 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  710 + }
  711 +
  712 + // 2. 遍历处理每个子设备
  713 + List<IotDeviceIdentity> deletedSubDevices = new ArrayList<>();
  714 + for (IotDeviceIdentity subDeviceIdentity : params.getSubDevices()) {
  715 + try {
  716 + deleteDeviceTopo(gatewayDevice, subDeviceIdentity);
  717 + deletedSubDevices.add(subDeviceIdentity);
  718 + } catch (Exception ex) {
  719 + log.warn("[handleTopoDeleteMessage][网关({}/{}) 删除子设备失败,productKey={}, deviceName={}]",
  720 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  721 + subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName(), ex);
  722 + }
  723 + }
  724 +
  725 + // 3. 返回响应数据(包含成功删除的子设备列表)
  726 + return deletedSubDevices;
  727 + }
  728 +
  729 + private void deleteDeviceTopo(IotDeviceDO gatewayDevice, IotDeviceIdentity subDeviceIdentity) {
  730 + // 1.1 获取子设备
  731 + IotDeviceDO subDevice = getSelf().getDeviceFromCache(subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
  732 + if (subDevice == null) {
  733 + throw exception(DEVICE_NOT_EXISTS);
  734 + }
  735 + // 1.2 校验子设备是否绑定到该网关
  736 + if (ObjUtil.notEqual(subDevice.getGatewayId(), gatewayDevice.getId())) {
  737 + throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY,
  738 + subDeviceIdentity.getProductKey(), subDeviceIdentity.getDeviceName());
  739 + }
  740 +
  741 + // 2. 更新数据库(将 gatewayId 设置为 null)
  742 + deviceMapper.updateGatewayIdBatch(singletonList(subDevice.getId()), null);
  743 + log.info("[deleteDeviceTopo][网关({}/{}) 解绑子设备({}/{})]",
  744 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  745 + subDevice.getProductKey(), subDevice.getDeviceName());
  746 +
  747 + // 3. 清空对应缓存
  748 + deleteDeviceCache(subDevice);
  749 +
  750 + // 4. 子设备下线
  751 + if (Objects.equals(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  752 + updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  753 + }
  754 + }
  755 +
  756 + @Override
  757 + public IotDeviceTopoGetRespDTO handleTopoGetMessage(IotDeviceDO gatewayDevice) {
  758 + // 1. 校验网关设备类型
  759 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  760 + throw exception(DEVICE_NOT_GATEWAY);
  761 + }
  762 +
  763 + // 2. 获取子设备列表并转换
  764 + List<IotDeviceDO> subDevices = deviceMapper.selectListByGatewayId(gatewayDevice.getId());
  765 + List<IotDeviceIdentity> subDeviceIdentities = convertList(subDevices, subDevice ->
  766 + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  767 + return new IotDeviceTopoGetRespDTO().setSubDevices(subDeviceIdentities);
  768 + }
  769 +
  770 + /**
  771 + * 发送拓扑变更通知给网关设备
  772 + *
  773 + * @param gatewayId 网关设备编号
  774 + * @param status 变更状态(0-创建, 1-删除)
  775 + * @param subDevices 子设备列表
  776 + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
  777 + */
  778 + private void sendTopoChangeNotify(Long gatewayId, Integer status, List<IotDeviceDO> subDevices) {
  779 + if (CollUtil.isEmpty(subDevices)) {
  780 + return;
  781 + }
  782 + // 1. 获取网关设备
  783 + IotDeviceDO gatewayDevice = deviceMapper.selectById(gatewayId);
  784 + if (gatewayDevice == null) {
  785 + log.warn("[sendTopoChangeNotify][网关设备({}) 不存在,无法发送拓扑变更通知]", gatewayId);
  786 + return;
  787 + }
  788 +
  789 + try {
  790 + // 2.1 构建拓扑变更通知消息
  791 + List<IotDeviceIdentity> subList = convertList(subDevices, subDevice ->
  792 + new IotDeviceIdentity(subDevice.getProductKey(), subDevice.getDeviceName()));
  793 + IotDeviceTopoChangeReqDTO params = new IotDeviceTopoChangeReqDTO(status, subList);
  794 + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(
  795 + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);
  796 +
  797 + // 2.2 发送消息
  798 + deviceMessageService.sendDeviceMessage(notifyMessage, gatewayDevice);
  799 + log.info("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知成功,status={}, subDevices={}]",
  800 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  801 + status, subList);
  802 + } catch (Exception ex) {
  803 + log.error("[sendTopoChangeNotify][网关({}/{}) 发送拓扑变更通知失败,status={}]",
  804 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), status, ex);
  805 + }
  806 + }
  807 +
  808 + // ========== 设备动态注册 ==========
  809 +
  810 + @Override
  811 + public IotDeviceRegisterRespDTO registerDevice(IotDeviceRegisterReqDTO reqDTO) {
  812 + // 1.1 校验产品
  813 + IotProductDO product = TenantUtils.executeIgnore(() ->
  814 + productService.getProductByProductKey(reqDTO.getProductKey()));
  815 + if (product == null) {
  816 + throw exception(PRODUCT_NOT_EXISTS);
  817 + }
  818 + // 1.2 校验产品是否开启动态注册
  819 + if (BooleanUtil.isFalse(product.getRegisterEnabled())) {
  820 + throw exception(DEVICE_REGISTER_DISABLED);
  821 + }
  822 + // 1.3 验证 productSecret
  823 + if (ObjUtil.notEqual(product.getProductSecret(), reqDTO.getProductSecret())) {
  824 + throw exception(DEVICE_REGISTER_SECRET_INVALID);
  825 + }
  826 + return TenantUtils.execute(product.getTenantId(), () -> {
  827 + // 1.4 校验设备是否已存在(已存在则不允许重复注册)
  828 + IotDeviceDO device = getSelf().getDeviceFromCache(reqDTO.getProductKey(), reqDTO.getDeviceName());
  829 + if (device != null) {
  830 + throw exception(DEVICE_REGISTER_ALREADY_EXISTS);
  831 + }
  832 +
  833 + // 2.1 自动创建设备
  834 + IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
  835 + .setDeviceName(reqDTO.getDeviceName())
  836 + .setProductId(product.getId());
  837 + device = createDevice0(createReqVO);
  838 + log.info("[registerDevice][产品({}) 自动创建设备({})]",
  839 + reqDTO.getProductKey(), reqDTO.getDeviceName());
  840 + // 2.2 返回设备密钥
  841 + return new IotDeviceRegisterRespDTO(device.getProductKey(), device.getDeviceName(), device.getDeviceSecret());
  842 + });
  843 + }
  844 +
  845 + @Override
  846 + public List<IotSubDeviceRegisterRespDTO> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO) {
  847 + // 1. 校验网关设备
  848 + IotDeviceDO gatewayDevice = getSelf().getDeviceFromCache(reqDTO.getGatewayProductKey(), reqDTO.getGatewayDeviceName());
  849 +
  850 + // 2. 遍历注册每个子设备
  851 + return TenantUtils.execute(gatewayDevice.getTenantId(), () ->
  852 + registerSubDevices0(gatewayDevice, reqDTO.getSubDevices()));
  853 + }
  854 +
  855 + @Override
  856 + public List<IotSubDeviceRegisterRespDTO> handleSubDeviceRegisterMessage(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  857 + // 1. 解析参数
  858 + if (!(message.getParams() instanceof List)) {
  859 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  860 + }
  861 + List<IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.convertList(message.getParams(), IotSubDeviceRegisterReqDTO.class);
  862 +
  863 + // 2. 遍历注册每个子设备
  864 + return registerSubDevices0(gatewayDevice, subDevices);
  865 + }
  866 +
  867 + private List<IotSubDeviceRegisterRespDTO> registerSubDevices0(IotDeviceDO gatewayDevice,
  868 + List<IotSubDeviceRegisterReqDTO> subDevices) {
  869 + // 1.1 校验网关设备
  870 + if (gatewayDevice == null) {
  871 + throw exception(DEVICE_NOT_EXISTS);
  872 + }
  873 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  874 + throw exception(DEVICE_NOT_GATEWAY);
  875 + }
  876 + // 1.2 注册设备不能为空
  877 + if (CollUtil.isEmpty(subDevices)) {
  878 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  879 + }
  880 +
  881 + // 2. 遍历注册每个子设备
  882 + List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(subDevices.size());
  883 + for (IotSubDeviceRegisterReqDTO subDevice : subDevices) {
  884 + try {
  885 + IotDeviceDO device = registerSubDevice0(gatewayDevice, subDevice);
  886 + results.add(new IotSubDeviceRegisterRespDTO(
  887 + subDevice.getProductKey(), subDevice.getDeviceName(), device.getDeviceSecret()));
  888 + } catch (Exception ex) {
  889 + log.error("[registerSubDevices0][子设备({}/{}) 注册失败]",
  890 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  891 + }
  892 + }
  893 + return results;
  894 + }
  895 +
  896 + private IotDeviceDO registerSubDevice0(IotDeviceDO gatewayDevice, IotSubDeviceRegisterReqDTO params) {
  897 + // 1.1 校验产品
  898 + IotProductDO product = productService.getProductByProductKey(params.getProductKey());
  899 + if (product == null) {
  900 + throw exception(PRODUCT_NOT_EXISTS);
  901 + }
  902 + // 1.2 校验产品是否为网关子设备类型
  903 + if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
  904 + throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
  905 + }
  906 + // 1.3 校验设备是否已存在(子设备动态注册:设备必须已预注册)
  907 + IotDeviceDO existDevice = getSelf().getDeviceFromCache(params.getProductKey(), params.getDeviceName());
  908 + if (existDevice == null) {
  909 + throw exception(DEVICE_NOT_EXISTS);
  910 + }
  911 + // 1.4 校验是否绑定到其他网关
  912 + if (existDevice.getGatewayId() != null && ObjUtil.notEqual(existDevice.getGatewayId(), gatewayDevice.getId())) {
  913 + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS,
  914 + existDevice.getProductKey(), existDevice.getDeviceName());
  915 + }
  916 +
  917 + // 2. 绑定到网关(如果尚未绑定)
  918 + if (existDevice.getGatewayId() == null) {
  919 + // 2.1 更新数据库
  920 + deviceMapper.updateById(new IotDeviceDO().setId(existDevice.getId()).setGatewayId(gatewayDevice.getId()));
  921 + // 2.2 清空对应缓存
  922 + deleteDeviceCache(existDevice);
  923 + log.info("[registerSubDevice][网关({}/{}) 绑定子设备({}/{})]",
  924 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  925 + existDevice.getProductKey(), existDevice.getDeviceName());
  926 + }
  927 + return existDevice;
504 } 928 }
505 929
506 } 930 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageService.java
@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
10 -import jakarta.validation.constraints.NotEmpty;  
11 import jakarta.validation.constraints.NotNull; 10 import jakarta.validation.constraints.NotNull;
12 11
13 import javax.annotation.Nullable; 12 import javax.annotation.Nullable;
@@ -75,7 +74,7 @@ public interface IotDeviceMessageService { @@ -75,7 +74,7 @@ public interface IotDeviceMessageService {
75 */ 74 */
76 List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply( 75 List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(
77 @NotNull(message = "设备编号不能为空") Long deviceId, 76 @NotNull(message = "设备编号不能为空") Long deviceId,
78 - @NotEmpty(message = "请求编号不能为空") List<String> requestIds, 77 + List<String> requestIds,
79 Boolean reply); 78 Boolean reply);
80 79
81 /** 80 /**
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
1 package cn.iocoder.yudao.module.iot.service.device.message; 1 package cn.iocoder.yudao.module.iot.service.device.message;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
3 import cn.hutool.core.date.LocalDateTimeUtil; 5 import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.hutool.core.lang.Assert; 6 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.map.MapUtil; 7 import cn.hutool.core.map.MapUtil;
@@ -16,6 +18,10 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD @@ -16,6 +18,10 @@ import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsD
16 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
17 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 19 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
18 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; 20 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
  21 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  22 +import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
  23 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPostReqDTO;
19 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 25 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
20 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 26 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
21 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; 27 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
@@ -98,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -98,7 +104,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
98 return sendDeviceMessage(message, device); 104 return sendDeviceMessage(message, device);
99 } 105 }
100 106
101 - // TODO @芋艿:针对连接网关的设备,是不是 productKey、deviceName 需要调整下;  
102 @Override 107 @Override
103 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) { 108 public IotDeviceMessage sendDeviceMessage(IotDeviceMessage message, IotDeviceDO device) {
104 return sendDeviceMessage(message, device, null); 109 return sendDeviceMessage(message, device, null);
@@ -168,7 +173,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -168,7 +173,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
168 // 2. 记录消息 173 // 2. 记录消息
169 getSelf().createDeviceLogAsync(message); 174 getSelf().createDeviceLogAsync(message);
170 175
171 - // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息 176 + // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息
172 if (IotDeviceMessageUtils.isReplyMessage(message) 177 if (IotDeviceMessageUtils.isReplyMessage(message)
173 || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod()) 178 || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
174 || StrUtil.isEmpty(message.getServerId())) { 179 || StrUtil.isEmpty(message.getServerId())) {
@@ -185,15 +190,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -185,15 +190,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
185 } 190 }
186 191
187 // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器 192 // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器
188 - @SuppressWarnings("SameReturnValue")  
189 private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) { 193 private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) {
190 // 设备上下线 194 // 设备上下线
191 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { 195 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) {
192 String stateStr = IotDeviceMessageUtils.getIdentifier(message); 196 String stateStr = IotDeviceMessageUtils.getIdentifier(message);
193 assert stateStr != null; 197 assert stateStr != null;
194 Assert.notEmpty(stateStr, "设备状态不能为空"); 198 Assert.notEmpty(stateStr, "设备状态不能为空");
195 - deviceService.updateDeviceState(device, Integer.valueOf(stateStr));  
196 - // TODO 芋艿:子设备的关联 199 + Integer state = Integer.valueOf(stateStr);
  200 + deviceService.updateDeviceState(device, state);
197 return null; 201 return null;
198 } 202 }
199 203
@@ -202,6 +206,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -202,6 +206,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
202 devicePropertyService.saveDeviceProperty(device, message); 206 devicePropertyService.saveDeviceProperty(device, message);
203 return null; 207 return null;
204 } 208 }
  209 + // 批量上报(属性+事件+子设备)
  210 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())) {
  211 + handlePackMessage(message, device);
  212 + return null;
  213 + }
205 214
206 // OTA 上报升级进度 215 // OTA 上报升级进度
207 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) { 216 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) {
@@ -209,10 +218,109 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -209,10 +218,109 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
209 return null; 218 return null;
210 } 219 }
211 220
212 - // TODO @芋艿:这里可以按需,添加别的逻辑; 221 + // 添加拓扑关系
  222 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())) {
  223 + return deviceService.handleTopoAddMessage(message, device);
  224 + }
  225 + // 删除拓扑关系
  226 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) {
  227 + return deviceService.handleTopoDeleteMessage(message, device);
  228 + }
  229 + // 获取拓扑关系
  230 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) {
  231 + return deviceService.handleTopoGetMessage(device);
  232 + }
  233 +
  234 + // 子设备动态注册
  235 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())) {
  236 + return deviceService.handleSubDeviceRegisterMessage(message, device);
  237 + }
  238 +
213 return null; 239 return null;
214 } 240 }
215 241
  242 + // ========== 批量上报处理方法 ==========
  243 +
  244 + /**
  245 + * 处理批量上报消息
  246 + * <p>
  247 + * 将 pack 消息拆分成多条标准消息,发送到 MQ 让规则引擎处理
  248 + *
  249 + * @param packMessage 批量消息
  250 + * @param gatewayDevice 网关设备
  251 + */
  252 + private void handlePackMessage(IotDeviceMessage packMessage, IotDeviceDO gatewayDevice) {
  253 + // 1. 解析参数
  254 + IotDevicePropertyPackPostReqDTO params = JsonUtils.convertObject(
  255 + packMessage.getParams(), IotDevicePropertyPackPostReqDTO.class);
  256 + if (params == null) {
  257 + log.warn("[handlePackMessage][消息({}) 参数解析失败]", packMessage);
  258 + return;
  259 + }
  260 +
  261 + // 2. 处理网关设备(自身)的数据
  262 + sendDevicePackData(gatewayDevice, packMessage.getServerId(), params.getProperties(), params.getEvents());
  263 +
  264 + // 3. 处理子设备的数据
  265 + if (CollUtil.isEmpty(params.getSubDevices())) {
  266 + return;
  267 + }
  268 + for (IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData : params.getSubDevices()) {
  269 + try {
  270 + IotDeviceIdentity identity = subDeviceData.getIdentity();
  271 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(identity.getProductKey(), identity.getDeviceName());
  272 + if (subDevice == null) {
  273 + log.warn("[handlePackMessage][子设备({}/{}) 不存在]", identity.getProductKey(), identity.getDeviceName());
  274 + continue;
  275 + }
  276 + // 特殊:子设备不需要指定 serverId,因为子设备实际可能连接在不同的 gateway-server 上,导致 serverId 不同
  277 + sendDevicePackData(subDevice, null, subDeviceData.getProperties(), subDeviceData.getEvents());
  278 + } catch (Exception ex) {
  279 + log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", subDeviceData.getIdentity().getProductKey(),
  280 + subDeviceData.getIdentity().getDeviceName(), ex);
  281 + }
  282 + }
  283 + }
  284 +
  285 + /**
  286 + * 发送设备 pack 数据到 MQ(属性 + 事件)
  287 + *
  288 + * @param device 设备
  289 + * @param serverId 服务标识
  290 + * @param properties 属性数据
  291 + * @param events 事件数据
  292 + */
  293 + private void sendDevicePackData(IotDeviceDO device, String serverId,
  294 + Map<String, Object> properties,
  295 + Map<String, IotDevicePropertyPackPostReqDTO.EventValue> events) {
  296 + // 1. 发送属性消息
  297 + if (MapUtil.isNotEmpty(properties)) {
  298 + IotDeviceMessage propertyMsg = IotDeviceMessage.requestOf(
  299 + device.getId(), device.getTenantId(), serverId,
  300 + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(),
  301 + IotDevicePropertyPostReqDTO.of(properties));
  302 + deviceMessageProducer.sendDeviceMessage(propertyMsg);
  303 + }
  304 +
  305 + // 2. 发送事件消息
  306 + if (MapUtil.isNotEmpty(events)) {
  307 + for (Map.Entry<String, IotDevicePropertyPackPostReqDTO.EventValue> eventEntry : events.entrySet()) {
  308 + String eventId = eventEntry.getKey();
  309 + IotDevicePropertyPackPostReqDTO.EventValue eventValue = eventEntry.getValue();
  310 + if (eventValue == null) {
  311 + continue;
  312 + }
  313 + IotDeviceMessage eventMsg = IotDeviceMessage.requestOf(
  314 + device.getId(), device.getTenantId(), serverId,
  315 + IotDeviceMessageMethodEnum.EVENT_POST.getMethod(),
  316 + IotDeviceEventPostReqDTO.of(eventId, eventValue.getValue(), eventValue.getTime()));
  317 + deviceMessageProducer.sendDeviceMessage(eventMsg);
  318 + }
  319 + }
  320 + }
  321 +
  322 + // ========= 设备消息查询 ==========
  323 +
216 @Override 324 @Override
217 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) { 325 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) {
218 try { 326 try {
@@ -228,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -228,9 +336,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
228 } 336 }
229 337
230 @Override 338 @Override
231 - public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId,  
232 - List<String> requestIds,  
233 - Boolean reply) { 339 + public List<IotDeviceMessageDO> getDeviceMessageListByRequestIdsAndReply(Long deviceId, List<String> requestIds, Boolean reply) {
  340 + if (CollUtil.isEmpty(requestIds)) {
  341 + return ListUtil.of();
  342 + }
234 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply); 343 return deviceMessageMapper.selectListByRequestIdsAndReply(deviceId, requestIds, reply);
235 } 344 }
236 345
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; 22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
23 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum; 23 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
24 import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; 24 import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  25 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
25 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 26 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
26 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService; 27 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
27 import jakarta.annotation.Resource; 28 import jakarta.annotation.Resource;
@@ -30,10 +31,12 @@ import org.springframework.context.annotation.Lazy; @@ -30,10 +31,12 @@ import org.springframework.context.annotation.Lazy;
30 import org.springframework.scheduling.annotation.Async; 31 import org.springframework.scheduling.annotation.Async;
31 import org.springframework.stereotype.Service; 32 import org.springframework.stereotype.Service;
32 33
  34 +import java.math.BigDecimal;
33 import java.time.LocalDateTime; 35 import java.time.LocalDateTime;
34 import java.util.*; 36 import java.util.*;
35 37
36 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 38 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
  39 +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal;
37 40
38 /** 41 /**
39 * IoT 设备【属性】数据 Service 实现类 42 * IoT 设备【属性】数据 Service 实现类
@@ -66,6 +69,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { @@ -66,6 +69,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
66 @Resource 69 @Resource
67 @Lazy // 延迟加载,解决循环依赖 70 @Lazy // 延迟加载,解决循环依赖
68 private IotProductService productService; 71 private IotProductService productService;
  72 + @Resource
  73 + @Lazy // 延迟加载,解决循环依赖
  74 + private IotDeviceService deviceService;
69 75
70 @Resource 76 @Resource
71 private DevicePropertyRedisDAO deviceDataRedisDAO; 77 private DevicePropertyRedisDAO deviceDataRedisDAO;
@@ -126,48 +132,60 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { @@ -126,48 +132,60 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
126 } 132 }
127 133
128 @Override 134 @Override
  135 + @SuppressWarnings("PatternVariableCanBeUsed")
129 public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { 136 public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) {
130 if (!(message.getParams() instanceof Map)) { 137 if (!(message.getParams() instanceof Map)) {
131 log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); 138 log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message);
132 return; 139 return;
133 } 140 }
  141 + Map<?, ?> params = (Map<?, ?>) message.getParams();
  142 + if (CollUtil.isEmpty(params)) {
  143 + log.error("[saveDeviceProperty][消息内容({}) 的 data 为空]", message);
  144 + return;
  145 + }
134 146
135 // 1. 根据物模型,拼接合法的属性 147 // 1. 根据物模型,拼接合法的属性
136 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? 148 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)?
137 List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); 149 List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId());
138 Map<String, Object> properties = new HashMap<>(); 150 Map<String, Object> properties = new HashMap<>();
139 - ((Map<?, ?>) message.getParams()).forEach((key, value) -> { 151 + params.forEach((key, value) -> {
140 IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key)); 152 IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key));
141 if (thingModel == null || thingModel.getProperty() == null) { 153 if (thingModel == null || thingModel.getProperty() == null) {
142 log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); 154 log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key);
143 return; 155 return;
144 } 156 }
145 - if (ObjectUtils.equalsAny(thingModel.getProperty().getDataType(), 157 + String dataType = thingModel.getProperty().getDataType();
  158 + if (ObjectUtils.equalsAny(dataType,
146 IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { 159 IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
147 // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 160 // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储
148 properties.put((String) key, JsonUtils.toJsonString(value)); 161 properties.put((String) key, JsonUtils.toJsonString(value));
149 - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(thingModel.getProperty().getDataType())) {  
150 - properties.put((String) key, Convert.toDouble(value));  
151 - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(thingModel.getProperty().getDataType())) { 162 + } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) {
  163 + properties.put((String) key, Convert.toInt(value));
  164 + } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) {
152 properties.put((String) key, Convert.toFloat(value)); 165 properties.put((String) key, Convert.toFloat(value));
153 - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(thingModel.getProperty().getDataType())) { 166 + } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) {
  167 + properties.put((String) key, Convert.toDouble(value));
  168 + } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) {
154 properties.put((String) key, Convert.toByte(value)); 169 properties.put((String) key, Convert.toByte(value));
155 - } else { 170 + } else {
156 properties.put((String) key, value); 171 properties.put((String) key, value);
157 } 172 }
158 }); 173 });
159 if (CollUtil.isEmpty(properties)) { 174 if (CollUtil.isEmpty(properties)) {
160 log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); 175 log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message);
161 - return;  
162 - } 176 + } else {
  177 + // 2.1 保存设备属性【数据】
  178 + devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime()));
163 179
164 - // 2.1 保存设备属性【数据】  
165 - devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); 180 + // 2.2 保存设备属性【日志】
  181 + Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry ->
  182 + IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build());
  183 + deviceDataRedisDAO.putAll(device.getId(), properties2);
  184 + }
166 185
167 - // 2.2 保存设备属性【日志】  
168 - Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry ->  
169 - IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build());  
170 - deviceDataRedisDAO.putAll(device.getId(), properties2); 186 + // 2.3 提取 GeoLocation 并更新设备定位
  187 + // 为什么 properties 为空,也要执行定位更新?因为可能上报的属性里,没有合法属性,但是包含 GeoLocation 定位属性
  188 + extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams());
171 } 189 }
172 190
173 @Override 191 @Override
@@ -213,4 +231,77 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { @@ -213,4 +231,77 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
213 return deviceServerIdRedisDAO.get(id); 231 return deviceServerIdRedisDAO.get(id);
214 } 232 }
215 233
  234 + // ========== 设备定位相关操作 ==========
  235 +
  236 + /**
  237 + * 从属性中提取 GeoLocation 并更新设备定位
  238 + *
  239 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/device-geolocation">阿里云规范</a>
  240 + * GeoLocation 结构体包含:Longitude, Latitude, Altitude, CoordinateSystem
  241 + */
  242 + private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) {
  243 + // 1. 解析 GeoLocation 经纬度坐标
  244 + BigDecimal[] location = parseGeoLocation(params);
  245 + if (location == null) {
  246 + return;
  247 + }
  248 +
  249 + // 2. 更新设备定位
  250 + deviceService.updateDeviceLocation(device, location[0], location[1]);
  251 + log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]",
  252 + device.getId(), location[0], location[1]);
  253 + }
  254 +
  255 + /**
  256 + * 从属性参数中解析 GeoLocation,返回经纬度坐标数组 [longitude, latitude]
  257 + *
  258 + * @param params 属性参数
  259 + * @return [经度, 纬度],解析失败返回 null
  260 + */
  261 + @SuppressWarnings("unchecked")
  262 + private BigDecimal[] parseGeoLocation(Map<?, ?> params) {
  263 + if (params == null) {
  264 + return null;
  265 + }
  266 + // 1. 查找 GeoLocation 属性(标识符为 GeoLocation 或 geoLocation)
  267 + Object geoValue = params.get("GeoLocation");
  268 + if (geoValue == null) {
  269 + geoValue = params.get("geoLocation");
  270 + }
  271 + if (geoValue == null) {
  272 + return null;
  273 + }
  274 +
  275 + // 2. 转换为 Map
  276 + Map<String, Object> geoLocation = null;
  277 + if (geoValue instanceof Map) {
  278 + geoLocation = (Map<String, Object>) geoValue;
  279 + } else if (geoValue instanceof String) {
  280 + geoLocation = JsonUtils.parseObject((String) geoValue, Map.class);
  281 + }
  282 + if (geoLocation == null) {
  283 + return null;
  284 + }
  285 +
  286 + // 3. 提取经纬度(支持阿里云命名规范:首字母大写)
  287 + BigDecimal longitude = getBigDecimal(geoLocation, "Longitude");
  288 + if (longitude == null) {
  289 + longitude = getBigDecimal(geoLocation, "longitude");
  290 + }
  291 + BigDecimal latitude = getBigDecimal(geoLocation, "Latitude");
  292 + if (latitude == null) {
  293 + latitude = getBigDecimal(geoLocation, "latitude");
  294 + }
  295 + if (longitude == null || latitude == null) {
  296 + return null;
  297 + }
  298 + // 校验经纬度范围:经度 -180 到 180,纬度 -90 到 90
  299 + if (longitude.compareTo(BigDecimal.valueOf(-180)) < 0 || longitude.compareTo(BigDecimal.valueOf(180)) > 0
  300 + || latitude.compareTo(BigDecimal.valueOf(-90)) < 0 || latitude.compareTo(BigDecimal.valueOf(90)) > 0) {
  301 + log.warn("[parseGeoLocation][经纬度超出有效范围: lng={}, lat={}]", longitude, latitude);
  302 + return null;
  303 + }
  304 + return new BigDecimal[]{longitude, latitude};
  305 + }
  306 +
216 } 307 }
217 \ No newline at end of file 308 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductService.java
@@ -106,6 +106,14 @@ public interface IotProductService { @@ -106,6 +106,14 @@ public interface IotProductService {
106 List<IotProductDO> getProductList(); 106 List<IotProductDO> getProductList();
107 107
108 /** 108 /**
  109 + * 根据设备类型获得产品列表
  110 + *
  111 + * @param deviceType 设备类型(可选)
  112 + * @return 产品列表
  113 + */
  114 + List<IotProductDO> getProductList(@Nullable Integer deviceType);
  115 +
  116 + /**
109 * 获得产品数量 117 * 获得产品数量
110 * 118 *
111 * @param createTime 创建时间,如果为空,则统计所有产品数量 119 * @param createTime 创建时间,如果为空,则统计所有产品数量
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.service.product;
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  6 +import cn.hutool.core.util.IdUtil;
6 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; 7 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO; 8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductPageReqVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO; 9 import cn.iocoder.yudao.module.iot.controller.admin.product.vo.product.IotProductSaveReqVO;
@@ -53,19 +54,22 @@ public class IotProductServiceImpl implements IotProductService { @@ -53,19 +54,22 @@ public class IotProductServiceImpl implements IotProductService {
53 54
54 // 2. 插入 55 // 2. 插入
55 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class) 56 IotProductDO product = BeanUtils.toBean(createReqVO, IotProductDO.class)
56 - .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus()); 57 + .setStatus(IotProductStatusEnum.UNPUBLISHED.getStatus())
  58 + .setProductSecret(generateProductSecret());
57 productMapper.insert(product); 59 productMapper.insert(product);
58 return product.getId(); 60 return product.getId();
59 } 61 }
60 62
  63 + private String generateProductSecret() {
  64 + return IdUtil.fastSimpleUUID();
  65 + }
  66 +
61 @Override 67 @Override
62 @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id") 68 @CacheEvict(value = RedisKeyConstants.PRODUCT, key = "#updateReqVO.id")
63 public void updateProduct(IotProductSaveReqVO updateReqVO) { 69 public void updateProduct(IotProductSaveReqVO updateReqVO) {
64 updateReqVO.setProductKey(null); // 不更新产品标识 70 updateReqVO.setProductKey(null); // 不更新产品标识
65 - // 1.1 校验存在  
66 - IotProductDO iotProductDO = validateProductExists(updateReqVO.getId());  
67 - // 1.2 发布状态不可更新  
68 - validateProductStatus(iotProductDO); 71 + // 1. 校验存在
  72 + validateProductExists(updateReqVO.getId());
69 73
70 // 2. 更新 74 // 2. 更新
71 IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class); 75 IotProductDO updateObj = BeanUtils.toBean(updateReqVO, IotProductDO.class);
@@ -158,6 +162,11 @@ public class IotProductServiceImpl implements IotProductService { @@ -158,6 +162,11 @@ public class IotProductServiceImpl implements IotProductService {
158 } 162 }
159 163
160 @Override 164 @Override
  165 + public List<IotProductDO> getProductList(Integer deviceType) {
  166 + return productMapper.selectList(deviceType);
  167 + }
  168 +
  169 + @Override
161 public Long getProductCount(LocalDateTime createTime) { 170 public Long getProductCount(LocalDateTime createTime) {
162 return productMapper.selectCountByCreateTime(createTime); 171 return productMapper.selectCountByCreateTime(createTime);
163 } 172 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleServiceImpl.java
@@ -271,6 +271,10 @@ public class IotDataRuleServiceImpl implements IotDataRuleService { @@ -271,6 +271,10 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
271 if (ObjUtil.notEqual(action.getType(), dataSink.getType())) { 271 if (ObjUtil.notEqual(action.getType(), dataSink.getType())) {
272 return; 272 return;
273 } 273 }
  274 + if (CommonStatusEnum.isDisable(dataSink.getStatus())) {
  275 + log.warn("[executeDataRuleAction][消息({}) 数据目的({}) 状态为禁用]", message.getId(), dataSink.getId());
  276 + return;
  277 + }
274 try { 278 try {
275 action.execute(message, dataSink); 279 action.execute(message, dataSink);
276 log.info("[executeDataRuleAction][消息({}) 数据目的({}) 执行成功]", message.getId(), dataSink.getId()); 280 log.info("[executeDataRuleAction][消息({}) 数据目的({}) 执行成功]", message.getId(), dataSink.getId());
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java
@@ -43,7 +43,6 @@ public class IotTcpDataRuleAction extends @@ -43,7 +43,6 @@ public class IotTcpDataRuleAction extends
43 config.getConnectTimeoutMs(), 43 config.getConnectTimeoutMs(),
44 config.getReadTimeoutMs(), 44 config.getReadTimeoutMs(),
45 config.getSsl(), 45 config.getSsl(),
46 - config.getSslCertPath(),  
47 config.getDataFormat() 46 config.getDataFormat()
48 ); 47 );
49 // 2.2 连接服务器 48 // 2.2 连接服务器
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java
@@ -8,6 +8,10 @@ import cn.iocoder.yudao.module.iot.service.rule.data.action.websocket.IotWebSock @@ -8,6 +8,10 @@ import cn.iocoder.yudao.module.iot.service.rule.data.action.websocket.IotWebSock
8 import lombok.extern.slf4j.Slf4j; 8 import lombok.extern.slf4j.Slf4j;
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
  11 +import java.util.concurrent.ConcurrentHashMap;
  12 +import java.util.concurrent.TimeUnit;
  13 +import java.util.concurrent.locks.ReentrantLock;
  14 +
11 /** 15 /**
12 * WebSocket 的 {@link IotDataRuleAction} 实现类 16 * WebSocket 的 {@link IotDataRuleAction} 实现类
13 * <p> 17 * <p>
@@ -22,6 +26,18 @@ import org.springframework.stereotype.Component; @@ -22,6 +26,18 @@ import org.springframework.stereotype.Component;
22 public class IotWebSocketDataRuleAction extends 26 public class IotWebSocketDataRuleAction extends
23 IotDataRuleCacheableAction<IotDataSinkWebSocketConfig, IotWebSocketClient> { 27 IotDataRuleCacheableAction<IotDataSinkWebSocketConfig, IotWebSocketClient> {
24 28
  29 + /**
  30 + * 锁等待超时时间(毫秒)
  31 + */
  32 + private static final long LOCK_WAIT_TIME_MS = 5000;
  33 +
  34 + /**
  35 + * 重连锁,key 为 WebSocket 服务器地址
  36 + * <p>
  37 + * WebSocket 连接是与特定服务器实例绑定的,使用单机锁即可保证重连的线程安全
  38 + */
  39 + private final ConcurrentHashMap<String, ReentrantLock> reconnectLocks = new ConcurrentHashMap<>();
  40 +
25 @Override 41 @Override
26 public Integer getType() { 42 public Integer getType() {
27 return IotDataSinkTypeEnum.WEBSOCKET.getType(); 43 return IotDataSinkTypeEnum.WEBSOCKET.getType();
@@ -62,12 +78,11 @@ public class IotWebSocketDataRuleAction extends @@ -62,12 +78,11 @@ public class IotWebSocketDataRuleAction extends
62 protected void execute(IotDeviceMessage message, IotDataSinkWebSocketConfig config) throws Exception { 78 protected void execute(IotDeviceMessage message, IotDataSinkWebSocketConfig config) throws Exception {
63 try { 79 try {
64 // 1.1 获取或创建 WebSocket 客户端 80 // 1.1 获取或创建 WebSocket 客户端
65 - // TODO @puhui999:需要加锁,保证必须连接上;  
66 IotWebSocketClient webSocketClient = getProducer(config); 81 IotWebSocketClient webSocketClient = getProducer(config);
67 - // 1.2 检查连接状态,如果断开则重新连接 82 +
  83 + // 1.2 检查连接状态,如果断开则使用分布式锁保证重连的线程安全
68 if (!webSocketClient.isConnected()) { 84 if (!webSocketClient.isConnected()) {
69 - log.warn("[execute][WebSocket 连接已断开,尝试重新连接,服务器: {}]", config.getServerUrl());  
70 - webSocketClient.connect(); 85 + reconnectWithLock(webSocketClient, config);
71 } 86 }
72 87
73 // 2.1 发送消息 88 // 2.1 发送消息
@@ -82,4 +97,34 @@ public class IotWebSocketDataRuleAction extends @@ -82,4 +97,34 @@ public class IotWebSocketDataRuleAction extends
82 } 97 }
83 } 98 }
84 99
  100 + // TODO @puhui999:为什么这里要加锁呀?
  101 + /**
  102 + * 使用锁进行重连,保证同一服务器地址的重连操作线程安全
  103 + *
  104 + * @param webSocketClient WebSocket 客户端
  105 + * @param config 配置信息
  106 + */
  107 + private void reconnectWithLock(IotWebSocketClient webSocketClient, IotDataSinkWebSocketConfig config) throws Exception {
  108 + ReentrantLock lock = reconnectLocks.computeIfAbsent(config.getServerUrl(), k -> new ReentrantLock());
  109 + boolean acquired = false;
  110 + try {
  111 + acquired = lock.tryLock(LOCK_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
  112 + if (!acquired) {
  113 + throw new RuntimeException("获取 WebSocket 重连锁超时,服务器: " + config.getServerUrl());
  114 + }
  115 + // 双重检查:获取锁后再次检查连接状态,避免重复连接
  116 + if (!webSocketClient.isConnected()) {
  117 + log.warn("[reconnectWithLock][WebSocket 连接已断开,尝试重新连接,服务器: {}]", config.getServerUrl());
  118 + webSocketClient.connect();
  119 + }
  120 + } catch (InterruptedException e) {
  121 + Thread.currentThread().interrupt();
  122 + throw new RuntimeException("获取 WebSocket 重连锁被中断,服务器: " + config.getServerUrl(), e);
  123 + } finally {
  124 + if (acquired && lock.isHeldByCurrentThread()) {
  125 + lock.unlock();
  126 + }
  127 + }
  128 + }
  129 +
85 } 130 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java
1 package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp; 1 package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
2 2
  3 +import cn.hutool.core.util.ObjUtil;
3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
@@ -31,8 +32,6 @@ public class IotTcpClient { @@ -31,8 +32,6 @@ public class IotTcpClient {
31 private final Integer connectTimeoutMs; 32 private final Integer connectTimeoutMs;
32 private final Integer readTimeoutMs; 33 private final Integer readTimeoutMs;
33 private final Boolean ssl; 34 private final Boolean ssl;
34 - // TODO @puhui999:sslCertPath 是不是没在用?  
35 - private final String sslCertPath;  
36 private final String dataFormat; 35 private final String dataFormat;
37 36
38 private Socket socket; 37 private Socket socket;
@@ -41,15 +40,13 @@ public class IotTcpClient { @@ -41,15 +40,13 @@ public class IotTcpClient {
41 private final AtomicBoolean connected = new AtomicBoolean(false); 40 private final AtomicBoolean connected = new AtomicBoolean(false);
42 41
43 public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs, 42 public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
44 - Boolean ssl, String sslCertPath, String dataFormat) { 43 + Boolean ssl, String dataFormat) {
45 this.host = host; 44 this.host = host;
46 this.port = port; 45 this.port = port;
47 this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkTcpConfig.DEFAULT_CONNECT_TIMEOUT_MS; 46 this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkTcpConfig.DEFAULT_CONNECT_TIMEOUT_MS;
48 this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : IotDataSinkTcpConfig.DEFAULT_READ_TIMEOUT_MS; 47 this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : IotDataSinkTcpConfig.DEFAULT_READ_TIMEOUT_MS;
49 this.ssl = ssl != null ? ssl : IotDataSinkTcpConfig.DEFAULT_SSL; 48 this.ssl = ssl != null ? ssl : IotDataSinkTcpConfig.DEFAULT_SSL;
50 - this.sslCertPath = sslCertPath;  
51 - // TODO @puhui999:可以使用 StrUtil.defaultIfBlank 方法简化  
52 - this.dataFormat = dataFormat != null ? dataFormat : IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT; 49 + this.dataFormat = ObjUtil.defaultIfBlank(dataFormat, IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT);
53 } 50 }
54 51
55 /** 52 /**
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java
@@ -4,13 +4,9 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -4,13 +4,9 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkWebSocketConfig; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkWebSocketConfig;
6 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
  7 +import okhttp3.*;
7 8
8 -import java.net.URI;  
9 -import java.net.http.HttpClient;  
10 -import java.net.http.WebSocket;  
11 -import java.time.Duration;  
12 -import java.util.concurrent.CompletableFuture;  
13 -import java.util.concurrent.CompletionStage; 9 +import java.util.concurrent.CountDownLatch;
14 import java.util.concurrent.TimeUnit; 10 import java.util.concurrent.TimeUnit;
15 import java.util.concurrent.atomic.AtomicBoolean; 11 import java.util.concurrent.atomic.AtomicBoolean;
16 12
@@ -19,21 +15,23 @@ import java.util.concurrent.atomic.AtomicBoolean; @@ -19,21 +15,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
19 * <p> 15 * <p>
20 * 负责与外部 WebSocket 服务器建立连接并发送设备消息 16 * 负责与外部 WebSocket 服务器建立连接并发送设备消息
21 * 支持 ws:// 和 wss:// 协议,支持 JSON 和 TEXT 数据格式 17 * 支持 ws:// 和 wss:// 协议,支持 JSON 和 TEXT 数据格式
22 - * 基于 Java 11+ 内置的 java.net.http.WebSocket 实现 18 + * 基于 OkHttp WebSocket 实现,兼容 JDK 8+
  19 + * <p>
  20 + * 注意:该类的线程安全由调用方(IotWebSocketDataRuleAction)通过分布式锁保证
23 * 21 *
24 * @author HUIHUI 22 * @author HUIHUI
25 */ 23 */
26 @Slf4j 24 @Slf4j
27 -public class IotWebSocketClient implements WebSocket.Listener { 25 +public class IotWebSocketClient {
28 26
29 private final String serverUrl; 27 private final String serverUrl;
30 private final Integer connectTimeoutMs; 28 private final Integer connectTimeoutMs;
31 private final Integer sendTimeoutMs; 29 private final Integer sendTimeoutMs;
32 private final String dataFormat; 30 private final String dataFormat;
33 31
34 - private WebSocket webSocket; 32 + private OkHttpClient okHttpClient;
  33 + private volatile WebSocket webSocket;
35 private final AtomicBoolean connected = new AtomicBoolean(false); 34 private final AtomicBoolean connected = new AtomicBoolean(false);
36 - private final StringBuilder messageBuffer = new StringBuilder();  
37 35
38 public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) { 36 public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) {
39 this.serverUrl = serverUrl; 37 this.serverUrl = serverUrl;
@@ -44,8 +42,9 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -44,8 +42,9 @@ public class IotWebSocketClient implements WebSocket.Listener {
44 42
45 /** 43 /**
46 * 连接到 WebSocket 服务器 44 * 连接到 WebSocket 服务器
  45 + * <p>
  46 + * 注意:调用方需要通过分布式锁保证并发安全
47 */ 47 */
48 - @SuppressWarnings("resource")  
49 public void connect() throws Exception { 48 public void connect() throws Exception {
50 if (connected.get()) { 49 if (connected.get()) {
51 log.warn("[connect][WebSocket 客户端已经连接,无需重复连接]"); 50 log.warn("[connect][WebSocket 客户端已经连接,无需重复连接]");
@@ -53,17 +52,30 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -53,17 +52,30 @@ public class IotWebSocketClient implements WebSocket.Listener {
53 } 52 }
54 53
55 try { 54 try {
56 - HttpClient httpClient = HttpClient.newBuilder()  
57 - .connectTimeout(Duration.ofMillis(connectTimeoutMs)) 55 + // 创建 OkHttpClient
  56 + okHttpClient = new OkHttpClient.Builder()
  57 + .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
  58 + .readTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS)
  59 + .writeTimeout(sendTimeoutMs, TimeUnit.MILLISECONDS)
  60 + .build();
  61 +
  62 + // 创建 WebSocket 请求
  63 + Request request = new Request.Builder()
  64 + .url(serverUrl)
58 .build(); 65 .build();
59 66
60 - CompletableFuture<WebSocket> future = httpClient.newWebSocketBuilder()  
61 - .connectTimeout(Duration.ofMillis(connectTimeoutMs))  
62 - .buildAsync(URI.create(serverUrl), this); 67 + // 使用 CountDownLatch 等待连接完成
  68 + CountDownLatch connectLatch = new CountDownLatch(1);
  69 + AtomicBoolean connectSuccess = new AtomicBoolean(false);
  70 + // 创建 WebSocket 连接
  71 + webSocket = okHttpClient.newWebSocket(request, new IotWebSocketListener(connectLatch, connectSuccess));
63 72
64 // 等待连接完成 73 // 等待连接完成
65 - webSocket = future.get(connectTimeoutMs, TimeUnit.MILLISECONDS);  
66 - connected.set(true); 74 + boolean await = connectLatch.await(connectTimeoutMs, TimeUnit.MILLISECONDS);
  75 + if (!await || !connectSuccess.get()) {
  76 + close();
  77 + throw new Exception("WebSocket 连接超时或失败,服务器地址: " + serverUrl);
  78 + }
67 log.info("[connect][WebSocket 客户端连接成功,服务器地址: {}]", serverUrl); 79 log.info("[connect][WebSocket 客户端连接成功,服务器地址: {}]", serverUrl);
68 } catch (Exception e) { 80 } catch (Exception e) {
69 close(); 81 close();
@@ -72,36 +84,6 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -72,36 +84,6 @@ public class IotWebSocketClient implements WebSocket.Listener {
72 } 84 }
73 } 85 }
74 86
75 - @Override  
76 - public void onOpen(WebSocket webSocket) {  
77 - log.debug("[onOpen][WebSocket 连接已打开]");  
78 - webSocket.request(1);  
79 - }  
80 -  
81 - @Override  
82 - public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {  
83 - messageBuffer.append(data);  
84 - if (last) {  
85 - log.debug("[onText][收到 WebSocket 消息: {}]", messageBuffer);  
86 - messageBuffer.setLength(0);  
87 - }  
88 - webSocket.request(1);  
89 - return null;  
90 - }  
91 -  
92 - @Override  
93 - public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {  
94 - connected.set(false);  
95 - log.info("[onClose][WebSocket 连接已关闭,状态码: {},原因: {}]", statusCode, reason);  
96 - return null;  
97 - }  
98 -  
99 - @Override  
100 - public void onError(WebSocket webSocket, Throwable error) {  
101 - connected.set(false);  
102 - log.error("[onError][WebSocket 发生错误]", error);  
103 - }  
104 -  
105 /** 87 /**
106 * 发送设备消息 88 * 发送设备消息
107 * 89 *
@@ -109,7 +91,8 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -109,7 +91,8 @@ public class IotWebSocketClient implements WebSocket.Listener {
109 * @throws Exception 发送异常 91 * @throws Exception 发送异常
110 */ 92 */
111 public void sendMessage(IotDeviceMessage message) throws Exception { 93 public void sendMessage(IotDeviceMessage message) throws Exception {
112 - if (!connected.get() || webSocket == null) { 94 + WebSocket ws = this.webSocket;
  95 + if (!connected.get() || ws == null) {
113 throw new IllegalStateException("WebSocket 客户端未连接"); 96 throw new IllegalStateException("WebSocket 客户端未连接");
114 } 97 }
115 98
@@ -121,9 +104,11 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -121,9 +104,11 @@ public class IotWebSocketClient implements WebSocket.Listener {
121 messageData = message.toString(); 104 messageData = message.toString();
122 } 105 }
123 106
124 - // 发送消息并等待完成  
125 - CompletableFuture<WebSocket> future = webSocket.sendText(messageData, true);  
126 - future.get(sendTimeoutMs, TimeUnit.MILLISECONDS); 107 + // 发送消息
  108 + boolean success = ws.send(messageData);
  109 + if (!success) {
  110 + throw new Exception("WebSocket 发送消息失败,消息队列已满或连接已关闭");
  111 + }
127 log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]", 112 log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
128 message.getDeviceId(), messageData.length()); 113 message.getDeviceId(), messageData.length());
129 } catch (Exception e) { 114 } catch (Exception e) {
@@ -136,18 +121,18 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -136,18 +121,18 @@ public class IotWebSocketClient implements WebSocket.Listener {
136 * 关闭连接 121 * 关闭连接
137 */ 122 */
138 public void close() { 123 public void close() {
139 - if (!connected.get() && webSocket == null) {  
140 - return;  
141 - }  
142 -  
143 try { 124 try {
144 if (webSocket != null) { 125 if (webSocket != null) {
145 - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "客户端主动关闭")  
146 - .orTimeout(5, TimeUnit.SECONDS)  
147 - .exceptionally(e -> {  
148 - log.warn("[close][发送关闭帧失败]", e);  
149 - return null;  
150 - }); 126 + // 发送正常关闭帧,状态码 1000 表示正常关闭
  127 + // TODO @puhui999:有没 1000 的枚举哈?在 okhttp 里
  128 + webSocket.close(1000, "客户端主动关闭");
  129 + webSocket = null;
  130 + }
  131 + if (okHttpClient != null) {
  132 + // 关闭连接池和调度器
  133 + okHttpClient.dispatcher().executorService().shutdown();
  134 + okHttpClient.connectionPool().evictAll();
  135 + okHttpClient = null;
151 } 136 }
152 connected.set(false); 137 connected.set(false);
153 log.info("[close][WebSocket 客户端连接已关闭,服务器地址: {}]", serverUrl); 138 log.info("[close][WebSocket 客户端连接已关闭,服务器地址: {}]", serverUrl);
@@ -174,4 +159,51 @@ public class IotWebSocketClient implements WebSocket.Listener { @@ -174,4 +159,51 @@ public class IotWebSocketClient implements WebSocket.Listener {
174 '}'; 159 '}';
175 } 160 }
176 161
  162 + /**
  163 + * OkHttp WebSocket 监听器
  164 + */
  165 + @SuppressWarnings("NullableProblems")
  166 + private class IotWebSocketListener extends WebSocketListener {
  167 +
  168 + private final CountDownLatch connectLatch;
  169 + private final AtomicBoolean connectSuccess;
  170 +
  171 + public IotWebSocketListener(CountDownLatch connectLatch, AtomicBoolean connectSuccess) {
  172 + this.connectLatch = connectLatch;
  173 + this.connectSuccess = connectSuccess;
  174 + }
  175 +
  176 + @Override
  177 + public void onOpen(WebSocket webSocket, Response response) {
  178 + connected.set(true);
  179 + connectSuccess.set(true);
  180 + connectLatch.countDown();
  181 + log.info("[onOpen][WebSocket 连接已打开,服务器: {}]", serverUrl);
  182 + }
  183 +
  184 + @Override
  185 + public void onMessage(WebSocket webSocket, String text) {
  186 + log.debug("[onMessage][收到消息: {}]", text);
  187 + }
  188 +
  189 + @Override
  190 + public void onClosing(WebSocket webSocket, int code, String reason) {
  191 + connected.set(false);
  192 + log.info("[onClosing][WebSocket 正在关闭,code: {}, reason: {}]", code, reason);
  193 + }
  194 +
  195 + @Override
  196 + public void onClosed(WebSocket webSocket, int code, String reason) {
  197 + connected.set(false);
  198 + log.info("[onClosed][WebSocket 已关闭,code: {}, reason: {}]", code, reason);
  199 + }
  200 +
  201 + @Override
  202 + public void onFailure(WebSocket webSocket, Throwable t, Response response) {
  203 + connected.set(false);
  204 + connectLatch.countDown(); // 确保连接失败时也释放等待
  205 + log.error("[onFailure][WebSocket 连接失败]", t);
  206 + }
  207 + }
  208 +
177 } 209 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
@@ -23,6 +23,7 @@ import cn.iocoder.yudao.module.iot.service.product.IotProductService; @@ -23,6 +23,7 @@ import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; 24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
25 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; 25 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
  26 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
26 import jakarta.annotation.Resource; 27 import jakarta.annotation.Resource;
27 import lombok.extern.slf4j.Slf4j; 28 import lombok.extern.slf4j.Slf4j;
28 import org.springframework.cache.annotation.CacheEvict; 29 import org.springframework.cache.annotation.CacheEvict;
@@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
62 private List<IotSceneRuleAction> sceneRuleActions; 63 private List<IotSceneRuleAction> sceneRuleActions;
63 @Resource 64 @Resource
64 private IotSceneRuleTimerHandler timerHandler; 65 private IotSceneRuleTimerHandler timerHandler;
  66 + @Resource
  67 + private IotTimerConditionEvaluator timerConditionEvaluator;
65 68
66 @Override 69 @Override
67 @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) 70 @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
@@ -222,19 +225,99 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -222,19 +225,99 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
222 return; 225 return;
223 } 226 }
224 // 1.2 判断是否有定时触发器,避免脏数据 227 // 1.2 判断是否有定时触发器,避免脏数据
225 - IotSceneRuleDO.Trigger config = CollUtil.findOne(scene.getTriggers(), 228 + IotSceneRuleDO.Trigger timerTrigger = CollUtil.findOne(scene.getTriggers(),
226 trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); 229 trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
227 - if (config == null) { 230 + if (timerTrigger == null) {
228 log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene); 231 log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene);
229 return; 232 return;
230 } 233 }
231 234
232 - // 2. 执行规则场景 235 + // 2. 评估条件组(新增逻辑)
  236 + log.info("[executeSceneRuleByTimer][规则场景({}) 开始评估条件组]", id);
  237 + if (!evaluateTimerConditionGroups(scene, timerTrigger)) {
  238 + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组不满足,跳过执行]", id);
  239 + return;
  240 + }
  241 + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组评估通过,准备执行动作]", id);
  242 +
  243 + // 3. 执行规则场景
233 TenantUtils.execute(scene.getTenantId(), 244 TenantUtils.execute(scene.getTenantId(),
234 () -> executeSceneRuleAction(null, ListUtil.toList(scene))); 245 () -> executeSceneRuleAction(null, ListUtil.toList(scene)));
235 } 246 }
236 247
237 /** 248 /**
  249 + * 评估定时触发器的条件组
  250 + *
  251 + * @param scene 场景规则
  252 + * @param trigger 定时触发器
  253 + * @return 是否满足条件
  254 + */
  255 + private boolean evaluateTimerConditionGroups(IotSceneRuleDO scene, IotSceneRuleDO.Trigger trigger) {
  256 + // 1. 如果没有条件组,直接返回 true(直接执行动作)
  257 + if (CollUtil.isEmpty(trigger.getConditionGroups())) {
  258 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 无条件组配置,直接执行]", scene.getId());
  259 + return true;
  260 + }
  261 +
  262 + // 2. 条件组之间是 OR 关系,任一条件组满足即可
  263 + for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
  264 + if (evaluateSingleConditionGroup(scene, conditionGroup)) {
  265 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 条件组匹配成功]", scene.getId());
  266 + return true;
  267 + }
  268 + }
  269 +
  270 + // 3. 所有条件组都不满足
  271 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 所有条件组都不满足]", scene.getId());
  272 + return false;
  273 + }
  274 +
  275 + /**
  276 + * 评估单个条件组
  277 + *
  278 + * @param scene 场景规则
  279 + * @param conditionGroup 条件组
  280 + * @return 是否满足条件
  281 + */
  282 + private boolean evaluateSingleConditionGroup(IotSceneRuleDO scene,
  283 + List<IotSceneRuleDO.TriggerCondition> conditionGroup) {
  284 + // 1. 空条件组视为满足
  285 + if (CollUtil.isEmpty(conditionGroup)) {
  286 + return true;
  287 + }
  288 +
  289 + // 2. 条件之间是 AND 关系,所有条件都必须满足
  290 + for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
  291 + if (!evaluateTimerCondition(scene, condition)) {
  292 + log.debug("[evaluateSingleConditionGroup][规则场景({}) 条件({}) 不满足]",
  293 + scene.getId(), condition);
  294 + return false;
  295 + }
  296 + }
  297 +
  298 + return true;
  299 + }
  300 +
  301 + /**
  302 + * 评估单个条件(定时触发器专用)
  303 + *
  304 + * @param scene 场景规则
  305 + * @param condition 条件
  306 + * @return 是否满足条件
  307 + */
  308 + private boolean evaluateTimerCondition(IotSceneRuleDO scene, IotSceneRuleDO.TriggerCondition condition) {
  309 + try {
  310 + boolean result = timerConditionEvaluator.evaluate(condition);
  311 + log.debug("[evaluateTimerCondition][规则场景({}) 条件类型({}) 评估结果: {}]",
  312 + scene.getId(), condition.getType(), result);
  313 + return result;
  314 + } catch (Exception e) {
  315 + log.error("[evaluateTimerCondition][规则场景({}) 条件评估异常]", scene.getId(), e);
  316 + return false;
  317 + }
  318 + }
  319 +
  320 + /**
238 * 基于消息,获得匹配的规则场景列表 321 * 基于消息,获得匹配的规则场景列表
239 * 322 *
240 * @param message 设备消息 323 * @param message 设备消息
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.text.CharPool;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.time.LocalDateTime;
  12 +import java.time.LocalTime;
  13 +import java.time.ZoneId;
  14 +import java.time.format.DateTimeFormatter;
  15 +import java.util.List;
  16 +
  17 +/**
  18 + * IoT 场景规则时间匹配工具类
  19 + * <p>
  20 + * 提供时间条件匹配的通用方法,供 {@link IotCurrentTimeConditionMatcher} 和 {@link IotTimerConditionEvaluator} 共同使用。
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Slf4j
  25 +public class IotSceneRuleTimeHelper {
  26 +
  27 + /**
  28 + * 时间格式化器 - HH:mm:ss
  29 + */
  30 + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
  31 +
  32 + /**
  33 + * 时间格式化器 - HH:mm
  34 + */
  35 + private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm");
  36 +
  37 + // TODO @puhui999:可以使用 lombok 简化
  38 + private IotSceneRuleTimeHelper() {
  39 + // 工具类,禁止实例化
  40 + }
  41 +
  42 + /**
  43 + * 判断是否为日期时间操作符
  44 + *
  45 + * @param operatorEnum 操作符枚举
  46 + * @return 是否为日期时间操作符
  47 + */
  48 + public static boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {
  49 + return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN
  50 + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN
  51 + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN;
  52 + }
  53 +
  54 + /**
  55 + * 判断是否为时间操作符(包括日期时间操作符和当日时间操作符)
  56 + *
  57 + * @param operatorEnum 操作符枚举
  58 + * @return 是否为时间操作符
  59 + */
  60 + public static boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {
  61 + return operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN
  62 + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN
  63 + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_BETWEEN
  64 + && !isDateTimeOperator(operatorEnum);
  65 + }
  66 +
  67 + /**
  68 + * 执行时间匹配逻辑
  69 + *
  70 + * @param operatorEnum 操作符枚举
  71 + * @param param 参数值
  72 + * @return 是否匹配
  73 + */
  74 + public static boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) {
  75 + try {
  76 + LocalDateTime now = LocalDateTime.now();
  77 + if (isDateTimeOperator(operatorEnum)) {
  78 + // 日期时间匹配(时间戳,秒级)
  79 + long currentTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond();
  80 + return matchDateTime(currentTimestamp, operatorEnum, param);
  81 + } else {
  82 + // 当日时间匹配(HH:mm:ss)
  83 + return matchTime(now.toLocalTime(), operatorEnum, param);
  84 + }
  85 + } catch (Exception e) {
  86 + log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e);
  87 + return false;
  88 + }
  89 + }
  90 +
  91 + /**
  92 + * 匹配日期时间(时间戳,秒级)
  93 + *
  94 + * @param currentTimestamp 当前时间戳
  95 + * @param operatorEnum 操作符枚举
  96 + * @param param 参数值
  97 + * @return 是否匹配
  98 + */
  99 + @SuppressWarnings("EnhancedSwitchMigration")
  100 + public static boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum,
  101 + String param) {
  102 + try {
  103 + // DATE_TIME_BETWEEN 需要解析两个时间戳,单独处理
  104 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN) {
  105 + return matchDateTimeBetween(currentTimestamp, param);
  106 + }
  107 + // 其他操作符只需要解析一个时间戳
  108 + long targetTimestamp = Long.parseLong(param);
  109 + switch (operatorEnum) {
  110 + case DATE_TIME_GREATER_THAN:
  111 + return currentTimestamp > targetTimestamp;
  112 + case DATE_TIME_LESS_THAN:
  113 + return currentTimestamp < targetTimestamp;
  114 + default:
  115 + log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum);
  116 + return false;
  117 + }
  118 + } catch (Exception e) {
  119 + log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e);
  120 + return false;
  121 + }
  122 + }
  123 +
  124 + /**
  125 + * 匹配日期时间区间
  126 + *
  127 + * @param currentTimestamp 当前时间戳
  128 + * @param param 参数值(格式:startTimestamp,endTimestamp)
  129 + * @return 是否匹配
  130 + */
  131 + public static boolean matchDateTimeBetween(long currentTimestamp, String param) {
  132 + List<String> timestampRange = StrUtil.splitTrim(param, CharPool.COMMA);
  133 + if (timestampRange.size() != 2) {
  134 + log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param);
  135 + return false;
  136 + }
  137 + long startTimestamp = Long.parseLong(timestampRange.get(0).trim());
  138 + long endTimestamp = Long.parseLong(timestampRange.get(1).trim());
  139 + // TODO @puhui999:hutool 里,看看有没 between 方法
  140 + return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;
  141 + }
  142 +
  143 + /**
  144 + * 匹配当日时间(HH:mm:ss 或 HH:mm)
  145 + *
  146 + * @param currentTime 当前时间
  147 + * @param operatorEnum 操作符枚举
  148 + * @param param 参数值
  149 + * @return 是否匹配
  150 + */
  151 + @SuppressWarnings("EnhancedSwitchMigration")
  152 + public static boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum,
  153 + String param) {
  154 + try {
  155 + // TIME_BETWEEN 需要解析两个时间,单独处理
  156 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN) {
  157 + return matchTimeBetween(currentTime, param);
  158 + }
  159 + // 其他操作符只需要解析一个时间
  160 + LocalTime targetTime = parseTime(param);
  161 + switch (operatorEnum) {
  162 + case TIME_GREATER_THAN:
  163 + return currentTime.isAfter(targetTime);
  164 + case TIME_LESS_THAN:
  165 + return currentTime.isBefore(targetTime);
  166 + default:
  167 + log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum);
  168 + return false;
  169 + }
  170 + } catch (Exception e) {
  171 + log.error("[matchTime][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e);
  172 + return false;
  173 + }
  174 + }
  175 +
  176 + /**
  177 + * 匹配时间区间
  178 + *
  179 + * @param currentTime 当前时间
  180 + * @param param 参数值(格式:startTime,endTime)
  181 + * @return 是否匹配
  182 + */
  183 + public static boolean matchTimeBetween(LocalTime currentTime, String param) {
  184 + List<String> timeRange = StrUtil.splitTrim(param, CharPool.COMMA);
  185 + if (timeRange.size() != 2) {
  186 + log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param);
  187 + return false;
  188 + }
  189 + LocalTime startTime = parseTime(timeRange.get(0).trim());
  190 + LocalTime endTime = parseTime(timeRange.get(1).trim());
  191 + // TODO @puhui999:hutool 里,看看有没 between 方法
  192 + return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);
  193 + }
  194 +
  195 + /**
  196 + * 解析时间字符串
  197 + * 支持 HH:mm 和 HH:mm:ss 两种格式
  198 + *
  199 + * @param timeStr 时间字符串
  200 + * @return 解析后的 LocalTime
  201 + */
  202 + public static LocalTime parseTime(String timeStr) {
  203 + Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空");
  204 + try {
  205 + // 尝试不同的时间格式
  206 + if (timeStr.length() == 5) { // HH:mm
  207 + return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT);
  208 + } else if (timeStr.length() == 8) { // HH:mm:ss
  209 + return LocalTime.parse(timeStr, TIME_FORMATTER);
  210 + } else {
  211 + throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式");
  212 + }
  213 + } catch (Exception e) {
  214 + log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e);
  215 + throw new IllegalArgumentException("时间格式无效: " + timeStr, e);
  216 + }
  217 + }
  218 +
  219 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.hutool.core.lang.Assert;  
4 -import cn.hutool.core.text.CharPool;  
5 -import cn.hutool.core.util.StrUtil;  
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper;
10 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
11 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
12 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
13 11
14 -import java.time.LocalDateTime;  
15 -import java.time.LocalTime;  
16 -import java.time.format.DateTimeFormatter;  
17 -import java.util.List;  
18 -  
19 /** 12 /**
20 * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑 13 * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
21 * 14 *
@@ -25,16 +18,6 @@ import java.util.List; @@ -25,16 +18,6 @@ import java.util.List;
25 @Slf4j 18 @Slf4j
26 public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { 19 public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
27 20
28 - /**  
29 - * 时间格式化器 - HH:mm:ss  
30 - */  
31 - private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");  
32 -  
33 - /**  
34 - * 时间格式化器 - HH:mm  
35 - */  
36 - private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm");  
37 -  
38 @Override 21 @Override
39 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 22 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
40 return IotSceneRuleConditionTypeEnum.CURRENT_TIME; 23 return IotSceneRuleConditionTypeEnum.CURRENT_TIME;
@@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc @@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc
62 return false; 45 return false;
63 } 46 }
64 47
65 - if (!isTimeOperator(operatorEnum)) { 48 + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) {
66 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); 49 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator);
67 return false; 50 return false;
68 } 51 }
69 52
70 // 2.1 执行时间匹配 53 // 2.1 执行时间匹配
71 - boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); 54 + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam());
72 55
73 // 2.2 记录匹配结果 56 // 2.2 记录匹配结果
74 if (matched) { 57 if (matched) {
@@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc @@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc
80 return matched; 63 return matched;
81 } 64 }
82 65
83 - /**  
84 - * 执行时间匹配逻辑  
85 - * 直接实现时间条件匹配,不使用 Spring EL 表达式  
86 - */  
87 - private boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
88 - try {  
89 - LocalDateTime now = LocalDateTime.now();  
90 -  
91 - if (isDateTimeOperator(operatorEnum)) {  
92 - // 日期时间匹配(时间戳)  
93 - long currentTimestamp = now.toEpochSecond(java.time.ZoneOffset.of("+8"));  
94 - return matchDateTime(currentTimestamp, operatorEnum, param);  
95 - } else {  
96 - // 当日时间匹配(HH:mm:ss)  
97 - return matchTime(now.toLocalTime(), operatorEnum, param);  
98 - }  
99 - } catch (Exception e) {  
100 - log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e);  
101 - return false;  
102 - }  
103 - }  
104 -  
105 - /**  
106 - * 判断是否为日期时间操作符  
107 - */  
108 - private boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {  
109 - return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN ||  
110 - operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN ||  
111 - operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN;  
112 - }  
113 -  
114 - /**  
115 - * 判断是否为时间操作符  
116 - */  
117 - private boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {  
118 - return operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN ||  
119 - operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN ||  
120 - operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN ||  
121 - isDateTimeOperator(operatorEnum);  
122 - }  
123 -  
124 - /**  
125 - * 匹配日期时间(时间戳)  
126 - * 直接实现时间戳比较逻辑  
127 - */  
128 - private boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
129 - try {  
130 - long targetTimestamp = Long.parseLong(param);  
131 - switch (operatorEnum) {  
132 - case DATE_TIME_GREATER_THAN:  
133 - return currentTimestamp > targetTimestamp;  
134 - case DATE_TIME_LESS_THAN:  
135 - return currentTimestamp < targetTimestamp;  
136 - case DATE_TIME_BETWEEN:  
137 - return matchDateTimeBetween(currentTimestamp, param);  
138 - default:  
139 - log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum);  
140 - return false;  
141 - }  
142 - } catch (Exception e) {  
143 - log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e);  
144 - return false;  
145 - }  
146 - }  
147 -  
148 - /**  
149 - * 匹配日期时间区间  
150 - */  
151 - private boolean matchDateTimeBetween(long currentTimestamp, String param) {  
152 - List<String> timestampRange = StrUtil.splitTrim(param, CharPool.COMMA);  
153 - if (timestampRange.size() != 2) {  
154 - log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param);  
155 - return false;  
156 - }  
157 - long startTimestamp = Long.parseLong(timestampRange.get(0).trim());  
158 - long endTimestamp = Long.parseLong(timestampRange.get(1).trim());  
159 - return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;  
160 - }  
161 -  
162 - /**  
163 - * 匹配当日时间(HH:mm:ss)  
164 - * 直接实现时间比较逻辑  
165 - */  
166 - private boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
167 - try {  
168 - LocalTime targetTime = parseTime(param);  
169 - switch (operatorEnum) {  
170 - case TIME_GREATER_THAN:  
171 - return currentTime.isAfter(targetTime);  
172 - case TIME_LESS_THAN:  
173 - return currentTime.isBefore(targetTime);  
174 - case TIME_BETWEEN:  
175 - return matchTimeBetween(currentTime, param);  
176 - default:  
177 - log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum);  
178 - return false;  
179 - }  
180 - } catch (Exception e) {  
181 - log.error("[matchTime][][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e);  
182 - return false;  
183 - }  
184 - }  
185 -  
186 - /**  
187 - * 匹配时间区间  
188 - */  
189 - private boolean matchTimeBetween(LocalTime currentTime, String param) {  
190 - List<String> timeRange = StrUtil.splitTrim(param, CharPool.COMMA);  
191 - if (timeRange.size() != 2) {  
192 - log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param);  
193 - return false;  
194 - }  
195 - LocalTime startTime = parseTime(timeRange.get(0).trim());  
196 - LocalTime endTime = parseTime(timeRange.get(1).trim());  
197 - return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);  
198 - }  
199 -  
200 - /**  
201 - * 解析时间字符串  
202 - * 支持 HH:mm 和 HH:mm:ss 两种格式  
203 - */  
204 - private LocalTime parseTime(String timeStr) {  
205 - Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空");  
206 -  
207 - try {  
208 - // 尝试不同的时间格式  
209 - if (timeStr.length() == 5) { // HH:mm  
210 - return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT);  
211 - } else if (timeStr.length() == 8) { // HH:mm:ss  
212 - return LocalTime.parse(timeStr, TIME_FORMATTER);  
213 - } else {  
214 - throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式");  
215 - }  
216 - } catch (Exception e) {  
217 - log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e);  
218 - throw new IllegalArgumentException("时间格式无效: " + timeStr, e);  
219 - }  
220 - }  
221 -  
222 @Override 66 @Override
223 public int getPriority() { 67 public int getPriority() {
224 return 40; // 较低优先级 68 return 40; // 较低优先级
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
@@ -38,8 +38,7 @@ public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerM @@ -38,8 +38,7 @@ public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerM
38 38
39 // 1.3 检查消息中是否包含触发器指定的属性标识符 39 // 1.3 检查消息中是否包含触发器指定的属性标识符
40 // 注意:属性上报可能同时上报多个属性,所以需要判断 trigger.getIdentifier() 是否在 message 的 params 中 40 // 注意:属性上报可能同时上报多个属性,所以需要判断 trigger.getIdentifier() 是否在 message 的 params 中
41 - // TODO @puhui999:可以考虑 notXXX 方法,简化代码(尽量取反)  
42 - if (!IotDeviceMessageUtils.containsIdentifier(message, trigger.getIdentifier())) { 41 + if (IotDeviceMessageUtils.notContainsIdentifier(message, trigger.getIdentifier())) {
43 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中不包含属性: " + 42 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中不包含属性: " +
44 trigger.getIdentifier()); 43 trigger.getIdentifier());
45 return false; 44 return false;
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 6 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
@@ -8,6 +9,8 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; @@ -8,6 +9,8 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
9 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
10 11
  12 +import java.util.Map;
  13 +
11 /** 14 /**
12 * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑 15 * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
13 * 16 *
@@ -28,13 +31,11 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger @@ -28,13 +31,11 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger
28 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); 31 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效");
29 return false; 32 return false;
30 } 33 }
31 -  
32 // 1.2 检查消息方法是否匹配 34 // 1.2 检查消息方法是否匹配
33 if (!IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod().equals(message.getMethod())) { 35 if (!IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod().equals(message.getMethod())) {
34 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod() + ", 实际: " + message.getMethod()); 36 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod() + ", 实际: " + message.getMethod());
35 return false; 37 return false;
36 } 38 }
37 -  
38 // 1.3 检查标识符是否匹配 39 // 1.3 检查标识符是否匹配
39 String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); 40 String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
40 if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { 41 if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
@@ -42,13 +43,58 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger @@ -42,13 +43,58 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger
42 return false; 43 return false;
43 } 44 }
44 45
45 - // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可  
46 - // 不需要检查操作符和值,因为服务调用本身就是触发条件  
47 - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件? 46 + // 2. 检查是否配置了参数条件
  47 + if (hasParameterCondition(trigger)) {
  48 + return matchParameterCondition(message, trigger);
  49 + }
  50 +
  51 + // 3. 无参数条件时,标识符匹配即成功
48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 52 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
49 return true; 53 return true;
50 } 54 }
51 55
  56 + /**
  57 + * 判断触发器是否配置了参数条件
  58 + *
  59 + * @param trigger 触发器配置
  60 + * @return 是否配置了参数条件
  61 + */
  62 + private boolean hasParameterCondition(IotSceneRuleDO.Trigger trigger) {
  63 + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue());
  64 + }
  65 +
  66 + /**
  67 + * 匹配参数条件
  68 + *
  69 + * @param message 设备消息
  70 + * @param trigger 触发器配置
  71 + * @return 是否匹配
  72 + */
  73 + private boolean matchParameterCondition(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  74 + // 1.1 从消息中提取服务调用的输入参数
  75 + Map<String, Object> inputParams = IotDeviceMessageUtils.extractServiceInputParams(message);
  76 + // TODO @puhui999:要考虑 empty 的情况么?
  77 + if (inputParams == null) {
  78 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中缺少服务输入参数");
  79 + return false;
  80 + }
  81 + // 1.2 获取要匹配的参数值(使用 identifier 作为参数名)
  82 + Object paramValue = inputParams.get(trigger.getIdentifier());
  83 + if (paramValue == null) {
  84 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "服务输入参数中缺少指定参数: " + trigger.getIdentifier());
  85 + return false;
  86 + }
  87 +
  88 + // 2. 使用条件评估器进行匹配
  89 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(paramValue, trigger.getOperator(), trigger.getValue());
  90 + if (matched) {
  91 + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
  92 + } else {
  93 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "服务输入参数条件不匹配");
  94 + }
  95 + return matched;
  96 + }
  97 +
52 @Override 98 @Override
53 public int getPriority() { 99 public int getPriority() {
54 return 40; // 较低优先级 100 return 40; // 较低优先级
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  10 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  11 +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
  12 +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper;
  13 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
  14 +import jakarta.annotation.Resource;
  15 +import lombok.extern.slf4j.Slf4j;
  16 +import org.springframework.stereotype.Component;
  17 +
  18 +import java.util.Map;
  19 +
  20 +/**
  21 + * IoT 定时触发器条件评估器
  22 + * <p>
  23 + * 与设备触发器不同,定时触发器没有设备消息上下文,
  24 + * 需要主动查询设备属性和状态来评估条件。
  25 + *
  26 + * @author HUIHUI
  27 + */
  28 +@Component
  29 +@Slf4j
  30 +public class IotTimerConditionEvaluator {
  31 +
  32 + @Resource
  33 + private IotDevicePropertyService devicePropertyService;
  34 +
  35 + @Resource
  36 + private IotDeviceService deviceService;
  37 +
  38 + /**
  39 + * 评估条件
  40 + *
  41 + * @param condition 条件配置
  42 + * @return 是否满足条件
  43 + */
  44 + @SuppressWarnings("EnhancedSwitchMigration")
  45 + public boolean evaluate(IotSceneRuleDO.TriggerCondition condition) {
  46 + // 1.1 基础参数校验
  47 + if (condition == null || condition.getType() == null) {
  48 + log.warn("[evaluate][条件为空或类型为空]");
  49 + return false;
  50 + }
  51 + // 1.2 根据条件类型分发到具体的评估方法
  52 + IotSceneRuleConditionTypeEnum conditionType =
  53 + IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
  54 + if (conditionType == null) {
  55 + log.warn("[evaluate][未知的条件类型: {}]", condition.getType());
  56 + return false;
  57 + }
  58 +
  59 + // 2. 分发评估
  60 + switch (conditionType) {
  61 + case DEVICE_PROPERTY:
  62 + return evaluateDevicePropertyCondition(condition);
  63 + case DEVICE_STATE:
  64 + return evaluateDeviceStateCondition(condition);
  65 + case CURRENT_TIME:
  66 + return evaluateCurrentTimeCondition(condition);
  67 + default:
  68 + log.warn("[evaluate][未知的条件类型: {}]", conditionType);
  69 + return false;
  70 + }
  71 + }
  72 +
  73 + /**
  74 + * 评估设备属性条件
  75 + *
  76 + * @param condition 条件配置
  77 + * @return 是否满足条件
  78 + */
  79 + private boolean evaluateDevicePropertyCondition(IotSceneRuleDO.TriggerCondition condition) {
  80 + // 1. 校验必要参数
  81 + if (condition.getDeviceId() == null) {
  82 + log.debug("[evaluateDevicePropertyCondition][设备ID为空]");
  83 + return false;
  84 + }
  85 + if (StrUtil.isBlank(condition.getIdentifier())) {
  86 + log.debug("[evaluateDevicePropertyCondition][属性标识符为空]");
  87 + return false;
  88 + }
  89 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  90 + log.debug("[evaluateDevicePropertyCondition][操作符或参数无效]");
  91 + return false;
  92 + }
  93 +
  94 + // 2.1 获取设备最新属性值
  95 + Map<String, IotDevicePropertyDO> properties =
  96 + devicePropertyService.getLatestDeviceProperties(condition.getDeviceId());
  97 + if (CollUtil.isEmpty(properties)) {
  98 + log.debug("[evaluateDevicePropertyCondition][设备({}) 无属性数据]", condition.getDeviceId());
  99 + return false;
  100 + }
  101 + // 2.2 获取指定属性
  102 + IotDevicePropertyDO property = properties.get(condition.getIdentifier());
  103 + if (property == null || property.getValue() == null) {
  104 + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 不存在或值为空]",
  105 + condition.getDeviceId(), condition.getIdentifier());
  106 + return false;
  107 + }
  108 +
  109 + // 3. 使用现有的条件评估逻辑进行比较
  110 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(
  111 + property.getValue(), condition.getOperator(), condition.getParam());
  112 + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 值({}) 操作符({}) 参数({}) 匹配结果: {}]",
  113 + condition.getDeviceId(), condition.getIdentifier(), property.getValue(),
  114 + condition.getOperator(), condition.getParam(), matched);
  115 + return matched;
  116 + }
  117 +
  118 + /**
  119 + * 评估设备状态条件
  120 + *
  121 + * @param condition 条件配置
  122 + * @return 是否满足条件
  123 + */
  124 + private boolean evaluateDeviceStateCondition(IotSceneRuleDO.TriggerCondition condition) {
  125 + // 1. 校验必要参数
  126 + if (condition.getDeviceId() == null) {
  127 + log.debug("[evaluateDeviceStateCondition][设备ID为空]");
  128 + return false;
  129 + }
  130 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  131 + log.debug("[evaluateDeviceStateCondition][操作符或参数无效]");
  132 + return false;
  133 + }
  134 +
  135 + // 2.1 获取设备信息
  136 + IotDeviceDO device = deviceService.getDevice(condition.getDeviceId());
  137 + if (device == null) {
  138 + log.debug("[evaluateDeviceStateCondition][设备({}) 不存在]", condition.getDeviceId());
  139 + return false;
  140 + }
  141 + // 2.2 获取设备状态
  142 + Integer state = device.getState();
  143 + if (state == null) {
  144 + log.debug("[evaluateDeviceStateCondition][设备({}) 状态为空]", condition.getDeviceId());
  145 + return false;
  146 + }
  147 +
  148 + // 3. 比较状态
  149 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(
  150 + state.toString(), condition.getOperator(), condition.getParam());
  151 + log.debug("[evaluateDeviceStateCondition][设备({}) 状态({}) 操作符({}) 参数({}) 匹配结果: {}]",
  152 + condition.getDeviceId(), state, condition.getOperator(), condition.getParam(), matched);
  153 + return matched;
  154 + }
  155 +
  156 + /**
  157 + * 评估当前时间条件
  158 + *
  159 + * @param condition 条件配置
  160 + * @return 是否满足条件
  161 + */
  162 + private boolean evaluateCurrentTimeCondition(IotSceneRuleDO.TriggerCondition condition) {
  163 + // 1.1 校验必要参数
  164 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  165 + log.debug("[evaluateCurrentTimeCondition][操作符或参数无效]");
  166 + return false;
  167 + }
  168 + // 1.2 验证操作符是否为支持的时间操作符
  169 + IotSceneRuleConditionOperatorEnum operatorEnum =
  170 + IotSceneRuleConditionOperatorEnum.operatorOf(condition.getOperator());
  171 + if (operatorEnum == null) {
  172 + log.debug("[evaluateCurrentTimeCondition][无效的操作符: {}]", condition.getOperator());
  173 + return false;
  174 + }
  175 + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) {
  176 + log.debug("[evaluateCurrentTimeCondition][不支持的时间操作符: {}]", condition.getOperator());
  177 + return false;
  178 + }
  179 +
  180 + // 2. 执行时间匹配
  181 + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam());
  182 + log.debug("[evaluateCurrentTimeCondition][操作符({}) 参数({}) 匹配结果: {}]",
  183 + condition.getOperator(), condition.getParam(), matched);
  184 + return matched;
  185 + }
  186 +
  187 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
  2 +
  3 +import cn.hutool.core.util.ReflectUtil;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
  5 +import org.junit.jupiter.api.Test;
  6 +
  7 +import static org.junit.jupiter.api.Assertions.*;
  8 +
  9 +/**
  10 + * {@link IotTcpClient} 的单元测试
  11 + * <p>
  12 + * 测试 dataFormat 默认值行为
  13 + * Property 1: TCP 客户端 dataFormat 默认值行为
  14 + * Validates: Requirements 1.1, 1.2
  15 + *
  16 + * @author HUIHUI
  17 + */
  18 +class IotTcpClientTest {
  19 +
  20 + @Test
  21 + public void testConstructor_dataFormatNull() {
  22 + // 准备参数
  23 + String host = "localhost";
  24 + Integer port = 8080;
  25 +
  26 + // 调用
  27 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, null);
  28 +
  29 + // 断言:dataFormat 为 null 时应使用默认值
  30 + assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT,
  31 + ReflectUtil.getFieldValue(client, "dataFormat"));
  32 + }
  33 +
  34 + @Test
  35 + public void testConstructor_dataFormatEmpty() {
  36 + // 准备参数
  37 + String host = "localhost";
  38 + Integer port = 8080;
  39 +
  40 + // 调用
  41 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, "");
  42 +
  43 + // 断言:dataFormat 为空字符串时应使用默认值
  44 + assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT,
  45 + ReflectUtil.getFieldValue(client, "dataFormat"));
  46 + }
  47 +
  48 + @Test
  49 + public void testConstructor_dataFormatBlank() {
  50 + // 准备参数
  51 + String host = "localhost";
  52 + Integer port = 8080;
  53 +
  54 + // 调用
  55 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, " ");
  56 +
  57 + // 断言:dataFormat 为纯空白字符串时应使用默认值
  58 + assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT,
  59 + ReflectUtil.getFieldValue(client, "dataFormat"));
  60 + }
  61 +
  62 + @Test
  63 + public void testConstructor_dataFormatValid() {
  64 + // 准备参数
  65 + String host = "localhost";
  66 + Integer port = 8080;
  67 + String dataFormat = "BINARY";
  68 +
  69 + // 调用
  70 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, dataFormat);
  71 +
  72 + // 断言:dataFormat 为有效值时应保持原值
  73 + assertEquals(dataFormat, ReflectUtil.getFieldValue(client, "dataFormat"));
  74 + }
  75 +
  76 + @Test
  77 + public void testConstructor_defaultValues() {
  78 + // 准备参数
  79 + String host = "localhost";
  80 + Integer port = 8080;
  81 +
  82 + // 调用
  83 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, null);
  84 +
  85 + // 断言:验证所有默认值
  86 + assertEquals(host, ReflectUtil.getFieldValue(client, "host"));
  87 + assertEquals(port, ReflectUtil.getFieldValue(client, "port"));
  88 + assertEquals(IotDataSinkTcpConfig.DEFAULT_CONNECT_TIMEOUT_MS,
  89 + ReflectUtil.getFieldValue(client, "connectTimeoutMs"));
  90 + assertEquals(IotDataSinkTcpConfig.DEFAULT_READ_TIMEOUT_MS,
  91 + ReflectUtil.getFieldValue(client, "readTimeoutMs"));
  92 + assertEquals(IotDataSinkTcpConfig.DEFAULT_SSL,
  93 + ReflectUtil.getFieldValue(client, "ssl"));
  94 + assertEquals(IotDataSinkTcpConfig.DEFAULT_DATA_FORMAT,
  95 + ReflectUtil.getFieldValue(client, "dataFormat"));
  96 + }
  97 +
  98 + @Test
  99 + public void testConstructor_customValues() {
  100 + // 准备参数
  101 + String host = "192.168.1.100";
  102 + Integer port = 9090;
  103 + Integer connectTimeoutMs = 3000;
  104 + Integer readTimeoutMs = 8000;
  105 + Boolean ssl = true;
  106 + String dataFormat = "BINARY";
  107 +
  108 + // 调用
  109 + IotTcpClient client = new IotTcpClient(host, port, connectTimeoutMs, readTimeoutMs, ssl, dataFormat);
  110 +
  111 + // 断言:验证自定义值
  112 + assertEquals(host, ReflectUtil.getFieldValue(client, "host"));
  113 + assertEquals(port, ReflectUtil.getFieldValue(client, "port"));
  114 + assertEquals(connectTimeoutMs, ReflectUtil.getFieldValue(client, "connectTimeoutMs"));
  115 + assertEquals(readTimeoutMs, ReflectUtil.getFieldValue(client, "readTimeoutMs"));
  116 + assertEquals(ssl, ReflectUtil.getFieldValue(client, "ssl"));
  117 + assertEquals(dataFormat, ReflectUtil.getFieldValue(client, "dataFormat"));
  118 + }
  119 +
  120 + @Test
  121 + public void testIsConnected_initialState() {
  122 + // 准备参数
  123 + String host = "localhost";
  124 + Integer port = 8080;
  125 +
  126 + // 调用
  127 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, null);
  128 +
  129 + // 断言:初始状态应为未连接
  130 + assertFalse(client.isConnected());
  131 + }
  132 +
  133 + @Test
  134 + public void testToString() {
  135 + // 准备参数
  136 + String host = "localhost";
  137 + Integer port = 8080;
  138 +
  139 + // 调用
  140 + IotTcpClient client = new IotTcpClient(host, port, null, null, null, null);
  141 + String result = client.toString();
  142 +
  143 + // 断言
  144 + assertNotNull(result);
  145 + assertTrue(result.contains("host='localhost'"));
  146 + assertTrue(result.contains("port=8080"));
  147 + assertTrue(result.contains("dataFormat='JSON'"));
  148 + assertTrue(result.contains("connected=false"));
  149 + }
  150 +
  151 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action.websocket;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import okhttp3.Response;
  5 +import okhttp3.WebSocket;
  6 +import okhttp3.WebSocketListener;
  7 +import okhttp3.mockwebserver.MockResponse;
  8 +import okhttp3.mockwebserver.MockWebServer;
  9 +import org.jetbrains.annotations.NotNull;
  10 +import org.jetbrains.annotations.Nullable;
  11 +import org.junit.jupiter.api.AfterEach;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +
  15 +import static org.junit.jupiter.api.Assertions.*;
  16 +
  17 +/**
  18 + * {@link IotWebSocketClient} 的单元测试
  19 + *
  20 + * @author HUIHUI
  21 + */
  22 +class IotWebSocketClientTest {
  23 +
  24 + private MockWebServer mockWebServer;
  25 +
  26 + @BeforeEach
  27 + public void setUp() throws Exception {
  28 + mockWebServer = new MockWebServer();
  29 + mockWebServer.start();
  30 + }
  31 +
  32 + @AfterEach
  33 + public void tearDown() throws Exception {
  34 + if (mockWebServer != null) {
  35 + mockWebServer.shutdown();
  36 + }
  37 + }
  38 +
  39 + /**
  40 + * 简单的 WebSocket 监听器,用于测试
  41 + */
  42 + private static class TestWebSocketListener extends WebSocketListener {
  43 + @Override
  44 + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
  45 + // 连接打开
  46 + }
  47 +
  48 + @Override
  49 + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
  50 + // 收到消息
  51 + }
  52 +
  53 + @Override
  54 + public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
  55 + webSocket.close(code, reason);
  56 + }
  57 +
  58 + @Override
  59 + public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
  60 + // 连接失败
  61 + }
  62 + }
  63 +
  64 + @Test
  65 + public void testConstructor_defaultValues() {
  66 + // 准备参数
  67 + String serverUrl = "ws://localhost:8080";
  68 +
  69 + // 调用
  70 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, null, null, null);
  71 +
  72 + // 断言:验证默认值被正确设置
  73 + assertNotNull(client);
  74 + assertFalse(client.isConnected());
  75 + }
  76 +
  77 + @Test
  78 + public void testConstructor_customValues() {
  79 + // 准备参数
  80 + String serverUrl = "ws://localhost:8080";
  81 + Integer connectTimeoutMs = 3000;
  82 + Integer sendTimeoutMs = 5000;
  83 + String dataFormat = "TEXT";
  84 +
  85 + // 调用
  86 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, connectTimeoutMs, sendTimeoutMs, dataFormat);
  87 +
  88 + // 断言
  89 + assertNotNull(client);
  90 + assertFalse(client.isConnected());
  91 + }
  92 +
  93 + @Test
  94 + public void testConnect_success() throws Exception {
  95 + // 准备参数:使用 MockWebServer 的 WebSocket 端点
  96 + String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
  97 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  98 +
  99 + // mock:设置 MockWebServer 响应 WebSocket 升级请求
  100 + mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener()));
  101 +
  102 + // 调用
  103 + client.connect();
  104 +
  105 + // 断言
  106 + assertTrue(client.isConnected());
  107 +
  108 + // 清理
  109 + client.close();
  110 + }
  111 +
  112 + @Test
  113 + public void testConnect_alreadyConnected() throws Exception {
  114 + // 准备参数
  115 + String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
  116 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  117 +
  118 + // mock
  119 + mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener()));
  120 +
  121 + // 调用:第一次连接
  122 + client.connect();
  123 + assertTrue(client.isConnected());
  124 +
  125 + // 调用:第二次连接(应该不会重复连接)
  126 + client.connect();
  127 + assertTrue(client.isConnected());
  128 +
  129 + // 清理
  130 + client.close();
  131 + }
  132 +
  133 + @Test
  134 + public void testSendMessage_success() throws Exception {
  135 + // 准备参数
  136 + String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
  137 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  138 +
  139 + IotDeviceMessage message = IotDeviceMessage.builder()
  140 + .deviceId(123L)
  141 + .method("thing.property.report")
  142 + .params("{\"temperature\": 25.5}")
  143 + .build();
  144 +
  145 + // mock
  146 + mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener()));
  147 +
  148 + // 调用
  149 + client.connect();
  150 + client.sendMessage(message);
  151 +
  152 + // 断言:消息发送成功不抛异常
  153 + assertTrue(client.isConnected());
  154 +
  155 + // 清理
  156 + client.close();
  157 + }
  158 +
  159 + @Test
  160 + public void testSendMessage_notConnected() {
  161 + // 准备参数
  162 + String serverUrl = "ws://localhost:8080";
  163 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  164 +
  165 + IotDeviceMessage message = IotDeviceMessage.builder()
  166 + .deviceId(123L)
  167 + .method("thing.property.report")
  168 + .params("{\"temperature\": 25.5}")
  169 + .build();
  170 +
  171 + // 调用 & 断言:未连接时发送消息应抛出异常
  172 + assertThrows(IllegalStateException.class, () -> client.sendMessage(message));
  173 + }
  174 +
  175 + @Test
  176 + public void testClose_success() throws Exception {
  177 + // 准备参数
  178 + String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
  179 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  180 +
  181 + // mock
  182 + mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener()));
  183 +
  184 + // 调用
  185 + client.connect();
  186 + assertTrue(client.isConnected());
  187 +
  188 + client.close();
  189 +
  190 + // 断言
  191 + assertFalse(client.isConnected());
  192 + }
  193 +
  194 + @Test
  195 + public void testClose_notConnected() {
  196 + // 准备参数
  197 + String serverUrl = "ws://localhost:8080";
  198 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  199 +
  200 + // 调用:关闭未连接的客户端不应抛异常
  201 + assertDoesNotThrow(client::close);
  202 + assertFalse(client.isConnected());
  203 + }
  204 +
  205 + @Test
  206 + public void testIsConnected_initialState() {
  207 + // 准备参数
  208 + String serverUrl = "ws://localhost:8080";
  209 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  210 +
  211 + // 断言:初始状态应为未连接
  212 + assertFalse(client.isConnected());
  213 + }
  214 +
  215 + @Test
  216 + public void testToString() {
  217 + // 准备参数
  218 + String serverUrl = "ws://localhost:8080";
  219 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "JSON");
  220 +
  221 + // 调用
  222 + String result = client.toString();
  223 +
  224 + // 断言
  225 + assertNotNull(result);
  226 + assertTrue(result.contains("serverUrl='ws://localhost:8080'"));
  227 + assertTrue(result.contains("dataFormat='JSON'"));
  228 + assertTrue(result.contains("connected=false"));
  229 + }
  230 +
  231 + @Test
  232 + public void testSendMessage_textFormat() throws Exception {
  233 + // 准备参数
  234 + String serverUrl = "ws://" + mockWebServer.getHostName() + ":" + mockWebServer.getPort();
  235 + IotWebSocketClient client = new IotWebSocketClient(serverUrl, 5000, 5000, "TEXT");
  236 +
  237 + IotDeviceMessage message = IotDeviceMessage.builder()
  238 + .deviceId(123L)
  239 + .method("thing.property.report")
  240 + .params("{\"temperature\": 25.5}")
  241 + .build();
  242 +
  243 + // mock
  244 + mockWebServer.enqueue(new MockResponse().withWebSocketUpgrade(new TestWebSocketListener()));
  245 +
  246 + // 调用
  247 + client.connect();
  248 + client.sendMessage(message);
  249 +
  250 + // 断言:消息发送成功不抛异常
  251 + assertTrue(client.isConnected());
  252 +
  253 + // 清理
  254 + client.close();
  255 + }
  256 +
  257 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  10 +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
  11 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  12 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  13 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  14 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  15 +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
  16 +import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
  17 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
  18 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  19 +import org.junit.jupiter.api.BeforeEach;
  20 +import org.junit.jupiter.api.DisplayName;
  21 +import org.junit.jupiter.api.Nested;
  22 +import org.junit.jupiter.api.Test;
  23 +import org.mockito.InjectMocks;
  24 +import org.mockito.Mock;
  25 +
  26 +import java.util.*;
  27 +
  28 +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
  29 +import static org.mockito.ArgumentMatchers.any;
  30 +import static org.mockito.Mockito.*;
  31 +
  32 +/**
  33 + * {@link IotSceneRuleServiceImpl} 定时触发器条件组集成测试
  34 + * <p>
  35 + * 测试定时触发器的条件组评估功能:
  36 + * - 空条件组直接执行动作
  37 + * - 条件组评估后决定是否执行动作
  38 + * - 条件组之间的 OR 逻辑
  39 + * - 条件组内的 AND 逻辑
  40 + * - 所有条件组不满足时跳过执行
  41 + * <p>
  42 + * Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5
  43 + *
  44 + * @author HUIHUI
  45 + */
  46 +public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest {
  47 +
  48 + @InjectMocks
  49 + private IotSceneRuleServiceImpl sceneRuleService;
  50 +
  51 + @Mock
  52 + private IotSceneRuleMapper sceneRuleMapper;
  53 +
  54 + @Mock
  55 + private IotDeviceService deviceService;
  56 +
  57 + @Mock
  58 + private IotDevicePropertyService devicePropertyService;
  59 +
  60 + @Mock
  61 + private List<IotSceneRuleAction> sceneRuleActions;
  62 +
  63 + @Mock
  64 + private IotSceneRuleTimerHandler timerHandler;
  65 +
  66 + private IotTimerConditionEvaluator timerConditionEvaluator;
  67 +
  68 + // 测试常量
  69 + private static final Long SCENE_RULE_ID = 1L;
  70 + private static final Long TENANT_ID = 1L;
  71 + private static final Long DEVICE_ID = 100L;
  72 + private static final String PROPERTY_IDENTIFIER = "temperature";
  73 +
  74 + @BeforeEach
  75 + void setUp() {
  76 + // 创建并注入 timerConditionEvaluator 的依赖
  77 + timerConditionEvaluator = new IotTimerConditionEvaluator();
  78 + try {
  79 + var devicePropertyServiceField = IotTimerConditionEvaluator.class.getDeclaredField("devicePropertyService");
  80 + devicePropertyServiceField.setAccessible(true);
  81 + devicePropertyServiceField.set(timerConditionEvaluator, devicePropertyService);
  82 +
  83 + var deviceServiceField = IotTimerConditionEvaluator.class.getDeclaredField("deviceService");
  84 + deviceServiceField.setAccessible(true);
  85 + deviceServiceField.set(timerConditionEvaluator, deviceService);
  86 +
  87 + var evaluatorField = IotSceneRuleServiceImpl.class.getDeclaredField("timerConditionEvaluator");
  88 + evaluatorField.setAccessible(true);
  89 + evaluatorField.set(sceneRuleService, timerConditionEvaluator);
  90 + } catch (Exception e) {
  91 + throw new RuntimeException("Failed to inject dependencies", e);
  92 + }
  93 + }
  94 +
  95 + // ========== 辅助方法 ==========
  96 +
  97 + private IotSceneRuleDO createBaseSceneRule() {
  98 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  99 + sceneRule.setId(SCENE_RULE_ID);
  100 + sceneRule.setTenantId(TENANT_ID);
  101 + sceneRule.setName("测试定时触发器");
  102 + sceneRule.setStatus(CommonStatusEnum.ENABLE.getStatus());
  103 + sceneRule.setActions(Collections.emptyList());
  104 + return sceneRule;
  105 + }
  106 +
  107 + private IotSceneRuleDO.Trigger createTimerTrigger(String cronExpression,
  108 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups) {
  109 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  110 + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  111 + trigger.setCronExpression(cronExpression);
  112 + trigger.setConditionGroups(conditionGroups);
  113 + return trigger;
  114 + }
  115 +
  116 + private IotSceneRuleDO.TriggerCondition createDevicePropertyCondition(Long deviceId, String identifier,
  117 + String operator, String param) {
  118 + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
  119 + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
  120 + condition.setDeviceId(deviceId);
  121 + condition.setIdentifier(identifier);
  122 + condition.setOperator(operator);
  123 + condition.setParam(param);
  124 + return condition;
  125 + }
  126 +
  127 + private IotSceneRuleDO.TriggerCondition createDeviceStateCondition(Long deviceId, String operator, String param) {
  128 + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
  129 + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType());
  130 + condition.setDeviceId(deviceId);
  131 + condition.setOperator(operator);
  132 + condition.setParam(param);
  133 + return condition;
  134 + }
  135 +
  136 + private void mockDeviceProperty(Long deviceId, String identifier, Object value) {
  137 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  138 + IotDevicePropertyDO property = new IotDevicePropertyDO();
  139 + property.setValue(value);
  140 + properties.put(identifier, property);
  141 + when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties);
  142 + }
  143 +
  144 + private void mockDeviceState(Long deviceId, Integer state) {
  145 + IotDeviceDO device = new IotDeviceDO();
  146 + device.setId(deviceId);
  147 + device.setState(state);
  148 + when(deviceService.getDevice(deviceId)).thenReturn(device);
  149 + }
  150 +
  151 + /**
  152 + * 创建单条件的条件组列表
  153 + */
  154 + private List<List<IotSceneRuleDO.TriggerCondition>> createSingleConditionGroups(
  155 + IotSceneRuleDO.TriggerCondition condition) {
  156 + List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>();
  157 + group.add(condition);
  158 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  159 + groups.add(group);
  160 + return groups;
  161 + }
  162 +
  163 + /**
  164 + * 创建两个单条件组的条件组列表
  165 + */
  166 + private List<List<IotSceneRuleDO.TriggerCondition>> createTwoSingleConditionGroups(
  167 + IotSceneRuleDO.TriggerCondition cond1, IotSceneRuleDO.TriggerCondition cond2) {
  168 + List<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
  169 + group1.add(cond1);
  170 + List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
  171 + group2.add(cond2);
  172 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  173 + groups.add(group1);
  174 + groups.add(group2);
  175 + return groups;
  176 + }
  177 +
  178 + /**
  179 + * 创建单个多条件组的条件组列表
  180 + */
  181 + private List<List<IotSceneRuleDO.TriggerCondition>> createSingleGroupWithMultipleConditions(
  182 + IotSceneRuleDO.TriggerCondition... conditions) {
  183 + List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>(Arrays.asList(conditions));
  184 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  185 + groups.add(group);
  186 + return groups;
  187 + }
  188 +
  189 + // ========== 测试用例 ==========
  190 +
  191 + @Nested
  192 + @DisplayName("空条件组测试 - Validates: Requirement 2.1")
  193 + class EmptyConditionGroupsTest {
  194 +
  195 + @Test
  196 + @DisplayName("定时触发器无条件组时,应直接执行动作")
  197 + void testTimerTrigger_withNullConditionGroups_shouldExecuteActions() {
  198 + // 准备数据
  199 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  200 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", null);
  201 + sceneRule.setTriggers(ListUtil.toList(trigger));
  202 +
  203 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  204 +
  205 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  206 +
  207 + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID);
  208 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  209 + verify(deviceService, never()).getDevice(any());
  210 + }
  211 +
  212 + @Test
  213 + @DisplayName("定时触发器条件组为空列表时,应直接执行动作")
  214 + void testTimerTrigger_withEmptyConditionGroups_shouldExecuteActions() {
  215 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  216 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", Collections.emptyList());
  217 + sceneRule.setTriggers(ListUtil.toList(trigger));
  218 +
  219 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  220 +
  221 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  222 +
  223 + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID);
  224 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  225 + }
  226 + }
  227 +
  228 + @Nested
  229 + @DisplayName("条件组 OR 逻辑测试 - Validates: Requirements 2.2, 2.3")
  230 + class ConditionGroupOrLogicTest {
  231 +
  232 + @Test
  233 + @DisplayName("多个条件组中第一个满足时,应执行动作")
  234 + void testMultipleConditionGroups_firstGroupMatches_shouldExecuteActions() {
  235 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  236 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  237 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  238 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  239 +
  240 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  241 + createTwoSingleConditionGroups(condition1, condition2);
  242 +
  243 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  244 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  245 + sceneRule.setTriggers(ListUtil.toList(trigger));
  246 +
  247 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  248 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  249 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  250 +
  251 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  252 +
  253 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  254 + }
  255 +
  256 + @Test
  257 + @DisplayName("多个条件组中第二个满足时,应执行动作")
  258 + void testMultipleConditionGroups_secondGroupMatches_shouldExecuteActions() {
  259 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  260 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  261 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  262 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  263 +
  264 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  265 + createTwoSingleConditionGroups(condition1, condition2);
  266 +
  267 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  268 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  269 + sceneRule.setTriggers(ListUtil.toList(trigger));
  270 +
  271 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  272 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  273 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  274 +
  275 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  276 +
  277 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  278 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1);
  279 + }
  280 + }
  281 +
  282 + @Nested
  283 + @DisplayName("条件组内 AND 逻辑测试 - Validates: Requirement 2.4")
  284 + class ConditionGroupAndLogicTest {
  285 +
  286 + @Test
  287 + @DisplayName("条件组内所有条件都满足时,该组应匹配成功")
  288 + void testSingleConditionGroup_allConditionsMatch_shouldPass() {
  289 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  290 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  291 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  292 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "80");
  293 +
  294 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  295 + createSingleGroupWithMultipleConditions(condition1, condition2);
  296 +
  297 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  298 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  299 + sceneRule.setTriggers(ListUtil.toList(trigger));
  300 +
  301 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  302 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  303 + tempProperty.setValue(30);
  304 + properties.put("temperature", tempProperty);
  305 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  306 + humidityProperty.setValue(60);
  307 + properties.put("humidity", humidityProperty);
  308 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  309 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  310 +
  311 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  312 +
  313 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  314 + }
  315 +
  316 + @Test
  317 + @DisplayName("条件组内有一个条件不满足时,该组应匹配失败")
  318 + void testSingleConditionGroup_oneConditionFails_shouldFail() {
  319 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  320 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  321 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  322 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50");
  323 +
  324 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  325 + createSingleGroupWithMultipleConditions(condition1, condition2);
  326 +
  327 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  328 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  329 + sceneRule.setTriggers(ListUtil.toList(trigger));
  330 +
  331 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  332 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  333 + tempProperty.setValue(30);
  334 + properties.put("temperature", tempProperty);
  335 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  336 + humidityProperty.setValue(60); // 不满足 < 50
  337 + properties.put("humidity", humidityProperty);
  338 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  339 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  340 +
  341 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  342 +
  343 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  344 + }
  345 + }
  346 +
  347 + @Nested
  348 + @DisplayName("所有条件组不满足测试 - Validates: Requirement 2.5")
  349 + class AllConditionGroupsFailTest {
  350 +
  351 + @Test
  352 + @DisplayName("所有条件组都不满足时,应跳过动作执行")
  353 + void testAllConditionGroups_allFail_shouldSkipExecution() {
  354 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  355 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  356 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  357 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  358 +
  359 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  360 + createTwoSingleConditionGroups(condition1, condition2);
  361 +
  362 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  363 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  364 + sceneRule.setTriggers(ListUtil.toList(trigger));
  365 +
  366 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  367 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  368 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  369 +
  370 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  371 +
  372 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  373 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1);
  374 + }
  375 + }
  376 +
  377 + @Nested
  378 + @DisplayName("设备状态条件测试 - Validates: Requirements 4.1, 4.2")
  379 + class DeviceStateConditionTest {
  380 +
  381 + @Test
  382 + @DisplayName("设备在线状态条件满足时,应匹配成功")
  383 + void testDeviceStateCondition_online_shouldMatch() {
  384 + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition(
  385 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  386 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  387 +
  388 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  389 +
  390 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  391 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  392 + sceneRule.setTriggers(ListUtil.toList(trigger));
  393 +
  394 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  395 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  396 +
  397 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  398 +
  399 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  400 + }
  401 +
  402 + @Test
  403 + @DisplayName("设备不存在时,条件应不匹配")
  404 + void testDeviceStateCondition_deviceNotExists_shouldNotMatch() {
  405 + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition(
  406 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  407 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  408 +
  409 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  410 +
  411 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  412 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  413 + sceneRule.setTriggers(ListUtil.toList(trigger));
  414 +
  415 + when(deviceService.getDevice(DEVICE_ID)).thenReturn(null);
  416 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  417 +
  418 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  419 +
  420 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  421 + }
  422 + }
  423 +
  424 + @Nested
  425 + @DisplayName("设备属性条件测试 - Validates: Requirements 3.1, 3.2, 3.3")
  426 + class DevicePropertyConditionTest {
  427 +
  428 + @Test
  429 + @DisplayName("设备属性条件满足时,应匹配成功")
  430 + void testDevicePropertyCondition_match_shouldPass() {
  431 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  432 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25");
  433 +
  434 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  435 +
  436 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  437 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  438 + sceneRule.setTriggers(ListUtil.toList(trigger));
  439 +
  440 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  441 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  442 +
  443 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  444 +
  445 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  446 + }
  447 +
  448 + @Test
  449 + @DisplayName("设备属性不存在时,条件应不匹配")
  450 + void testDevicePropertyCondition_propertyNotExists_shouldNotMatch() {
  451 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  452 + DEVICE_ID, "nonexistent", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25");
  453 +
  454 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  455 +
  456 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  457 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  458 + sceneRule.setTriggers(ListUtil.toList(trigger));
  459 +
  460 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(Collections.emptyMap());
  461 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  462 +
  463 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  464 +
  465 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  466 + }
  467 +
  468 + @Test
  469 + @DisplayName("设备属性等于条件测试")
  470 + void testDevicePropertyCondition_equals_shouldMatch() {
  471 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  472 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), "30");
  473 +
  474 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  475 +
  476 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  477 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  478 + sceneRule.setTriggers(ListUtil.toList(trigger));
  479 +
  480 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  481 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  482 +
  483 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  484 +
  485 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  486 + }
  487 + }
  488 +
  489 + @Nested
  490 + @DisplayName("场景规则状态测试")
  491 + class SceneRuleStatusTest {
  492 +
  493 + @Test
  494 + @DisplayName("场景规则不存在时,应直接返回")
  495 + void testSceneRule_notExists_shouldReturn() {
  496 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(null);
  497 +
  498 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  499 +
  500 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  501 + }
  502 +
  503 + @Test
  504 + @DisplayName("场景规则已禁用时,应直接返回")
  505 + void testSceneRule_disabled_shouldReturn() {
  506 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  507 + sceneRule.setStatus(CommonStatusEnum.DISABLE.getStatus());
  508 +
  509 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  510 +
  511 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  512 +
  513 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  514 + }
  515 +
  516 + @Test
  517 + @DisplayName("场景规则无定时触发器时,应直接返回")
  518 + void testSceneRule_noTimerTrigger_shouldReturn() {
  519 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  520 + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
  521 + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
  522 + sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
  523 +
  524 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  525 +
  526 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  527 +
  528 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  529 + }
  530 + }
  531 +
  532 + @Nested
  533 + @DisplayName("复杂条件组合测试")
  534 + class ComplexConditionCombinationTest {
  535 +
  536 + @Test
  537 + @DisplayName("混合条件类型测试:设备属性 + 设备状态")
  538 + void testMixedConditionTypes_propertyAndState() {
  539 + IotSceneRuleDO.TriggerCondition propertyCondition = createDevicePropertyCondition(
  540 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  541 + IotSceneRuleDO.TriggerCondition stateCondition = createDeviceStateCondition(
  542 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  543 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  544 +
  545 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  546 + createSingleGroupWithMultipleConditions(propertyCondition, stateCondition);
  547 +
  548 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  549 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  550 + sceneRule.setTriggers(ListUtil.toList(trigger));
  551 +
  552 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  553 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  554 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  555 +
  556 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  557 +
  558 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  559 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  560 + }
  561 +
  562 + @Test
  563 + @DisplayName("多条件组 OR 逻辑 + 组内 AND 逻辑综合测试")
  564 + void testComplexOrAndLogic() {
  565 + // 条件组1:温度 > 30 AND 湿度 < 50(不满足)
  566 + // 条件组2:温度 > 20 AND 设备在线(满足)
  567 + IotSceneRuleDO.TriggerCondition group1Cond1 = createDevicePropertyCondition(
  568 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "30");
  569 + IotSceneRuleDO.TriggerCondition group1Cond2 = createDevicePropertyCondition(
  570 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50");
  571 +
  572 + IotSceneRuleDO.TriggerCondition group2Cond1 = createDevicePropertyCondition(
  573 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  574 + IotSceneRuleDO.TriggerCondition group2Cond2 = createDeviceStateCondition(
  575 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  576 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  577 +
  578 + // 创建两个条件组
  579 + List<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
  580 + group1.add(group1Cond1);
  581 + group1.add(group1Cond2);
  582 + List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
  583 + group2.add(group2Cond1);
  584 + group2.add(group2Cond2);
  585 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = new ArrayList<>();
  586 + conditionGroups.add(group1);
  587 + conditionGroups.add(group2);
  588 +
  589 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  590 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  591 + sceneRule.setTriggers(ListUtil.toList(trigger));
  592 +
  593 + // Mock:温度 25,湿度 60,设备在线
  594 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  595 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  596 + tempProperty.setValue(25);
  597 + properties.put("temperature", tempProperty);
  598 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  599 + humidityProperty.setValue(60);
  600 + properties.put("humidity", humidityProperty);
  601 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  602 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  603 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  604 +
  605 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  606 +
  607 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  608 + }
  609 + }
  610 +
  611 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 import org.junit.jupiter.api.BeforeEach; 9 import org.junit.jupiter.api.BeforeEach;
10 -import org.junit.jupiter.api.Disabled;  
11 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
12 11
13 import java.util.HashMap; 12 import java.util.HashMap;
@@ -23,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*; @@ -23,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.*;
23 * 22 *
24 * @author HUIHUI 23 * @author HUIHUI
25 */ 24 */
26 -@Disabled // TODO @puhui999:单测有报错,先屏蔽  
27 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { 25 public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 26
29 private IotDeviceServiceInvokeTriggerMatcher matcher; 27 private IotDeviceServiceInvokeTriggerMatcher matcher;
@@ -378,6 +376,268 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa @@ -378,6 +376,268 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa
378 assertFalse(result); 376 assertFalse(result);
379 } 377 }
380 378
  379 +
  380 + // ========== 参数条件匹配测试 ==========
  381 +
  382 + /**
  383 + * 测试无参数条件时的匹配逻辑 - 只要标识符匹配就返回 true
  384 + * **Property 4: 服务调用触发器参数匹配逻辑**
  385 + * **Validates: Requirements 5.2**
  386 + */
  387 + @Test
  388 + public void testMatches_noParameterCondition_success() {
  389 + // 准备参数
  390 + String serviceIdentifier = "testService";
  391 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  392 + .put("identifier", serviceIdentifier)
  393 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  394 + .put("level", 5)
  395 + .build())
  396 + .build();
  397 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  398 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  399 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  400 + trigger.setIdentifier(serviceIdentifier);
  401 + trigger.setOperator(null); // 无参数条件
  402 + trigger.setValue(null);
  403 +
  404 + // 调用
  405 + boolean result = matcher.matches(message, trigger);
  406 +
  407 + // 断言
  408 + assertTrue(result);
  409 + }
  410 +
  411 + /**
  412 + * 测试有参数条件时的匹配逻辑 - 参数条件匹配成功
  413 + * **Property 4: 服务调用触发器参数匹配逻辑**
  414 + * **Validates: Requirements 5.1**
  415 + */
  416 + @Test
  417 + public void testMatches_withParameterCondition_greaterThan_success() {
  418 + // 准备参数
  419 + String serviceIdentifier = "level";
  420 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  421 + .put("identifier", serviceIdentifier)
  422 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  423 + .put("level", 5)
  424 + .build())
  425 + .build();
  426 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  427 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  428 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  429 + trigger.setIdentifier(serviceIdentifier);
  430 + trigger.setOperator(">"); // 大于操作符
  431 + trigger.setValue("3");
  432 +
  433 + // 调用
  434 + boolean result = matcher.matches(message, trigger);
  435 +
  436 + // 断言
  437 + assertTrue(result);
  438 + }
  439 +
  440 + /**
  441 + * 测试有参数条件时的匹配逻辑 - 参数条件匹配失败
  442 + * **Property 4: 服务调用触发器参数匹配逻辑**
  443 + * **Validates: Requirements 5.1**
  444 + */
  445 + @Test
  446 + public void testMatches_withParameterCondition_greaterThan_failure() {
  447 + // 准备参数
  448 + String serviceIdentifier = "level";
  449 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  450 + .put("identifier", serviceIdentifier)
  451 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  452 + .put("level", 2)
  453 + .build())
  454 + .build();
  455 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  456 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  457 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  458 + trigger.setIdentifier(serviceIdentifier);
  459 + trigger.setOperator(">"); // 大于操作符
  460 + trigger.setValue("3");
  461 +
  462 + // 调用
  463 + boolean result = matcher.matches(message, trigger);
  464 +
  465 + // 断言
  466 + assertFalse(result);
  467 + }
  468 +
  469 + /**
  470 + * 测试有参数条件时的匹配逻辑 - 等于操作符
  471 + * **Property 4: 服务调用触发器参数匹配逻辑**
  472 + * **Validates: Requirements 5.1**
  473 + */
  474 + @Test
  475 + public void testMatches_withParameterCondition_equals_success() {
  476 + // 准备参数
  477 + String serviceIdentifier = "mode";
  478 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  479 + .put("identifier", serviceIdentifier)
  480 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  481 + .put("mode", "auto")
  482 + .build())
  483 + .build();
  484 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  485 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  486 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  487 + trigger.setIdentifier(serviceIdentifier);
  488 + trigger.setOperator("=="); // 等于操作符
  489 + trigger.setValue("auto");
  490 +
  491 + // 调用
  492 + boolean result = matcher.matches(message, trigger);
  493 +
  494 + // 断言
  495 + assertTrue(result);
  496 + }
  497 +
  498 + /**
  499 + * 测试参数缺失时的处理 - 消息中缺少 inputData
  500 + * **Property 4: 服务调用触发器参数匹配逻辑**
  501 + * **Validates: Requirements 5.3**
  502 + */
  503 + @Test
  504 + public void testMatches_withParameterCondition_missingInputData() {
  505 + // 准备参数
  506 + String serviceIdentifier = "testService";
  507 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  508 + .put("identifier", serviceIdentifier)
  509 + // 缺少 inputData 字段
  510 + .build();
  511 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  512 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  513 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  514 + trigger.setIdentifier(serviceIdentifier);
  515 + trigger.setOperator(">"); // 配置了参数条件
  516 + trigger.setValue("3");
  517 +
  518 + // 调用
  519 + boolean result = matcher.matches(message, trigger);
  520 +
  521 + // 断言
  522 + assertFalse(result);
  523 + }
  524 +
  525 + /**
  526 + * 测试参数缺失时的处理 - inputData 中缺少指定参数
  527 + * **Property 4: 服务调用触发器参数匹配逻辑**
  528 + * **Validates: Requirements 5.3**
  529 + */
  530 + @Test
  531 + public void testMatches_withParameterCondition_missingParam() {
  532 + // 准备参数
  533 + String serviceIdentifier = "level";
  534 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  535 + .put("identifier", serviceIdentifier)
  536 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  537 + .put("otherParam", 5) // 不是 level 参数
  538 + .build())
  539 + .build();
  540 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  541 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  542 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  543 + trigger.setIdentifier(serviceIdentifier);
  544 + trigger.setOperator(">"); // 配置了参数条件
  545 + trigger.setValue("3");
  546 +
  547 + // 调用
  548 + boolean result = matcher.matches(message, trigger);
  549 +
  550 + // 断言
  551 + assertFalse(result);
  552 + }
  553 +
  554 + /**
  555 + * 测试只有 operator 没有 value 时不触发参数条件匹配
  556 + * **Property 4: 服务调用触发器参数匹配逻辑**
  557 + * **Validates: Requirements 5.2**
  558 + */
  559 + @Test
  560 + public void testMatches_onlyOperator_noValue() {
  561 + // 准备参数
  562 + String serviceIdentifier = "testService";
  563 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  564 + .put("identifier", serviceIdentifier)
  565 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  566 + .put("level", 5)
  567 + .build())
  568 + .build();
  569 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  570 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  571 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  572 + trigger.setIdentifier(serviceIdentifier);
  573 + trigger.setOperator(">"); // 只有 operator
  574 + trigger.setValue(null); // 没有 value
  575 +
  576 + // 调用
  577 + boolean result = matcher.matches(message, trigger);
  578 +
  579 + // 断言:只有 operator 没有 value 时,不触发参数条件匹配,标识符匹配即成功
  580 + assertTrue(result);
  581 + }
  582 +
  583 + /**
  584 + * 测试只有 value 没有 operator 时不触发参数条件匹配
  585 + * **Property 4: 服务调用触发器参数匹配逻辑**
  586 + * **Validates: Requirements 5.2**
  587 + */
  588 + @Test
  589 + public void testMatches_onlyValue_noOperator() {
  590 + // 准备参数
  591 + String serviceIdentifier = "testService";
  592 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  593 + .put("identifier", serviceIdentifier)
  594 + .put("inputData", MapUtil.builder(new HashMap<String, Object>())
  595 + .put("level", 5)
  596 + .build())
  597 + .build();
  598 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  599 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  600 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  601 + trigger.setIdentifier(serviceIdentifier);
  602 + trigger.setOperator(null); // 没有 operator
  603 + trigger.setValue("3"); // 只有 value
  604 +
  605 + // 调用
  606 + boolean result = matcher.matches(message, trigger);
  607 +
  608 + // 断言:只有 value 没有 operator 时,不触发参数条件匹配,标识符匹配即成功
  609 + assertTrue(result);
  610 + }
  611 +
  612 + /**
  613 + * 测试使用 inputParams 字段(替代 inputData)
  614 + * **Property 4: 服务调用触发器参数匹配逻辑**
  615 + * **Validates: Requirements 5.1**
  616 + */
  617 + @Test
  618 + public void testMatches_withInputParams_success() {
  619 + // 准备参数
  620 + String serviceIdentifier = "level";
  621 + Map<String, Object> serviceParams = MapUtil.builder(new HashMap<String, Object>())
  622 + .put("identifier", serviceIdentifier)
  623 + .put("inputParams", MapUtil.builder(new HashMap<String, Object>()) // 使用 inputParams 而不是 inputData
  624 + .put("level", 5)
  625 + .build())
  626 + .build();
  627 + IotDeviceMessage message = createServiceInvokeMessage(serviceParams);
  628 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  629 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  630 + trigger.setIdentifier(serviceIdentifier);
  631 + trigger.setOperator(">"); // 大于操作符
  632 + trigger.setValue("3");
  633 +
  634 + // 调用
  635 + boolean result = matcher.matches(message, trigger);
  636 +
  637 + // 断言
  638 + assertTrue(result);
  639 + }
  640 +
381 // ========== 辅助方法 ========== 641 // ========== 辅助方法 ==========
382 642
383 /** 643 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
@@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 4 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  11 +
  12 +import java.util.List;
7 13
8 /** 14 /**
9 * IoT 设备通用 API 15 * IoT 设备通用 API
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi { @@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
28 */ 34 */
29 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO); 35 CommonResult<IotDeviceRespDTO> getDevice(IotDeviceGetReqDTO infoReqDTO);
30 36
  37 + /**
  38 + * 直连/网关设备动态注册(一型一密)
  39 + *
  40 + * @param reqDTO 动态注册请求
  41 + * @return 注册结果(包含 DeviceSecret)
  42 + */
  43 + CommonResult<IotDeviceRegisterRespDTO> registerDevice(IotDeviceRegisterReqDTO reqDTO);
  44 +
  45 + /**
  46 + * 网关子设备动态注册(网关代理转发)
  47 + *
  48 + * @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
  49 + * @return 注册结果列表
  50 + */
  51 + CommonResult<List<IotSubDeviceRegisterRespDTO>> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
  52 +
31 } 53 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
1 package cn.iocoder.yudao.module.iot.core.biz.dto; 1 package cn.iocoder.yudao.module.iot.core.biz.dto;
2 2
3 import jakarta.validation.constraints.NotEmpty; 3 import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.AllArgsConstructor;
4 import lombok.Data; 5 import lombok.Data;
  6 +import lombok.NoArgsConstructor;
5 7
6 /** 8 /**
7 * IoT 设备认证 Request DTO 9 * IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data; @@ -9,6 +11,8 @@ import lombok.Data;
9 * @author 芋道源码 11 * @author 芋道源码
10 */ 12 */
11 @Data 13 @Data
  14 +@NoArgsConstructor
  15 +@AllArgsConstructor
12 public class IotDeviceAuthReqDTO { 16 public class IotDeviceAuthReqDTO {
13 17
14 /** 18 /**
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.biz.dto;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 子设备动态注册 Request DTO
  12 + * <p>
  13 + * 额外包含了网关设备的标识信息
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class IotSubDeviceRegisterFullReqDTO {
  19 +
  20 + /**
  21 + * 网关设备 ProductKey
  22 + */
  23 + @NotEmpty(message = "网关产品标识不能为空")
  24 + private String gatewayProductKey;
  25 +
  26 + /**
  27 + * 网关设备 DeviceName
  28 + */
  29 + @NotEmpty(message = "网关设备名称不能为空")
  30 + private String gatewayDeviceName;
  31 +
  32 + /**
  33 + * 子设备注册列表
  34 + */
  35 + @NotNull(message = "子设备注册列表不能为空")
  36 + private List<IotSubDeviceRegisterReqDTO> subDevices;
  37 +
  38 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
@@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
24 24
25 // TODO 芋艿:要不要加个 ping 消息; 25 // TODO 芋艿:要不要加个 ping 消息;
26 26
  27 + // ========== 拓扑管理 ==========
  28 + // 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
  29 +
  30 + TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
  31 + TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
  32 + TOPO_GET("thing.topo.get", "获取拓扑关系", true),
  33 + TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
  34 +
  35 + // ========== 设备注册 ==========
  36 + // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
  37 +
  38 + DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
  39 + SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
  40 +
27 // ========== 设备属性 ========== 41 // ========== 设备属性 ==========
28 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 42 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
29 43
30 PROPERTY_POST("thing.property.post", "属性上报", true), 44 PROPERTY_POST("thing.property.post", "属性上报", true),
31 PROPERTY_SET("thing.property.set", "属性设置", false), 45 PROPERTY_SET("thing.property.set", "属性设置", false),
32 46
  47 + PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
  48 +
33 // ========== 设备事件 ========== 49 // ========== 设备事件 ==========
34 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 50 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
35 51
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
50 66
51 OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), 67 OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
52 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), 68 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
  69 +
53 ; 70 ;
54 71
55 public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod) 72 public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java deleted
1 -package cn.iocoder.yudao.module.iot.core.enums;  
2 -  
3 -import cn.iocoder.yudao.framework.common.core.ArrayValuable;  
4 -import lombok.Getter;  
5 -import lombok.RequiredArgsConstructor;  
6 -  
7 -import java.util.Arrays;  
8 -  
9 -/**  
10 - * IoT 设备消息类型枚举  
11 - */  
12 -@Getter  
13 -@RequiredArgsConstructor  
14 -public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {  
15 -  
16 - STATE("state"), // 设备状态  
17 -// PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
18 - EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
19 - SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务  
20 - CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置  
21 - OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级  
22 - REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册  
23 - TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑  
24 -  
25 - public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);  
26 -  
27 - /**  
28 - * 属性  
29 - */  
30 - private final String type;  
31 -  
32 - @Override  
33 - public String[] array() {  
34 - return ARRAYS;  
35 - }  
36 -  
37 -}  
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
@@ -108,6 +108,23 @@ public class IotDeviceMessage { @@ -108,6 +108,23 @@ public class IotDeviceMessage {
108 return of(requestId, method, params, null, null, null); 108 return of(requestId, method, params, null, null, null);
109 } 109 }
110 110
  111 + /**
  112 + * 创建设备请求消息(包含设备信息)
  113 + *
  114 + * @param deviceId 设备编号
  115 + * @param tenantId 租户编号
  116 + * @param serverId 服务标识
  117 + * @param method 消息方法
  118 + * @param params 消息参数
  119 + * @return 消息对象
  120 + */
  121 + public static IotDeviceMessage requestOf(Long deviceId, Long tenantId, String serverId,
  122 + String method, Object params) {
  123 + IotDeviceMessage message = of(null, method, params, null, null, null);
  124 + return message.setId(IotDeviceMessageUtils.generateMessageId())
  125 + .setDeviceId(deviceId).setTenantId(tenantId).setServerId(serverId);
  126 + }
  127 +
111 public static IotDeviceMessage replyOf(String requestId, String method, 128 public static IotDeviceMessage replyOf(String requestId, String method,
112 Object data, Integer code, String msg) { 129 Object data, Integer code, String msg) {
113 if (code == null) { 130 if (code == null) {
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +/**
  9 + * IoT 设备标识
  10 + *
  11 + * 用于标识一个设备的基本信息(productKey + deviceName)
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceIdentity {
  19 +
  20 + /**
  21 + * 产品标识
  22 + */
  23 + @NotEmpty(message = "产品标识不能为空")
  24 + private String productKey;
  25 +
  26 + /**
  27 + * 设备名称
  28 + */
  29 + @NotEmpty(message = "设备名称不能为空")
  30 + private String deviceName;
  31 +
  32 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * IoT 设备动态注册 Request DTO
  8 + * <p>
  9 + * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
  10 + *
  11 + * @author 芋道源码
  12 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  13 + */
  14 +@Data
  15 +public class IotDeviceRegisterReqDTO {
  16 +
  17 + /**
  18 + * 产品标识
  19 + */
  20 + @NotEmpty(message = "产品标识不能为空")
  21 + private String productKey;
  22 +
  23 + /**
  24 + * 设备名称
  25 + */
  26 + @NotEmpty(message = "设备名称不能为空")
  27 + private String deviceName;
  28 +
  29 + /**
  30 + * 产品密钥
  31 + */
  32 + @NotEmpty(message = "产品密钥不能为空")
  33 + private String productSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * IoT 设备动态注册 Response DTO
  9 + * <p>
  10 + * 用于直连设备/网关的一型一密动态注册响应
  11 + *
  12 + * @author 芋道源码
  13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotDeviceRegisterRespDTO {
  19 +
  20 + /**
  21 + * 产品标识
  22 + */
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 设备名称
  27 + */
  28 + private String deviceName;
  29 +
  30 + /**
  31 + * 设备密钥
  32 + */
  33 + private String deviceSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * IoT 子设备动态注册 Request DTO
  8 + * <p>
  9 + * 用于 thing.auth.register.sub 消息的 params 数组元素
  10 + *
  11 + * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  15 + */
  16 +@Data
  17 +public class IotSubDeviceRegisterReqDTO {
  18 +
  19 + /**
  20 + * 子设备 ProductKey
  21 + */
  22 + @NotEmpty(message = "产品标识不能为空")
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 子设备 DeviceName
  27 + */
  28 + @NotEmpty(message = "设备名称不能为空")
  29 + private String deviceName;
  30 +
  31 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.auth;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * IoT 子设备动态注册 Response DTO
  9 + * <p>
  10 + * 用于 thing.auth.register.sub 响应的设备信息
  11 + *
  12 + * @author 芋道源码
  13 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  14 + */
  15 +@Data
  16 +@NoArgsConstructor
  17 +@AllArgsConstructor
  18 +public class IotSubDeviceRegisterRespDTO {
  19 +
  20 + /**
  21 + * 子设备 ProductKey
  22 + */
  23 + private String productKey;
  24 +
  25 + /**
  26 + * 子设备 DeviceName
  27 + */
  28 + private String deviceName;
  29 +
  30 + /**
  31 + * 分配的 DeviceSecret
  32 + */
  33 + private String deviceSecret;
  34 +
  35 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.event;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT 设备事件上报 Request DTO
  7 + * <p>
  8 + * 用于 thing.event.post 消息的 params 参数
  9 + *
  10 + * @author 芋道源码
  11 + * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-events">阿里云 - 设备上报事件</a>
  12 + */
  13 +@Data
  14 +public class IotDeviceEventPostReqDTO {
  15 +
  16 + /**
  17 + * 事件标识符
  18 + */
  19 + private String identifier;
  20 +
  21 + /**
  22 + * 事件输出参数
  23 + */
  24 + private Object value;
  25 +
  26 + /**
  27 + * 上报时间(毫秒时间戳,可选)
  28 + */
  29 + private Long time;
  30 +
  31 + /**
  32 + * 创建事件上报 DTO
  33 + *
  34 + * @param identifier 事件标识符
  35 + * @param value 事件值
  36 + * @return DTO 对象
  37 + */
  38 + public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
  39 + return of(identifier, value, null);
  40 + }
  41 +
  42 + /**
  43 + * 创建事件上报 DTO(带时间)
  44 + *
  45 + * @param identifier 事件标识符
  46 + * @param value 事件值
  47 + * @param time 上报时间
  48 + * @return DTO 对象
  49 + */
  50 + public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
  51 + return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
  52 + }
  53 +
  54 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java 0 → 100644
  1 +/**
  2 + * IoT Topic 消息体 DTO 定义
  3 + * <p>
  4 + * 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
  5 + *
  6 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云 Alink 协议</a>
  7 + */
  8 +package cn.iocoder.yudao.module.iot.core.topic;
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.property;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * IoT 设备属性批量上报 Request DTO
  11 + * <p>
  12 + * 用于 thing.event.property.pack.post 消息的 params 参数
  13 + *
  14 + * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
  16 + */
  17 +@Data
  18 +public class IotDevicePropertyPackPostReqDTO {
  19 +
  20 + /**
  21 + * 网关自身属性
  22 + * <p>
  23 + * key: 属性标识符
  24 + * value: 属性值
  25 + */
  26 + private Map<String, Object> properties;
  27 +
  28 + /**
  29 + * 网关自身事件
  30 + * <p>
  31 + * key: 事件标识符
  32 + * value: 事件值对象(包含 value 和 time)
  33 + */
  34 + private Map<String, EventValue> events;
  35 +
  36 + /**
  37 + * 子设备数据列表
  38 + */
  39 + private List<SubDeviceData> subDevices;
  40 +
  41 + /**
  42 + * 事件值对象
  43 + */
  44 + @Data
  45 + public static class EventValue {
  46 +
  47 + /**
  48 + * 事件参数
  49 + */
  50 + private Object value;
  51 +
  52 + /**
  53 + * 上报时间(毫秒时间戳)
  54 + */
  55 + private Long time;
  56 +
  57 + }
  58 +
  59 + /**
  60 + * 子设备数据
  61 + */
  62 + @Data
  63 + public static class SubDeviceData {
  64 +
  65 + /**
  66 + * 子设备标识
  67 + */
  68 + private IotDeviceIdentity identity;
  69 +
  70 + /**
  71 + * 子设备属性
  72 + * <p>
  73 + * key: 属性标识符
  74 + * value: 属性值
  75 + */
  76 + private Map<String, Object> properties;
  77 +
  78 + /**
  79 + * 子设备事件
  80 + * <p>
  81 + * key: 事件标识符
  82 + * value: 事件值对象(包含 value 和 time)
  83 + */
  84 + private Map<String, EventValue> events;
  85 +
  86 + }
  87 +
  88 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.property;
  2 +
  3 +import java.util.HashMap;
  4 +import java.util.Map;
  5 +
  6 +/**
  7 + * IoT 设备属性上报 Request DTO
  8 + * <p>
  9 + * 用于 thing.property.post 消息的 params 参数
  10 + * <p>
  11 + * 本质是一个 Map,key 为属性标识符,value 为属性值
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="http://help.aliyun.com/zh/marketplace/device-reporting-attributes">阿里云 - 设备上报属性</a>
  15 + */
  16 +public class IotDevicePropertyPostReqDTO extends HashMap<String, Object> {
  17 +
  18 + public IotDevicePropertyPostReqDTO() {
  19 + super();
  20 + }
  21 +
  22 + public IotDevicePropertyPostReqDTO(Map<String, Object> properties) {
  23 + super(properties);
  24 + }
  25 +
  26 + /**
  27 + * 创建属性上报 DTO
  28 + *
  29 + * @param properties 属性数据
  30 + * @return DTO 对象
  31 + */
  32 + public static IotDevicePropertyPostReqDTO of(Map<String, Object> properties) {
  33 + return new IotDevicePropertyPostReqDTO(properties);
  34 + }
  35 +
  36 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import lombok.Data;
  6 +
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * IoT 设备拓扑添加 Request DTO
  11 + * <p>
  12 + * 用于 thing.topo.add 消息的 params 参数
  13 + *
  14 + * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
  16 + */
  17 +@Data
  18 +public class IotDeviceTopoAddReqDTO {
  19 +
  20 + /**
  21 + * 子设备认证信息列表
  22 + * <p>
  23 + * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
  24 + */
  25 + @NotEmpty(message = "子设备认证信息列表不能为空")
  26 + private List<IotDeviceAuthReqDTO> subDevices;
  27 +
  28 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 设备拓扑关系变更通知 Request DTO
  12 + * <p>
  13 + * 用于 thing.topo.change 下行消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + * @see <a href="https://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes">阿里云 - 通知网关拓扑关系变化</a>
  17 + */
  18 +@Data
  19 +@NoArgsConstructor
  20 +@AllArgsConstructor
  21 +public class IotDeviceTopoChangeReqDTO {
  22 +
  23 + public static final Integer STATUS_CREATE = 0;
  24 + public static final Integer STATUS_DELETE = 1;
  25 +
  26 + /**
  27 + * 拓扑关系状态
  28 + */
  29 + private Integer status;
  30 +
  31 + /**
  32 + * 子设备列表
  33 + */
  34 + private List<IotDeviceIdentity> subList;
  35 +
  36 + public static IotDeviceTopoChangeReqDTO ofCreate(List<IotDeviceIdentity> subList) {
  37 + return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
  38 + }
  39 +
  40 + public static IotDeviceTopoChangeReqDTO ofDelete(List<IotDeviceIdentity> subList) {
  41 + return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
  42 + }
  43 +
  44 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import jakarta.validation.Valid;
  5 +import jakarta.validation.constraints.NotEmpty;
  6 +import lombok.Data;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * IoT 设备拓扑删除 Request DTO
  12 + * <p>
  13 + * 用于 thing.topo.delete 消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
  17 + */
  18 +@Data
  19 +public class IotDeviceTopoDeleteReqDTO {
  20 +
  21 + /**
  22 + * 子设备标识列表
  23 + */
  24 + @Valid
  25 + @NotEmpty(message = "子设备标识列表不能为空")
  26 + private List<IotDeviceIdentity> subDevices;
  27 +
  28 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT 设备拓扑关系获取 Request DTO
  7 + * <p>
  8 + * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
  9 + *
  10 + * @author 芋道源码
  11 + * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
  12 + */
  13 +@Data
  14 +public class IotDeviceTopoGetReqDTO {
  15 +
  16 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  4 +import lombok.Data;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * IoT 设备拓扑关系获取 Response DTO
  10 + * <p>
  11 + * 用于 thing.topo.get 响应
  12 + *
  13 + * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
  15 + */
  16 +@Data
  17 +public class IotDeviceTopoGetRespDTO {
  18 +
  19 + /**
  20 + * 子设备列表
  21 + */
  22 + private List<IotDeviceIdentity> subDevices;
  23 +
  24 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
1 package cn.iocoder.yudao.module.iot.core.util; 1 package cn.iocoder.yudao.module.iot.core.util;
2 2
  3 +import cn.hutool.core.util.StrUtil;
3 import cn.hutool.crypto.digest.DigestUtil; 4 import cn.hutool.crypto.digest.DigestUtil;
4 import cn.hutool.crypto.digest.HmacAlgorithm; 5 import cn.hutool.crypto.digest.HmacAlgorithm;
5 -import lombok.AllArgsConstructor;  
6 -import lombok.Data;  
7 -import lombok.NoArgsConstructor; 6 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  7 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
8 8
9 /** 9 /**
10 * IoT 设备【认证】的工具类,参考阿里云 10 * IoT 设备【认证】的工具类,参考阿里云
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor; @@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
13 */ 13 */
14 public class IotDeviceAuthUtils { 14 public class IotDeviceAuthUtils {
15 15
16 - /**  
17 - * 认证信息  
18 - */  
19 - @Data  
20 - @NoArgsConstructor  
21 - @AllArgsConstructor  
22 - public static class AuthInfo {  
23 -  
24 - /**  
25 - * 客户端 ID  
26 - */  
27 - private String clientId;  
28 -  
29 - /**  
30 - * 用户名  
31 - */  
32 - private String username;  
33 -  
34 - /**  
35 - * 密码  
36 - */  
37 - private String password;  
38 -  
39 - }  
40 -  
41 - /**  
42 - * 设备信息  
43 - */  
44 - @Data  
45 - public static class DeviceInfo {  
46 -  
47 - private String productKey;  
48 -  
49 - private String deviceName;  
50 -  
51 - }  
52 -  
53 - public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) { 16 + public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
54 String clientId = buildClientId(productKey, deviceName); 17 String clientId = buildClientId(productKey, deviceName);
55 String username = buildUsername(productKey, deviceName); 18 String username = buildUsername(productKey, deviceName);
56 - String content = "clientId" + clientId +  
57 - "deviceName" + deviceName +  
58 - "deviceSecret" + deviceSecret +  
59 - "productKey" + productKey;  
60 - String password = buildPassword(deviceSecret, content);  
61 - return new AuthInfo(clientId, username, password); 19 + String password = buildPassword(deviceSecret,
  20 + buildContent(clientId, productKey, deviceName, deviceSecret));
  21 + return new IotDeviceAuthReqDTO(clientId, username, password);
62 } 22 }
63 23
64 - private static String buildClientId(String productKey, String deviceName) { 24 + public static String buildClientId(String productKey, String deviceName) {
65 return String.format("%s.%s", productKey, deviceName); 25 return String.format("%s.%s", productKey, deviceName);
66 } 26 }
67 27
68 - private static String buildUsername(String productKey, String deviceName) { 28 + public static String buildUsername(String productKey, String deviceName) {
69 return String.format("%s&%s", deviceName, productKey); 29 return String.format("%s&%s", deviceName, productKey);
70 } 30 }
71 31
72 - private static String buildPassword(String deviceSecret, String content) {  
73 - return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes()) 32 + public static String buildPassword(String deviceSecret, String content) {
  33 + return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
74 .digestHex(content); 34 .digestHex(content);
75 } 35 }
76 36
77 - public static DeviceInfo parseUsername(String username) { 37 + private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
  38 + return "clientId" + clientId +
  39 + "deviceName" + deviceName +
  40 + "deviceSecret" + deviceSecret +
  41 + "productKey" + productKey;
  42 + }
  43 +
  44 + public static IotDeviceIdentity parseUsername(String username) {
78 String[] usernameParts = username.split("&"); 45 String[] usernameParts = username.split("&");
79 if (usernameParts.length != 2) { 46 if (usernameParts.length != 2) {
80 return null; 47 return null;
81 } 48 }
82 - return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]); 49 + return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
83 } 50 }
84 51
85 } 52 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -72,7 +72,7 @@ public class IotDeviceMessageUtils { @@ -72,7 +72,7 @@ public class IotDeviceMessageUtils {
72 72
73 /** 73 /**
74 * 判断消息中是否包含指定的标识符 74 * 判断消息中是否包含指定的标识符
75 - * 75 + * <p>
76 * 对于不同消息类型的处理: 76 * 对于不同消息类型的处理:
77 * - EVENT_POST/SERVICE_INVOKE:检查 params.identifier 是否匹配 77 * - EVENT_POST/SERVICE_INVOKE:检查 params.identifier 是否匹配
78 * - STATE_UPDATE:检查 params.state 是否匹配 78 * - STATE_UPDATE:检查 params.state 是否匹配
@@ -100,6 +100,17 @@ public class IotDeviceMessageUtils { @@ -100,6 +100,17 @@ public class IotDeviceMessageUtils {
100 } 100 }
101 101
102 /** 102 /**
  103 + * 判断消息中是否不包含指定的标识符
  104 + *
  105 + * @param message 消息
  106 + * @param identifier 要检查的标识符
  107 + * @return 是否不包含
  108 + */
  109 + public static boolean notContainsIdentifier(IotDeviceMessage message, String identifier) {
  110 + return !containsIdentifier(message, identifier);
  111 + }
  112 +
  113 + /**
103 * 将 params 解析为 Map 114 * 将 params 解析为 Map
104 * 115 *
105 * @param params 参数(可能是 Map 或 JSON 字符串) 116 * @param params 参数(可能是 Map 或 JSON 字符串)
@@ -196,6 +207,43 @@ public class IotDeviceMessageUtils { @@ -196,6 +207,43 @@ public class IotDeviceMessageUtils {
196 return null; 207 return null;
197 } 208 }
198 209
  210 + /**
  211 + * 从服务调用消息中提取输入参数
  212 + * <p>
  213 + * 服务调用消息的 params 结构通常为:
  214 + * {
  215 + * "identifier": "serviceIdentifier",
  216 + * "inputData": { ... } 或 "inputParams": { ... }
  217 + * }
  218 + *
  219 + * @param message 设备消息
  220 + * @return 输入参数 Map,如果未找到则返回 null
  221 + */
  222 + @SuppressWarnings("unchecked")
  223 + public static Map<String, Object> extractServiceInputParams(IotDeviceMessage message) {
  224 + // 1. 参数校验
  225 + Object params = message.getParams();
  226 + if (params == null) {
  227 + return null;
  228 + }
  229 + if (!(params instanceof Map)) {
  230 + return null;
  231 + }
  232 + Map<String, Object> paramsMap = (Map<String, Object>) params;
  233 +
  234 + // 尝试从 inputData 字段获取
  235 + Object inputData = paramsMap.get("inputData");
  236 + if (inputData instanceof Map) {
  237 + return (Map<String, Object>) inputData;
  238 + }
  239 + // 尝试从 inputParams 字段获取
  240 + Object inputParams = paramsMap.get("inputParams");
  241 + if (inputParams instanceof Map) {
  242 + return (Map<String, Object>) inputParams;
  243 + }
  244 + return null;
  245 + }
  246 +
199 // ========== Topic 相关 ========== 247 // ========== Topic 相关 ==========
200 248
201 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { 249 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
1 package cn.iocoder.yudao.module.iot.core.util; 1 package cn.iocoder.yudao.module.iot.core.util;
2 2
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 import org.junit.jupiter.api.Test; 5 import org.junit.jupiter.api.Test;
5 6
6 import java.util.HashMap; 7 import java.util.HashMap;
7 import java.util.Map; 8 import java.util.Map;
8 9
9 -import static org.junit.jupiter.api.Assertions.assertEquals;  
10 -import static org.junit.jupiter.api.Assertions.assertNull; 10 +import static org.junit.jupiter.api.Assertions.*;
11 11
12 /** 12 /**
13 * {@link IotDeviceMessageUtils} 的单元测试 13 * {@link IotDeviceMessageUtils} 的单元测试
@@ -138,4 +138,72 @@ public class IotDeviceMessageUtilsTest { @@ -138,4 +138,72 @@ public class IotDeviceMessageUtilsTest {
138 Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature"); 138 Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
139 assertEquals(25.5, result); // 应该返回直接标识符的值 139 assertEquals(25.5, result); // 应该返回直接标识符的值
140 } 140 }
  141 +
  142 + // ========== notContainsIdentifier 测试 ==========
  143 +
  144 + /**
  145 + * 测试 notContainsIdentifier 与 containsIdentifier 的互补性
  146 + * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
  147 + * **Validates: Requirements 4.1**
  148 + */
  149 + @Test
  150 + public void testNotContainsIdentifier_complementary_whenContains() {
  151 + // 准备参数:消息包含指定标识符
  152 + IotDeviceMessage message = new IotDeviceMessage();
  153 + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
  154 + Map<String, Object> params = new HashMap<>();
  155 + params.put("temperature", 25);
  156 + message.setParams(params);
  157 + String identifier = "temperature";
  158 +
  159 + // 调用 & 断言:验证互补性
  160 + boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
  161 + boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
  162 + assertTrue(containsResult);
  163 + assertFalse(notContainsResult);
  164 + assertEquals(!containsResult, notContainsResult);
  165 + }
  166 +
  167 + /**
  168 + * 测试 notContainsIdentifier 与 containsIdentifier 的互补性
  169 + * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
  170 + * **Validates: Requirements 4.1**
  171 + */
  172 + @Test
  173 + public void testNotContainsIdentifier_complementary_whenNotContains() {
  174 + // 准备参数:消息不包含指定标识符
  175 + IotDeviceMessage message = new IotDeviceMessage();
  176 + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
  177 + Map<String, Object> params = new HashMap<>();
  178 + params.put("temperature", 25);
  179 + message.setParams(params);
  180 + String identifier = "humidity";
  181 +
  182 + // 调用 & 断言:验证互补性
  183 + boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
  184 + boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
  185 + assertFalse(containsResult);
  186 + assertTrue(notContainsResult);
  187 + assertEquals(!containsResult, notContainsResult);
  188 + }
  189 +
  190 + /**
  191 + * 测试 notContainsIdentifier 与 containsIdentifier 的互补性 - 空参数场景
  192 + * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
  193 + * **Validates: Requirements 4.1**
  194 + */
  195 + @Test
  196 + public void testNotContainsIdentifier_complementary_nullParams() {
  197 + // 准备参数:params 为 null
  198 + IotDeviceMessage message = new IotDeviceMessage();
  199 + message.setParams(null);
  200 + String identifier = "temperature";
  201 +
  202 + // 调用 & 断言:验证互补性
  203 + boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
  204 + boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
  205 + assertFalse(containsResult);
  206 + assertTrue(notContainsResult);
  207 + assertEquals(!containsResult, notContainsResult);
  208 + }
141 } 209 }
yudao-module-iot/yudao-module-iot-gateway/pom.xml
@@ -48,6 +48,12 @@ @@ -48,6 +48,12 @@
48 <artifactId>vertx-mqtt</artifactId> 48 <artifactId>vertx-mqtt</artifactId>
49 </dependency> 49 </dependency>
50 50
  51 + <!-- CoAP 相关 - Eclipse Californium -->
  52 + <dependency>
  53 + <groupId>org.eclipse.californium</groupId>
  54 + <artifactId>californium-core</artifactId>
  55 + </dependency>
  56 +
51 <!-- 测试相关 --> 57 <!-- 测试相关 -->
52 <dependency> 58 <dependency>
53 <groupId>cn.iocoder.boot</groupId> 59 <groupId>cn.iocoder.boot</groupId>
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component; @@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
18 @Component 18 @Component
19 public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { 19 public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
20 20
21 - private static final String TYPE = "Alink"; 21 + public static final String TYPE = "Alink";
22 22
23 @Data 23 @Data
24 @NoArgsConstructor 24 @NoArgsConstructor
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component; @@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
13 import java.nio.charset.StandardCharsets; 13 import java.nio.charset.StandardCharsets;
14 14
15 /** 15 /**
16 - * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 16 + * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
17 * <p> 17 * <p>
18 * 二进制协议格式(所有数值使用大端序): 18 * 二进制协议格式(所有数值使用大端序):
19 * 19 *
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
11 import org.springframework.stereotype.Component; 11 import org.springframework.stereotype.Component;
12 12
13 /** 13 /**
14 - * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 14 + * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
15 * 15 *
16 * 采用纯 JSON 格式传输,格式如下: 16 * 采用纯 JSON 格式传输,格式如下:
17 * { 17 * {
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
1 package cn.iocoder.yudao.module.iot.gateway.config; 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2
3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; 3 import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
4 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; 6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
@@ -10,13 +12,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr @@ -10,13 +12,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
10 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; 12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
11 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
12 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler; 14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
13 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsDownstreamSubscriber;  
14 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;  
15 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;  
16 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;  
17 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; 15 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
18 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; 16 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
19 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; 17 import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
  18 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
  19 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
  20 +import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
  21 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
  22 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
  23 +import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
20 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 24 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
21 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 25 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
22 import io.vertx.core.Vertx; 26 import io.vertx.core.Vertx;
@@ -40,9 +44,15 @@ public class IotGatewayConfiguration { @@ -40,9 +44,15 @@ public class IotGatewayConfiguration {
40 @Slf4j 44 @Slf4j
41 public static class HttpProtocolConfiguration { 45 public static class HttpProtocolConfiguration {
42 46
  47 + @Bean(name = "httpVertx", destroyMethod = "close")
  48 + public Vertx httpVertx() {
  49 + return Vertx.vertx();
  50 + }
  51 +
43 @Bean 52 @Bean
44 - public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) {  
45 - return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp()); 53 + public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
  54 + @Qualifier("httpVertx") Vertx httpVertx) {
  55 + return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
46 } 56 }
47 57
48 @Bean 58 @Bean
@@ -110,11 +120,9 @@ public class IotGatewayConfiguration { @@ -110,11 +120,9 @@ public class IotGatewayConfiguration {
110 @Bean 120 @Bean
111 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, 121 public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
112 IotDeviceMessageService messageService, 122 IotDeviceMessageService messageService,
113 - IotDeviceService deviceService,  
114 IotTcpConnectionManager connectionManager, 123 IotTcpConnectionManager connectionManager,
115 IotMessageBus messageBus) { 124 IotMessageBus messageBus) {
116 - return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,  
117 - messageBus); 125 + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
118 } 126 }
119 127
120 } 128 }
@@ -157,39 +165,88 @@ public class IotGatewayConfiguration { @@ -157,39 +165,88 @@ public class IotGatewayConfiguration {
157 } 165 }
158 166
159 /** 167 /**
160 - * IoT 网关 MQTT WebSocket 协议配置类 168 + * IoT 网关 UDP 协议配置类
161 */ 169 */
162 @Configuration 170 @Configuration
163 - @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt-ws", name = "enabled", havingValue = "true") 171 + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
164 @Slf4j 172 @Slf4j
165 - public static class MqttWsProtocolConfiguration { 173 + public static class UdpProtocolConfiguration {
166 174
167 - @Bean(name = "mqttWsVertx", destroyMethod = "close")  
168 - public Vertx mqttWsVertx() { 175 + @Bean(name = "udpVertx", destroyMethod = "close")
  176 + public Vertx udpVertx() {
169 return Vertx.vertx(); 177 return Vertx.vertx();
170 } 178 }
171 179
172 @Bean 180 @Bean
173 - public IotMqttWsUpstreamProtocol iotMqttWsUpstreamProtocol(IotGatewayProperties gatewayProperties,  
174 - IotDeviceMessageService messageService,  
175 - IotMqttWsConnectionManager connectionManager,  
176 - @Qualifier("mqttWsVertx") Vertx mqttWsVertx) {  
177 - return new IotMqttWsUpstreamProtocol(gatewayProperties.getProtocol().getMqttWs(),  
178 - messageService, connectionManager, mqttWsVertx); 181 + public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
  182 + IotDeviceService deviceService,
  183 + IotDeviceMessageService messageService,
  184 + IotUdpSessionManager sessionManager,
  185 + @Qualifier("udpVertx") Vertx udpVertx) {
  186 + return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
  187 + deviceService, messageService, sessionManager, udpVertx);
  188 + }
  189 +
  190 + @Bean
  191 + public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
  192 + IotDeviceMessageService messageService,
  193 + IotUdpSessionManager sessionManager,
  194 + IotMessageBus messageBus) {
  195 + return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
  196 + }
  197 +
  198 + }
  199 +
  200 + /**
  201 + * IoT 网关 CoAP 协议配置类
  202 + */
  203 + @Configuration
  204 + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
  205 + @Slf4j
  206 + public static class CoapProtocolConfiguration {
  207 +
  208 + @Bean
  209 + public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
  210 + return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
  211 + }
  212 +
  213 + @Bean
  214 + public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
  215 + IotMessageBus messageBus) {
  216 + return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
  217 + }
  218 +
  219 + }
  220 +
  221 + /**
  222 + * IoT 网关 WebSocket 协议配置类
  223 + */
  224 + @Configuration
  225 + @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
  226 + @Slf4j
  227 + public static class WebSocketProtocolConfiguration {
  228 +
  229 + @Bean(name = "websocketVertx", destroyMethod = "close")
  230 + public Vertx websocketVertx() {
  231 + return Vertx.vertx();
179 } 232 }
180 233
181 @Bean 234 @Bean
182 - public IotMqttWsDownstreamHandler iotMqttWsDownstreamHandler(IotDeviceMessageService messageService,  
183 - IotDeviceService deviceService,  
184 - IotMqttWsConnectionManager connectionManager) {  
185 - return new IotMqttWsDownstreamHandler(messageService, deviceService, connectionManager); 235 + public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
  236 + IotDeviceService deviceService,
  237 + IotDeviceMessageService messageService,
  238 + IotWebSocketConnectionManager connectionManager,
  239 + @Qualifier("websocketVertx") Vertx websocketVertx) {
  240 + return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
  241 + deviceService, messageService, connectionManager, websocketVertx);
186 } 242 }
187 243
188 @Bean 244 @Bean
189 - public IotMqttWsDownstreamSubscriber iotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol mqttWsUpstreamProtocol,  
190 - IotMqttWsDownstreamHandler downstreamHandler,  
191 - IotMessageBus messageBus) {  
192 - return new IotMqttWsDownstreamSubscriber(mqttWsUpstreamProtocol, downstreamHandler, messageBus); 245 + public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
  246 + IotDeviceMessageService messageService,
  247 + IotWebSocketConnectionManager connectionManager,
  248 + IotMessageBus messageBus) {
  249 + return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
193 } 250 }
194 251
195 } 252 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
@@ -93,6 +93,21 @@ public class IotGatewayProperties { @@ -93,6 +93,21 @@ public class IotGatewayProperties {
93 */ 93 */
94 private MqttWsProperties mqttWs; 94 private MqttWsProperties mqttWs;
95 95
  96 + /**
  97 + * UDP 组件配置
  98 + */
  99 + private UdpProperties udp;
  100 +
  101 + /**
  102 + * CoAP 组件配置
  103 + */
  104 + private CoapProperties coap;
  105 +
  106 + /**
  107 + * WebSocket 组件配置
  108 + */
  109 + private WebSocketProperties websocket;
  110 +
96 } 111 }
97 112
98 @Data 113 @Data
@@ -503,4 +518,129 @@ public class IotGatewayProperties { @@ -503,4 +518,129 @@ public class IotGatewayProperties {
503 518
504 } 519 }
505 520
  521 + @Data
  522 + public static class UdpProperties {
  523 +
  524 + /**
  525 + * 是否开启
  526 + */
  527 + @NotNull(message = "是否开启不能为空")
  528 + private Boolean enabled;
  529 +
  530 + /**
  531 + * 服务端口(默认 8093)
  532 + */
  533 + private Integer port = 8093;
  534 +
  535 + /**
  536 + * 接收缓冲区大小(默认 64KB)
  537 + */
  538 + private Integer receiveBufferSize = 65536;
  539 +
  540 + /**
  541 + * 发送缓冲区大小(默认 64KB)
  542 + */
  543 + private Integer sendBufferSize = 65536;
  544 +
  545 + /**
  546 + * 会话超时时间(毫秒,默认 60 秒)
  547 + * <p>
  548 + * 用于清理不活跃的设备地址映射
  549 + */
  550 + private Long sessionTimeoutMs = 60000L;
  551 +
  552 + /**
  553 + * 会话清理间隔(毫秒,默认 30 秒)
  554 + */
  555 + private Long sessionCleanIntervalMs = 30000L;
  556 +
  557 + }
  558 +
  559 + @Data
  560 + public static class CoapProperties {
  561 +
  562 + /**
  563 + * 是否开启
  564 + */
  565 + @NotNull(message = "是否开启不能为空")
  566 + private Boolean enabled;
  567 +
  568 + /**
  569 + * 服务端口(CoAP 默认端口 5683)
  570 + */
  571 + @NotNull(message = "服务端口不能为空")
  572 + private Integer port = 5683;
  573 +
  574 + /**
  575 + * 最大消息大小(字节)
  576 + */
  577 + @NotNull(message = "最大消息大小不能为空")
  578 + private Integer maxMessageSize = 1024;
  579 +
  580 + /**
  581 + * ACK 超时时间(毫秒)
  582 + */
  583 + @NotNull(message = "ACK 超时时间不能为空")
  584 + private Integer ackTimeout = 2000;
  585 +
  586 + /**
  587 + * 最大重传次数
  588 + */
  589 + @NotNull(message = "最大重传次数不能为空")
  590 + private Integer maxRetransmit = 4;
  591 +
  592 + }
  593 +
  594 + @Data
  595 + public static class WebSocketProperties {
  596 +
  597 + /**
  598 + * 是否开启
  599 + */
  600 + @NotNull(message = "是否开启不能为空")
  601 + private Boolean enabled;
  602 +
  603 + /**
  604 + * 服务器端口(默认:8094)
  605 + */
  606 + private Integer port = 8094;
  607 +
  608 + /**
  609 + * WebSocket 路径(默认:/ws)
  610 + */
  611 + @NotEmpty(message = "WebSocket 路径不能为空")
  612 + private String path = "/ws";
  613 +
  614 + /**
  615 + * 最大消息大小(字节,默认 64KB)
  616 + */
  617 + private Integer maxMessageSize = 65536;
  618 +
  619 + /**
  620 + * 最大帧大小(字节,默认 64KB)
  621 + */
  622 + private Integer maxFrameSize = 65536;
  623 +
  624 + /**
  625 + * 空闲超时时间(秒,默认 60)
  626 + */
  627 + private Integer idleTimeoutSeconds = 60;
  628 +
  629 + /**
  630 + * 是否启用 SSL(wss://)
  631 + */
  632 + private Boolean sslEnabled = false;
  633 +
  634 + /**
  635 + * SSL 证书路径
  636 + */
  637 + private String sslCertPath;
  638 +
  639 + /**
  640 + * SSL 私钥路径
  641 + */
  642 + private String sslKeyPath;
  643 +
  644 + }
  645 +
506 } 646 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
  4 +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  7 +import jakarta.annotation.PostConstruct;
  8 +import lombok.RequiredArgsConstructor;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +/**
  12 + * IoT 网关 CoAP 订阅者:接收下行给设备的消息
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@RequiredArgsConstructor
  17 +@Slf4j
  18 +public class IotCoapDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {
  19 +
  20 + private final IotCoapUpstreamProtocol protocol;
  21 +
  22 + private final IotMessageBus messageBus;
  23 +
  24 + @PostConstruct
  25 + public void init() {
  26 + messageBus.register(this);
  27 + }
  28 +
  29 + @Override
  30 + public String getTopic() {
  31 + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
  32 + }
  33 +
  34 + @Override
  35 + public String getGroup() {
  36 + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
  37 + return getTopic();
  38 + }
  39 +
  40 + @Override
  41 + public void onMessage(IotDeviceMessage message) {
  42 + // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
  43 + log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
  44 + }
  45 +
  46 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  4 +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
  5 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapAuthResource;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterHandler;
  8 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapRegisterResource;
  9 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamTopicResource;
  10 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.IotCoapUpstreamHandler;
  11 +import jakarta.annotation.PostConstruct;
  12 +import jakarta.annotation.PreDestroy;
  13 +import lombok.Getter;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.eclipse.californium.core.CoapResource;
  16 +import org.eclipse.californium.core.CoapServer;
  17 +import org.eclipse.californium.core.config.CoapConfig;
  18 +import org.eclipse.californium.elements.config.Configuration;
  19 +
  20 +import java.util.concurrent.TimeUnit;
  21 +
  22 +/**
  23 + * IoT 网关 CoAP 协议:接收设备上行消息
  24 + *
  25 + * 基于 Eclipse Californium 实现,支持:
  26 + * 1. 认证:POST /auth
  27 + * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  28 + * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Slf4j
  33 +public class IotCoapUpstreamProtocol {
  34 +
  35 + private final IotGatewayProperties.CoapProperties coapProperties;
  36 +
  37 + private CoapServer coapServer;
  38 +
  39 + @Getter
  40 + private final String serverId;
  41 +
  42 + public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
  43 + this.coapProperties = coapProperties;
  44 + this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
  45 + }
  46 +
  47 + @PostConstruct
  48 + public void start() {
  49 + try {
  50 + // 1.1 创建网络配置(Californium 3.x API)
  51 + Configuration config = Configuration.createStandardWithoutFile();
  52 + config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
  53 + config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
  54 + config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
  55 + config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
  56 + // 1.2 创建 CoAP 服务器
  57 + coapServer = new CoapServer(config);
  58 +
  59 + // 2.1 添加 /auth 认证资源
  60 + IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
  61 + IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
  62 + coapServer.add(authResource);
  63 + // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
  64 + IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
  65 + IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
  66 + authResource.add(new CoapResource("register") {{
  67 + add(registerResource);
  68 + }});
  69 + // 2.3 添加 /topic 根资源(用于上行消息)
  70 + IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
  71 + IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
  72 + coapServer.add(topicResource);
  73 +
  74 + // 3. 启动服务器
  75 + coapServer.start();
  76 + log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
  77 + } catch (Exception e) {
  78 + log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
  79 + throw e;
  80 + }
  81 + }
  82 +
  83 + @PreDestroy
  84 + public void stop() {
  85 + if (coapServer != null) {
  86 + try {
  87 + coapServer.stop();
  88 + log.info("[stop][IoT 网关 CoAP 协议已停止]");
  89 + } catch (Exception e) {
  90 + log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
  91 + }
  92 + }
  93 + }
  94 +
  95 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java 0 → 100644
  1 +/**
  2 + * CoAP 协议实现包
  3 + * <p>
  4 + * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
  5 + * <p>
  6 + * URI 路径:
  7 + * - 认证:POST /auth
  8 + * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  9 + * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  10 + * <p>
  11 + * Token 通过 CoAP Option 2088 携带
  12 + */
  13 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.BooleanUtil;
  6 +import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.extra.spring.SpringUtil;
  8 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  10 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  11 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
  12 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  13 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  14 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  15 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
  16 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  17 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.eclipse.californium.core.coap.CoAP;
  20 +import org.eclipse.californium.core.server.resources.CoapExchange;
  21 +
  22 +import java.util.Map;
  23 +
  24 +/**
  25 + * IoT 网关 CoAP 协议的【认证】处理器
  26 + *
  27 + * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler}
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@Slf4j
  32 +public class IotCoapAuthHandler {
  33 +
  34 + private final IotDeviceTokenService deviceTokenService;
  35 + private final IotDeviceCommonApi deviceApi;
  36 + private final IotDeviceMessageService deviceMessageService;
  37 +
  38 + public IotCoapAuthHandler() {
  39 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  40 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  41 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  42 + }
  43 +
  44 + /**
  45 + * 处理认证请求
  46 + *
  47 + * @param exchange CoAP 交换对象
  48 + * @param protocol 协议对象
  49 + */
  50 + @SuppressWarnings("unchecked")
  51 + public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
  52 + try {
  53 + // 1.1 解析请求体
  54 + byte[] payload = exchange.getRequestPayload();
  55 + if (payload == null || payload.length == 0) {
  56 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
  57 + return;
  58 + }
  59 + Map<String, Object> body;
  60 + try {
  61 + body = JsonUtils.parseObject(new String(payload), Map.class);
  62 + } catch (Exception e) {
  63 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
  64 + return;
  65 + }
  66 + // 1.2 解析参数
  67 + String clientId = MapUtil.getStr(body, "clientId");
  68 + if (StrUtil.isEmpty(clientId)) {
  69 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
  70 + return;
  71 + }
  72 + String username = MapUtil.getStr(body, "username");
  73 + if (StrUtil.isEmpty(username)) {
  74 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
  75 + return;
  76 + }
  77 + String password = MapUtil.getStr(body, "password");
  78 + if (StrUtil.isEmpty(password)) {
  79 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
  80 + return;
  81 + }
  82 +
  83 + // 2.1 执行认证
  84 + CommonResult<Boolean> result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
  85 + .setClientId(clientId).setUsername(username).setPassword(password));
  86 + if (result.isError()) {
  87 + log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
  88 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
  89 + return;
  90 + }
  91 + if (!BooleanUtil.isTrue(result.getData())) {
  92 + log.warn("[handle][认证失败,clientId: {}]", clientId);
  93 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
  94 + return;
  95 + }
  96 + // 2.2 生成 Token
  97 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
  98 + Assert.notNull(deviceInfo, "设备信息不能为空");
  99 + String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  100 + Assert.notBlank(token, "生成 token 不能为空");
  101 +
  102 + // 3. 执行上线
  103 + IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
  104 + deviceMessageService.sendDeviceMessage(message,
  105 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
  106 +
  107 + // 4. 返回成功响应
  108 + log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
  109 + deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  110 + IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
  111 + } catch (Exception e) {
  112 + log.error("[handle][认证处理异常]", e);
  113 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
  114 + }
  115 + }
  116 +
  117 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.eclipse.californium.core.CoapResource;
  6 +import org.eclipse.californium.core.server.resources.CoapExchange;
  7 +
  8 +/**
  9 + * IoT 网关 CoAP 协议的认证资源(/auth)
  10 + *
  11 + * 设备通过此资源进行认证,获取 Token
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Slf4j
  16 +public class IotCoapAuthResource extends CoapResource {
  17 +
  18 + public static final String PATH = "auth";
  19 +
  20 + private final IotCoapUpstreamProtocol protocol;
  21 + private final IotCoapAuthHandler authHandler;
  22 +
  23 + public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
  24 + IotCoapAuthHandler authHandler) {
  25 + super(PATH);
  26 + this.protocol = protocol;
  27 + this.authHandler = authHandler;
  28 + log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
  29 + }
  30 +
  31 + @Override
  32 + public void handlePOST(CoapExchange exchange) {
  33 + log.debug("[handlePOST][收到 /auth POST 请求]");
  34 + authHandler.handle(exchange, protocol);
  35 + }
  36 +
  37 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  8 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  9 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  10 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  11 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.eclipse.californium.core.coap.CoAP;
  14 +import org.eclipse.californium.core.server.resources.CoapExchange;
  15 +
  16 +import java.util.Map;
  17 +
  18 +/**
  19 + * IoT 网关 CoAP 协议的【设备动态注册】处理器
  20 + * <p>
  21 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  22 + *
  23 + * @author 芋道源码
  24 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  25 + * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
  26 + */
  27 +@Slf4j
  28 +public class IotCoapRegisterHandler {
  29 +
  30 + private final IotDeviceCommonApi deviceApi;
  31 +
  32 + public IotCoapRegisterHandler() {
  33 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  34 + }
  35 +
  36 + /**
  37 + * 处理设备动态注册请求
  38 + *
  39 + * @param exchange CoAP 交换对象
  40 + */
  41 + @SuppressWarnings("unchecked")
  42 + public void handle(CoapExchange exchange) {
  43 + try {
  44 + // 1.1 解析请求体
  45 + byte[] payload = exchange.getRequestPayload();
  46 + if (payload == null || payload.length == 0) {
  47 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
  48 + return;
  49 + }
  50 + Map<String, Object> body;
  51 + try {
  52 + body = JsonUtils.parseObject(new String(payload), Map.class);
  53 + } catch (Exception e) {
  54 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
  55 + return;
  56 + }
  57 +
  58 + // 1.2 解析参数
  59 + String productKey = MapUtil.getStr(body, "productKey");
  60 + if (StrUtil.isEmpty(productKey)) {
  61 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
  62 + return;
  63 + }
  64 + String deviceName = MapUtil.getStr(body, "deviceName");
  65 + if (StrUtil.isEmpty(deviceName)) {
  66 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
  67 + return;
  68 + }
  69 + String productSecret = MapUtil.getStr(body, "productSecret");
  70 + if (StrUtil.isEmpty(productSecret)) {
  71 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
  72 + return;
  73 + }
  74 +
  75 + // 2. 调用动态注册
  76 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  77 + .setProductKey(productKey)
  78 + .setDeviceName(deviceName)
  79 + .setProductSecret(productSecret);
  80 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
  81 + if (result.isError()) {
  82 + log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]",
  83 + productKey, deviceName, result.getMsg());
  84 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
  85 + "设备动态注册失败:" + result.getMsg());
  86 + return;
  87 + }
  88 +
  89 + // 3. 返回成功响应
  90 + log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName);
  91 + IotCoapUtils.respondSuccess(exchange, result.getData());
  92 + } catch (Exception e) {
  93 + log.error("[handle][设备动态注册处理异常]", e);
  94 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
  95 + }
  96 + }
  97 +
  98 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.eclipse.californium.core.CoapResource;
  5 +import org.eclipse.californium.core.server.resources.CoapExchange;
  6 +
  7 +/**
  8 + * IoT 网关 CoAP 协议的设备动态注册资源(/auth/register/device)
  9 + * <p>
  10 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class IotCoapRegisterResource extends CoapResource {
  16 +
  17 + public static final String PATH = "device";
  18 +
  19 + private final IotCoapRegisterHandler registerHandler;
  20 +
  21 + public IotCoapRegisterResource(IotCoapRegisterHandler registerHandler) {
  22 + super(PATH);
  23 + this.registerHandler = registerHandler;
  24 + log.info("[IotCoapRegisterResource][创建 CoAP 设备动态注册资源: /auth/register/{}]", PATH);
  25 + }
  26 +
  27 + @Override
  28 + public void handlePOST(CoapExchange exchange) {
  29 + log.debug("[handlePOST][收到设备动态注册请求]");
  30 + registerHandler.handle(exchange);
  31 + }
  32 +
  33 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.text.StrPool;
  6 +import cn.hutool.core.util.ArrayUtil;
  7 +import cn.hutool.core.util.ObjUtil;
  8 +import cn.hutool.core.util.StrUtil;
  9 +import cn.hutool.extra.spring.SpringUtil;
  10 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  12 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  13 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
  14 +import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
  15 +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import org.eclipse.californium.core.coap.CoAP;
  18 +import org.eclipse.californium.core.server.resources.CoapExchange;
  19 +
  20 +import java.util.List;
  21 +
  22 +/**
  23 + * IoT 网关 CoAP 协议的【上行】处理器
  24 + *
  25 + * 处理设备通过 CoAP 协议发送的上行消息,包括:
  26 + * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
  27 + * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
  28 + *
  29 + * Token 通过自定义 CoAP Option 2088 携带
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Slf4j
  34 +public class IotCoapUpstreamHandler {
  35 +
  36 + private final IotDeviceTokenService deviceTokenService;
  37 + private final IotDeviceMessageService deviceMessageService;
  38 +
  39 + public IotCoapUpstreamHandler() {
  40 + this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
  41 + this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
  42 + }
  43 +
  44 + /**
  45 + * 处理 CoAP 请求
  46 + *
  47 + * @param exchange CoAP 交换对象
  48 + * @param protocol 协议对象
  49 + */
  50 + public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
  51 + try {
  52 + // 1. 解析通用参数
  53 + List<String> uriPath = exchange.getRequestOptions().getUriPath();
  54 + String productKey = CollUtil.get(uriPath, 2);
  55 + String deviceName = CollUtil.get(uriPath, 3);
  56 + byte[] payload = exchange.getRequestPayload();
  57 + if (StrUtil.isEmpty(productKey)) {
  58 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
  59 + return;
  60 + }
  61 + if (StrUtil.isEmpty(deviceName)) {
  62 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
  63 + return;
  64 + }
  65 + if (ArrayUtil.isEmpty(payload)) {
  66 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
  67 + return;
  68 + }
  69 +
  70 + // 2. 认证:从自定义 Option 获取 token
  71 + String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
  72 + if (StrUtil.isEmpty(token)) {
  73 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
  74 + return;
  75 + }
  76 + // 验证 token
  77 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
  78 + if (deviceInfo == null) {
  79 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
  80 + return;
  81 + }
  82 + // 验证设备信息匹配
  83 + if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
  84 + || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
  85 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
  86 + return;
  87 + }
  88 +
  89 + // 2.1 解析 method:deviceName 后面的路径,用 . 拼接
  90 + // 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
  91 + String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
  92 +
  93 + // 2.2 解码消息
  94 + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
  95 + if (ObjUtil.notEqual(method, message.getMethod())) {
  96 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
  97 + return;
  98 + }
  99 + // 2.3 发送消息到消息总线
  100 + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
  101 +
  102 + // 3. 返回成功响应
  103 + IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
  104 + } catch (Exception e) {
  105 + log.error("[handle][CoAP 请求处理异常]", e);
  106 + IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
  107 + }
  108 + }
  109 +
  110 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
  2 +
  3 +import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.eclipse.californium.core.CoapResource;
  6 +import org.eclipse.californium.core.server.resources.CoapExchange;
  7 +import org.eclipse.californium.core.server.resources.Resource;
  8 +
  9 +/**
  10 + * IoT 网关 CoAP 协议的【上行】Topic 资源
  11 + *
  12 + * 支持任意深度的路径匹配:
  13 + * - /topic/sys/{productKey}/{deviceName}/thing/property/post
  14 + * - /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +public class IotCoapUpstreamTopicResource extends CoapResource {
  20 +
  21 + public static final String PATH = "topic";
  22 +
  23 + private final IotCoapUpstreamProtocol protocol;
  24 + private final IotCoapUpstreamHandler upstreamHandler;
  25 +
  26 + /**
  27 + * 创建根资源(/topic)
  28 + */
  29 + public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
  30 + IotCoapUpstreamHandler upstreamHandler) {
  31 + this(PATH, protocol, upstreamHandler);
  32 + log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
  33 + }
  34 +
  35 + /**
  36 + * 创建子资源(动态路径)
  37 + */
  38 + private IotCoapUpstreamTopicResource(String name,
  39 + IotCoapUpstreamProtocol protocol,
  40 + IotCoapUpstreamHandler upstreamHandler) {
  41 + super(name);
  42 + this.protocol = protocol;
  43 + this.upstreamHandler = upstreamHandler;
  44 + }
  45 +
  46 + @Override
  47 + public Resource getChild(String name) {
  48 + // 递归创建动态子资源,支持任意深度路径
  49 + return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
  50 + }
  51 +
  52 + @Override
  53 + public void handleGET(CoapExchange exchange) {
  54 + upstreamHandler.handle(exchange, protocol);
  55 + }
  56 +
  57 + @Override
  58 + public void handlePOST(CoapExchange exchange) {
  59 + upstreamHandler.handle(exchange, protocol);
  60 + }
  61 +
  62 + @Override
  63 + public void handlePUT(CoapExchange exchange) {
  64 + upstreamHandler.handle(exchange, protocol);
  65 + }
  66 +
  67 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import org.eclipse.californium.core.coap.CoAP;
  7 +import org.eclipse.californium.core.coap.MediaTypeRegistry;
  8 +import org.eclipse.californium.core.coap.Option;
  9 +import org.eclipse.californium.core.server.resources.CoapExchange;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
  12 +
  13 +/**
  14 + * IoT CoAP 协议工具类
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public class IotCoapUtils {
  19 +
  20 + /**
  21 + * 自定义 CoAP Option 编号,用于携带 Token
  22 + * <p>
  23 + * CoAP Option 范围 2048-65535 属于实验/自定义范围
  24 + */
  25 + public static final int OPTION_TOKEN = 2088;
  26 +
  27 + /**
  28 + * 返回成功响应
  29 + *
  30 + * @param exchange CoAP 交换对象
  31 + * @param data 响应数据
  32 + */
  33 + public static void respondSuccess(CoapExchange exchange, Object data) {
  34 + CommonResult<Object> result = CommonResult.success(data);
  35 + String json = JsonUtils.toJsonString(result);
  36 + exchange.respond(CoAP.ResponseCode.CONTENT, json, MediaTypeRegistry.APPLICATION_JSON);
  37 + }
  38 +
  39 + /**
  40 + * 返回错误响应
  41 + *
  42 + * @param exchange CoAP 交换对象
  43 + * @param code CoAP 响应码
  44 + * @param message 错误消息
  45 + */
  46 + public static void respondError(CoapExchange exchange, CoAP.ResponseCode code, String message) {
  47 + int errorCode = mapCoapCodeToErrorCode(code);
  48 + CommonResult<Object> result = CommonResult.error(errorCode, message);
  49 + String json = JsonUtils.toJsonString(result);
  50 + exchange.respond(code, json, MediaTypeRegistry.APPLICATION_JSON);
  51 + }
  52 +
  53 + /**
  54 + * 从自定义 CoAP Option 中获取 Token
  55 + *
  56 + * @param exchange CoAP 交换对象
  57 + * @param optionNumber Option 编号
  58 + * @return Token 值,如果不存在则返回 null
  59 + */
  60 + public static String getTokenFromOption(CoapExchange exchange, int optionNumber) {
  61 + Option option = CollUtil.findOne(exchange.getRequestOptions().getOthers(),
  62 + o -> o.getNumber() == optionNumber);
  63 + return option != null ? new String(option.getValue()) : null;
  64 + }
  65 +
  66 + /**
  67 + * 将 CoAP 响应码映射到业务错误码
  68 + *
  69 + * @param code CoAP 响应码
  70 + * @return 业务错误码
  71 + */
  72 + public static int mapCoapCodeToErrorCode(CoAP.ResponseCode code) {
  73 + if (code == CoAP.ResponseCode.BAD_REQUEST) {
  74 + return BAD_REQUEST.getCode();
  75 + } else if (code == CoAP.ResponseCode.UNAUTHORIZED) {
  76 + return UNAUTHORIZED.getCode();
  77 + } else if (code == CoAP.ResponseCode.FORBIDDEN) {
  78 + return FORBIDDEN.getCode();
  79 + } else {
  80 + return INTERNAL_SERVER_ERROR.getCode();
  81 + }
  82 + }
  83 +
  84 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/package-info.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.emqx;
0 \ No newline at end of file 2 \ No newline at end of file
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/router/IotEmqxAuthEventHandler.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  10 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
11 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 12 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
12 import io.vertx.core.json.JsonObject; 13 import io.vertx.core.json.JsonObject;
@@ -103,7 +104,7 @@ public class IotEmqxAuthEventHandler { @@ -103,7 +104,7 @@ public class IotEmqxAuthEventHandler {
103 JsonObject body = null; 104 JsonObject body = null;
104 try { 105 try {
105 // 1. 解析请求体 106 // 1. 解析请求体
106 - body = parseRequestBody(context); 107 + body = parseEventRequestBody(context);
107 if (body == null) { 108 if (body == null) {
108 return; 109 return;
109 } 110 }
@@ -152,7 +153,9 @@ public class IotEmqxAuthEventHandler { @@ -152,7 +153,9 @@ public class IotEmqxAuthEventHandler {
152 } 153 }
153 154
154 /** 155 /**
155 - * 解析请求体 156 + * 解析认证接口请求体
  157 + * <p>
  158 + * 认证接口解析失败时返回 JSON 格式响应(包含 result 字段)
156 * 159 *
157 * @param context 路由上下文 160 * @param context 路由上下文
158 * @return 请求体JSON对象,解析失败时返回null 161 * @return 请求体JSON对象,解析失败时返回null
@@ -174,6 +177,30 @@ public class IotEmqxAuthEventHandler { @@ -174,6 +177,30 @@ public class IotEmqxAuthEventHandler {
174 } 177 }
175 178
176 /** 179 /**
  180 + * 解析事件接口请求体
  181 + * <p>
  182 + * 事件接口解析失败时仅返回 200 状态码,无响应体(符合 EMQX Webhook 规范)
  183 + *
  184 + * @param context 路由上下文
  185 + * @return 请求体JSON对象,解析失败时返回null
  186 + */
  187 + private JsonObject parseEventRequestBody(RoutingContext context) {
  188 + try {
  189 + JsonObject body = context.body().asJsonObject();
  190 + if (body == null) {
  191 + log.info("[parseEventRequestBody][请求体为空]");
  192 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
  193 + return null;
  194 + }
  195 + return body;
  196 + } catch (Exception e) {
  197 + log.error("[parseEventRequestBody][body({}) 解析请求体失败]", context.body().asString(), e);
  198 + context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
  199 + return null;
  200 + }
  201 + }
  202 +
  203 + /**
177 * 执行设备认证 204 * 执行设备认证
178 * 205 *
179 * @param clientId 客户端ID 206 * @param clientId 客户端ID
@@ -201,7 +228,7 @@ public class IotEmqxAuthEventHandler { @@ -201,7 +228,7 @@ public class IotEmqxAuthEventHandler {
201 */ 228 */
202 private void handleDeviceStateChange(String username, boolean online) { 229 private void handleDeviceStateChange(String username, boolean online) {
203 // 1. 解析设备信息 230 // 1. 解析设备信息
204 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 231 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
205 if (deviceInfo == null) { 232 if (deviceInfo == null) {
206 log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username); 233 log.debug("[handleDeviceStateChange][跳过非设备({})连接]", username);
207 return; 234 return;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpUpstreamProtocol.java
@@ -3,8 +3,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http; @@ -3,8 +3,9 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.http;
3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 3 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; 4 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler; 5 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
  6 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler;
  7 +import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterSubHandler;
6 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler; 8 import cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
7 -import io.vertx.core.AbstractVerticle;  
8 import io.vertx.core.Vertx; 9 import io.vertx.core.Vertx;
9 import io.vertx.core.http.HttpServer; 10 import io.vertx.core.http.HttpServer;
10 import io.vertx.core.http.HttpServerOptions; 11 import io.vertx.core.http.HttpServerOptions;
@@ -22,31 +23,36 @@ import lombok.extern.slf4j.Slf4j; @@ -22,31 +23,36 @@ import lombok.extern.slf4j.Slf4j;
22 * @author 芋道源码 23 * @author 芋道源码
23 */ 24 */
24 @Slf4j 25 @Slf4j
25 -public class IotHttpUpstreamProtocol extends AbstractVerticle { 26 +public class IotHttpUpstreamProtocol {
26 27
27 private final IotGatewayProperties.HttpProperties httpProperties; 28 private final IotGatewayProperties.HttpProperties httpProperties;
28 29
  30 + private final Vertx vertx;
  31 +
29 private HttpServer httpServer; 32 private HttpServer httpServer;
30 33
31 @Getter 34 @Getter
32 private final String serverId; 35 private final String serverId;
33 36
34 - public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties) { 37 + public IotHttpUpstreamProtocol(IotGatewayProperties.HttpProperties httpProperties, Vertx vertx) {
35 this.httpProperties = httpProperties; 38 this.httpProperties = httpProperties;
  39 + this.vertx = vertx;
36 this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort()); 40 this.serverId = IotDeviceMessageUtils.generateServerId(httpProperties.getServerPort());
37 } 41 }
38 42
39 - @Override  
40 @PostConstruct 43 @PostConstruct
41 public void start() { 44 public void start() {
42 // 创建路由 45 // 创建路由
43 - Vertx vertx = Vertx.vertx();  
44 Router router = Router.router(vertx); 46 Router router = Router.router(vertx);
45 router.route().handler(BodyHandler.create()); 47 router.route().handler(BodyHandler.create());
46 48
47 // 创建处理器,添加路由处理器 49 // 创建处理器,添加路由处理器
48 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this); 50 IotHttpAuthHandler authHandler = new IotHttpAuthHandler(this);
49 router.post(IotHttpAuthHandler.PATH).handler(authHandler); 51 router.post(IotHttpAuthHandler.PATH).handler(authHandler);
  52 + IotHttpRegisterHandler registerHandler = new IotHttpRegisterHandler();
  53 + router.post(IotHttpRegisterHandler.PATH).handler(registerHandler);
  54 + IotHttpRegisterSubHandler registerSubHandler = new IotHttpRegisterSubHandler();
  55 + router.post(IotHttpRegisterSubHandler.PATH).handler(registerSubHandler);
50 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this); 56 IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
51 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler); 57 router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
52 58
@@ -70,7 +76,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle { @@ -70,7 +76,6 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
70 } 76 }
71 } 77 }
72 78
73 - @Override  
74 @PreDestroy 79 @PreDestroy
75 public void stop() { 80 public void stop() {
76 if (httpServer != null) { 81 if (httpServer != null) {
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/package-info.java 0 → 100644
  1 +/**
  2 + * HTTP 协议实现包
  3 + * <p>
  4 + * 提供基于 Vert.x HTTP Server 的 IoT 设备连接和消息处理功能
  5 + */
  6 +package cn.iocoder.yudao.module.iot.gateway.protocol.http;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAbstractHandler.java
@@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil; @@ -7,7 +7,8 @@ import cn.hutool.extra.spring.SpringUtil;
7 import cn.iocoder.yudao.framework.common.exception.ServiceException; 7 import cn.iocoder.yudao.framework.common.exception.ServiceException;
8 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 9 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 10 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
  11 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
11 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 12 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
12 import io.vertx.core.Handler; 13 import io.vertx.core.Handler;
13 import io.vertx.core.http.HttpHeaders; 14 import io.vertx.core.http.HttpHeaders;
@@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt; @@ -54,7 +55,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
54 private void beforeHandle(RoutingContext context) { 55 private void beforeHandle(RoutingContext context) {
55 // 如果不需要认证,则不走前置处理 56 // 如果不需要认证,则不走前置处理
56 String path = context.request().path(); 57 String path = context.request().path();
57 - if (ObjUtil.equal(path, IotHttpAuthHandler.PATH)) { 58 + if (ObjectUtils.equalsAny(path, IotHttpAuthHandler.PATH, IotHttpRegisterHandler.PATH)) {
58 return; 59 return;
59 } 60 }
60 61
@@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt; @@ -73,7 +74,7 @@ public abstract class IotHttpAbstractHandler implements Handler&lt;RoutingContext&gt;
73 } 74 }
74 75
75 // 校验 token 76 // 校验 token
76 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.verifyToken(token); 77 + IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
77 Assert.notNull(deviceInfo, "设备信息不能为空"); 78 Assert.notNull(deviceInfo, "设备信息不能为空");
78 // 校验设备信息是否匹配 79 // 校验设备信息是否匹配
79 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey()) 80 if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpAuthHandler.java
@@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 9 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 12 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; 13 import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
14 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService; 14 import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
@@ -51,6 +51,9 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { @@ -51,6 +51,9 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
51 public CommonResult<Object> handle0(RoutingContext context) { 51 public CommonResult<Object> handle0(RoutingContext context) {
52 // 1. 解析参数 52 // 1. 解析参数
53 JsonObject body = context.body().asJsonObject(); 53 JsonObject body = context.body().asJsonObject();
  54 + if (body == null) {
  55 + throw invalidParamException("请求体不能为空");
  56 + }
54 String clientId = body.getString("clientId"); 57 String clientId = body.getString("clientId");
55 if (StrUtil.isEmpty(clientId)) { 58 if (StrUtil.isEmpty(clientId)) {
56 throw invalidParamException("clientId 不能为空"); 59 throw invalidParamException("clientId 不能为空");
@@ -72,7 +75,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler { @@ -72,7 +75,7 @@ public class IotHttpAuthHandler extends IotHttpAbstractHandler {
72 throw exception(DEVICE_AUTH_FAIL); 75 throw exception(DEVICE_AUTH_FAIL);
73 } 76 }
74 // 2.2 生成 Token 77 // 2.2 生成 Token
75 - IotDeviceAuthUtils.DeviceInfo deviceInfo = deviceTokenService.parseUsername(username); 78 + IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
76 Assert.notNull(deviceInfo, "设备信息不能为空"); 79 Assert.notNull(deviceInfo, "设备信息不能为空");
77 String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName()); 80 String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
78 Assert.notBlank(token, "生成 token 不能为空位"); 81 Assert.notBlank(token, "生成 token 不能为空位");
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
  9 +import io.vertx.core.json.JsonObject;
  10 +import io.vertx.ext.web.RoutingContext;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  13 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  14 +
  15 +/**
  16 + * IoT 网关 HTTP 协议的【设备动态注册】处理器
  17 + * <p>
  18 + * 用于直连设备/网关的一型一密动态注册,不需要认证
  19 + *
  20 + * @author 芋道源码
  21 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  22 + */
  23 +public class IotHttpRegisterHandler extends IotHttpAbstractHandler {
  24 +
  25 + public static final String PATH = "/auth/register/device";
  26 +
  27 + private final IotDeviceCommonApi deviceApi;
  28 +
  29 + public IotHttpRegisterHandler() {
  30 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  31 + }
  32 +
  33 + @Override
  34 + public CommonResult<Object> handle0(RoutingContext context) {
  35 + // 1. 解析参数
  36 + JsonObject body = context.body().asJsonObject();
  37 + if (body == null) {
  38 + throw invalidParamException("请求体不能为空");
  39 + }
  40 + String productKey = body.getString("productKey");
  41 + if (StrUtil.isEmpty(productKey)) {
  42 + throw invalidParamException("productKey 不能为空");
  43 + }
  44 + String deviceName = body.getString("deviceName");
  45 + if (StrUtil.isEmpty(deviceName)) {
  46 + throw invalidParamException("deviceName 不能为空");
  47 + }
  48 + String productSecret = body.getString("productSecret");
  49 + if (StrUtil.isEmpty(productSecret)) {
  50 + throw invalidParamException("productSecret 不能为空");
  51 + }
  52 +
  53 + // 2. 调用动态注册
  54 + IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
  55 + .setProductKey(productKey).setDeviceName(deviceName).setProductSecret(productSecret);
  56 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(reqDTO);
  57 + result.checkError();
  58 +
  59 + // 3. 返回结果
  60 + return success(result.getData());
  61 + }
  62 +
  63 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpRegisterSubHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.protocol.http.router;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
  7 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
  8 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  9 +import io.vertx.core.json.JsonObject;
  10 +import io.vertx.ext.web.RoutingContext;
  11 +
  12 +import java.util.List;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  15 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  16 +
  17 +/**
  18 + * IoT 网关 HTTP 协议的【子设备动态注册】处理器
  19 + * <p>
  20 + * 用于子设备的动态注册,需要网关认证
  21 + *
  22 + * @author 芋道源码
  23 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/register-devices">阿里云 - 动态注册子设备</a>
  24 + */
  25 +public class IotHttpRegisterSubHandler extends IotHttpAbstractHandler {
  26 +
  27 + /**
  28 + * 路径:/auth/register/sub-device/:productKey/:deviceName
  29 + * <p>
  30 + * productKey 和 deviceName 是网关设备的标识
  31 + */
  32 + public static final String PATH = "/auth/register/sub-device/:productKey/:deviceName";
  33 +
  34 + private final IotDeviceCommonApi deviceApi;
  35 +
  36 + public IotHttpRegisterSubHandler() {
  37 + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  38 + }
  39 +
  40 + @Override
  41 + public CommonResult<Object> handle0(RoutingContext context) {
  42 + // 1. 解析通用参数
  43 + String productKey = context.pathParam("productKey");
  44 + String deviceName = context.pathParam("deviceName");
  45 +
  46 + // 2. 解析子设备列表
  47 + JsonObject body = context.body().asJsonObject();
  48 + if (body == null) {
  49 + throw invalidParamException("请求体不能为空");
  50 + }
  51 + if (body.getJsonArray("params") == null) {
  52 + throw invalidParamException("params 不能为空");
  53 + }
  54 + List<cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO> subDevices = JsonUtils.parseArray(
  55 + body.getJsonArray("params").toString(), cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO.class);
  56 +
  57 + // 3. 调用子设备动态注册
  58 + IotSubDeviceRegisterFullReqDTO reqDTO = new IotSubDeviceRegisterFullReqDTO()
  59 + .setGatewayProductKey(productKey).setGatewayDeviceName(deviceName).setSubDevices(subDevices);
  60 + CommonResult<List<IotSubDeviceRegisterRespDTO>> result = deviceApi.registerSubDevices(reqDTO);
  61 + result.checkError();
  62 +
  63 + // 4. 返回结果
  64 + return success(result.getData());
  65 + }
  66 +
  67 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/router/IotHttpUpstreamHandler.java
@@ -12,6 +12,8 @@ import io.vertx.ext.web.RoutingContext; @@ -12,6 +12,8 @@ import io.vertx.ext.web.RoutingContext;
12 import lombok.RequiredArgsConstructor; 12 import lombok.RequiredArgsConstructor;
13 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
14 14
  15 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  16 +
15 /** 17 /**
16 * IoT 网关 HTTP 协议的【上行】处理器 18 * IoT 网关 HTTP 协议的【上行】处理器
17 * 19 *
@@ -40,6 +42,9 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler { @@ -40,6 +42,9 @@ public class IotHttpUpstreamHandler extends IotHttpAbstractHandler {
40 String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT); 42 String method = context.pathParam("*").replaceAll(StrPool.SLASH, StrPool.DOT);
41 43
42 // 2.1 解析消息 44 // 2.1 解析消息
  45 + if (context.body().buffer() == null) {
  46 + throw invalidParamException("请求体不能为空");
  47 + }
43 byte[] bytes = context.body().buffer().getBytes(); 48 byte[] bytes = context.body().buffer().getBytes();
44 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, 49 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes,
45 productKey, deviceName); 50 productKey, deviceName);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager;
2 2
3 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import io.netty.handler.codec.mqtt.MqttQoS; 4 import io.netty.handler.codec.mqtt.MqttQoS;
  5 +import io.vertx.core.buffer.Buffer;
5 import io.vertx.mqtt.MqttEndpoint; 6 import io.vertx.mqtt.MqttEndpoint;
6 import lombok.Data; 7 import lombok.Data;
7 import lombok.extern.slf4j.Slf4j; 8 import lombok.extern.slf4j.Slf4j;
@@ -87,9 +88,9 @@ public class IotMqttConnectionManager { @@ -87,9 +88,9 @@ public class IotMqttConnectionManager {
87 connectionMap.remove(oldEndpoint); 88 connectionMap.remove(oldEndpoint);
88 } 89 }
89 90
  91 + // 注册新连接
90 connectionMap.put(endpoint, connectionInfo); 92 connectionMap.put(endpoint, connectionInfo);
91 deviceEndpointMap.put(deviceId, endpoint); 93 deviceEndpointMap.put(deviceId, endpoint);
92 -  
93 log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", 94 log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]",
94 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); 95 deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
95 } 96 }
@@ -101,13 +102,12 @@ public class IotMqttConnectionManager { @@ -101,13 +102,12 @@ public class IotMqttConnectionManager {
101 */ 102 */
102 public void unregisterConnection(MqttEndpoint endpoint) { 103 public void unregisterConnection(MqttEndpoint endpoint) {
103 ConnectionInfo connectionInfo = connectionMap.remove(endpoint); 104 ConnectionInfo connectionInfo = connectionMap.remove(endpoint);
104 - if (connectionInfo != null) {  
105 - Long deviceId = connectionInfo.getDeviceId();  
106 - deviceEndpointMap.remove(deviceId);  
107 -  
108 - log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId,  
109 - getEndpointAddress(endpoint)); 105 + if (connectionInfo == null) {
  106 + return;
110 } 107 }
  108 + Long deviceId = connectionInfo.getDeviceId();
  109 + deviceEndpointMap.remove(deviceId);
  110 + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, getEndpointAddress(endpoint));
111 } 111 }
112 112
113 /** 113 /**
@@ -166,7 +166,7 @@ public class IotMqttConnectionManager { @@ -166,7 +166,7 @@ public class IotMqttConnectionManager {
166 } 166 }
167 167
168 try { 168 try {
169 - endpoint.publish(topic, io.vertx.core.buffer.Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain); 169 + endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
170 log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos); 170 log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos);
171 return true; 171 return true;
172 } catch (Exception e) { 172 } catch (Exception e) {
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2 2
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.ArrayUtil;
3 import cn.hutool.core.util.BooleanUtil; 5 import cn.hutool.core.util.BooleanUtil;
4 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
5 import cn.hutool.extra.spring.SpringUtil; 7 import cn.hutool.extra.spring.SpringUtil;
6 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
7 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; 10 import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
8 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; 11 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
9 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO; 12 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
10 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 13 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
  14 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 15 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  16 +import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
  17 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
  18 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; 19 import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
13 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol; 20 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
14 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 21 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
15 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 22 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
  23 +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
16 import io.netty.handler.codec.mqtt.MqttConnectReturnCode; 24 import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
17 import io.netty.handler.codec.mqtt.MqttQoS; 25 import io.netty.handler.codec.mqtt.MqttQoS;
18 import io.vertx.mqtt.MqttEndpoint; 26 import io.vertx.mqtt.MqttEndpoint;
@@ -20,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription; @@ -20,6 +28,7 @@ import io.vertx.mqtt.MqttTopicSubscription;
20 import lombok.extern.slf4j.Slf4j; 28 import lombok.extern.slf4j.Slf4j;
21 29
22 import java.util.List; 30 import java.util.List;
  31 +import java.util.Map;
23 32
24 /** 33 /**
25 * MQTT 上行消息处理器 34 * MQTT 上行消息处理器
@@ -29,6 +38,16 @@ import java.util.List; @@ -29,6 +38,16 @@ import java.util.List;
29 @Slf4j 38 @Slf4j
30 public class IotMqttUpstreamHandler { 39 public class IotMqttUpstreamHandler {
31 40
  41 + /**
  42 + * 默认编解码类型(MQTT 使用 Alink 协议)
  43 + */
  44 + private static final String DEFAULT_CODEC_TYPE = "Alink";
  45 +
  46 + /**
  47 + * register 请求的 topic 后缀
  48 + */
  49 + private static final String REGISTER_TOPIC_SUFFIX = "/thing/auth/register";
  50 +
32 private final IotDeviceMessageService deviceMessageService; 51 private final IotDeviceMessageService deviceMessageService;
33 52
34 private final IotMqttConnectionManager connectionManager; 53 private final IotMqttConnectionManager connectionManager;
@@ -84,20 +103,28 @@ public class IotMqttUpstreamHandler { @@ -84,20 +103,28 @@ public class IotMqttUpstreamHandler {
84 }); 103 });
85 104
86 // 4. 设置消息处理器 105 // 4. 设置消息处理器
87 - endpoint.publishHandler(message -> { 106 + endpoint.publishHandler(mqttMessage -> {
88 try { 107 try {
89 - processMessage(clientId, message.topicName(), message.payload().getBytes()); 108 + // 4.1 根据 topic 判断是否为 register 请求
  109 + String topic = mqttMessage.topicName();
  110 + byte[] payload = mqttMessage.payload().getBytes();
  111 + if (topic.endsWith(REGISTER_TOPIC_SUFFIX)) {
  112 + // register 请求:使用默认编解码器处理(设备可能未注册)
  113 + processRegisterMessage(clientId, topic, payload, endpoint);
  114 + } else {
  115 + // 业务请求:正常处理
  116 + processMessage(clientId, topic, payload);
  117 + }
90 118
91 - // 根据 QoS 级别发送相应的确认消息  
92 - if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) { 119 + // 4.2 根据 QoS 级别发送相应的确认消息
  120 + if (mqttMessage.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
93 // QoS 1: 发送 PUBACK 确认 121 // QoS 1: 发送 PUBACK 确认
94 - endpoint.publishAcknowledge(message.messageId());  
95 - } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) { 122 + endpoint.publishAcknowledge(mqttMessage.messageId());
  123 + } else if (mqttMessage.qosLevel() == MqttQoS.EXACTLY_ONCE) {
96 // QoS 2: 发送 PUBREC 确认 124 // QoS 2: 发送 PUBREC 确认
97 - endpoint.publishReceived(message.messageId()); 125 + endpoint.publishReceived(mqttMessage.messageId());
98 } 126 }
99 // QoS 0 无需确认 127 // QoS 0 无需确认
100 -  
101 } catch (Exception e) { 128 } catch (Exception e) {
102 log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]", 129 log.error("[handle][消息解码失败,断开连接,客户端 ID: {},地址: {},错误: {}]",
103 clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage()); 130 clientId, connectionManager.getEndpointAddress(endpoint), e.getMessage());
@@ -160,10 +187,9 @@ public class IotMqttUpstreamHandler { @@ -160,10 +187,9 @@ public class IotMqttUpstreamHandler {
160 return; 187 return;
161 } 188 }
162 189
  190 + // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)
163 String productKey = topicParts[2]; 191 String productKey = topicParts[2];
164 String deviceName = topicParts[3]; 192 String deviceName = topicParts[3];
165 -  
166 - // 3. 解码消息(使用从 topic 解析的 productKey 和 deviceName)  
167 try { 193 try {
168 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName); 194 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
169 if (message == null) { 195 if (message == null) {
@@ -171,10 +197,9 @@ public class IotMqttUpstreamHandler { @@ -171,10 +197,9 @@ public class IotMqttUpstreamHandler {
171 return; 197 return;
172 } 198 }
173 199
  200 + // 4. 处理业务消息(认证已在连接时完成)
174 log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]", 201 log.info("[processMessage][收到设备消息,设备: {}.{}, 方法: {}]",
175 productKey, deviceName, message.getMethod()); 202 productKey, deviceName, message.getMethod());
176 -  
177 - // 4. 处理业务消息(认证已在连接时完成)  
178 handleBusinessRequest(message, productKey, deviceName); 203 handleBusinessRequest(message, productKey, deviceName);
179 } catch (Exception e) { 204 } catch (Exception e) {
180 log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]", 205 log.error("[processMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
@@ -214,7 +239,7 @@ public class IotMqttUpstreamHandler { @@ -214,7 +239,7 @@ public class IotMqttUpstreamHandler {
214 } 239 }
215 240
216 // 4. 获取设备信息 241 // 4. 获取设备信息
217 - IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username); 242 + IotDeviceIdentity deviceInfo = IotDeviceAuthUtils.parseUsername(username);
218 if (deviceInfo == null) { 243 if (deviceInfo == null) {
219 log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username); 244 log.warn("[authenticateDevice][用户名格式不正确,客户端 ID: {},用户名: {}]", clientId, username);
220 return false; 245 return false;
@@ -246,6 +271,186 @@ public class IotMqttUpstreamHandler { @@ -246,6 +271,186 @@ public class IotMqttUpstreamHandler {
246 } 271 }
247 272
248 /** 273 /**
  274 + * 处理 register 消息(设备动态注册,使用默认编解码器)
  275 + *
  276 + * @param clientId 客户端 ID
  277 + * @param topic 主题
  278 + * @param payload 消息内容
  279 + * @param endpoint MQTT 连接端点
  280 + */
  281 + private void processRegisterMessage(String clientId, String topic, byte[] payload, MqttEndpoint endpoint) {
  282 + // 1.1 基础检查
  283 + if (ArrayUtil.isEmpty(payload)) {
  284 + return;
  285 + }
  286 + // 1.2 解析主题,获取 productKey 和 deviceName
  287 + String[] topicParts = topic.split("/");
  288 + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  289 + log.warn("[processRegisterMessage][topic({}) 格式不正确]", topic);
  290 + return;
  291 + }
  292 + String productKey = topicParts[2];
  293 + String deviceName = topicParts[3];
  294 +
  295 + // 2. 使用默认编解码器解码消息(设备可能未注册,无法获取 codecType)
  296 + IotDeviceMessage message;
  297 + try {
  298 + message = deviceMessageService.decodeDeviceMessage(payload, DEFAULT_CODEC_TYPE);
  299 + if (message == null) {
  300 + log.warn("[processRegisterMessage][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  301 + return;
  302 + }
  303 + } catch (Exception e) {
  304 + log.error("[processRegisterMessage][消息解码异常,客户端 ID: {},主题: {},错误: {}]",
  305 + clientId, topic, e.getMessage(), e);
  306 + return;
  307 + }
  308 +
  309 + // 3. 处理设备动态注册请求
  310 + log.info("[processRegisterMessage][收到设备注册消息,设备: {}.{}, 方法: {}]",
  311 + productKey, deviceName, message.getMethod());
  312 + try {
  313 + handleRegisterRequest(message, productKey, deviceName, endpoint);
  314 + } catch (Exception e) {
  315 + log.error("[processRegisterMessage][消息处理异常,客户端 ID: {},主题: {},错误: {}]",
  316 + clientId, topic, e.getMessage(), e);
  317 + }
  318 + }
  319 +
  320 + /**
  321 + * 处理设备动态注册请求(一型一密,不需要 deviceSecret)
  322 + *
  323 + * @param message 消息信息
  324 + * @param productKey 产品 Key
  325 + * @param deviceName 设备名称
  326 + * @param endpoint MQTT 连接端点
  327 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification">阿里云 - 一型一密</a>
  328 + */
  329 + private void handleRegisterRequest(IotDeviceMessage message, String productKey, String deviceName, MqttEndpoint endpoint) {
  330 + String clientId = endpoint.clientIdentifier();
  331 + try {
  332 + // 1. 解析注册参数
  333 + IotDeviceRegisterReqDTO params = parseRegisterParams(message.getParams());
  334 + if (params == null) {
  335 + log.warn("[handleRegisterRequest][注册参数解析失败,客户端 ID: {}]", clientId);
  336 + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册参数不完整");
  337 + return;
  338 + }
  339 +
  340 + // 2. 调用动态注册 API
  341 + CommonResult<IotDeviceRegisterRespDTO> result = deviceApi.registerDevice(params);
  342 + if (result.isError()) {
  343 + log.warn("[handleRegisterRequest][注册失败,客户端 ID: {},错误: {}]", clientId, result.getMsg());
  344 + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getMsg());
  345 + return;
  346 + }
  347 +
  348 + // 3. 发送成功响应(包含 deviceSecret)
  349 + sendRegisterSuccessResponse(endpoint, productKey, deviceName, message.getRequestId(), result.getData());
  350 + log.info("[handleRegisterRequest][注册成功,设备名: {},客户端 ID: {}]",
  351 + params.getDeviceName(), clientId);
  352 + } catch (Exception e) {
  353 + log.error("[handleRegisterRequest][注册处理异常,客户端 ID: {}]", clientId, e);
  354 + sendRegisterErrorResponse(endpoint, productKey, deviceName, message.getRequestId(), "注册处理异常");
  355 + }
  356 + }
  357 +
  358 + /**
  359 + * 解析注册参数
  360 + *
  361 + * @param params 参数对象(通常为 Map 类型)
  362 + * @return 注册参数 DTO,解析失败时返回 null
  363 + */
  364 + @SuppressWarnings({"unchecked", "DuplicatedCode"})
  365 + private IotDeviceRegisterReqDTO parseRegisterParams(Object params) {
  366 + if (params == null) {
  367 + return null;
  368 + }
  369 + try {
  370 + // 参数默认为 Map 类型,直接转换
  371 + if (params instanceof Map) {
  372 + Map<String, Object> paramMap = (Map<String, Object>) params;
  373 + return new IotDeviceRegisterReqDTO()
  374 + .setProductKey(MapUtil.getStr(paramMap, "productKey"))
  375 + .setDeviceName(MapUtil.getStr(paramMap, "deviceName"))
  376 + .setProductSecret(MapUtil.getStr(paramMap, "productSecret"));
  377 + }
  378 + // 如果已经是目标类型,直接返回
  379 + if (params instanceof IotDeviceRegisterReqDTO) {
  380 + return (IotDeviceRegisterReqDTO) params;
  381 + }
  382 +
  383 + // 其他情况尝试 JSON 转换
  384 + return JsonUtils.convertObject(params, IotDeviceRegisterReqDTO.class);
  385 + } catch (Exception e) {
  386 + log.error("[parseRegisterParams][解析注册参数({})失败]", params, e);
  387 + return null;
  388 + }
  389 + }
  390 +
  391 + /**
  392 + * 发送注册成功响应(包含 deviceSecret)
  393 + *
  394 + * @param endpoint MQTT 连接端点
  395 + * @param productKey 产品 Key
  396 + * @param deviceName 设备名称
  397 + * @param requestId 请求 ID
  398 + * @param registerResp 注册响应
  399 + */
  400 + private void sendRegisterSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  401 + String requestId, IotDeviceRegisterRespDTO registerResp) {
  402 + try {
  403 + // 1. 构建响应消息(参考 HTTP 返回格式,直接返回 IotDeviceRegisterRespDTO)
  404 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
  405 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), registerResp, 0, null);
  406 +
  407 + // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
  408 + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
  409 +
  410 + // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
  411 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
  412 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
  413 + endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
  414 + MqttQoS.AT_LEAST_ONCE, false, false);
  415 + log.debug("[sendRegisterSuccessResponse][发送注册成功响应,主题: {}]", replyTopic);
  416 + } catch (Exception e) {
  417 + log.error("[sendRegisterSuccessResponse][发送注册成功响应异常,客户端 ID: {}]",
  418 + endpoint.clientIdentifier(), e);
  419 + }
  420 + }
  421 +
  422 + /**
  423 + * 发送注册错误响应
  424 + *
  425 + * @param endpoint MQTT 连接端点
  426 + * @param productKey 产品 Key
  427 + * @param deviceName 设备名称
  428 + * @param requestId 请求 ID
  429 + * @param errorMessage 错误消息
  430 + */
  431 + private void sendRegisterErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName,
  432 + String requestId, String errorMessage) {
  433 + try {
  434 + // 1. 构建响应消息
  435 + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId,
  436 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), null, 400, errorMessage);
  437 +
  438 + // 2. 编码消息(使用默认编解码器,因为设备可能还未注册)
  439 + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE);
  440 +
  441 + // 3. 构建响应主题并发送(格式:/sys/{productKey}/{deviceName}/thing/auth/register_reply)
  442 + String replyTopic = IotMqttTopicUtils.buildTopicByMethod(
  443 + IotDeviceMessageMethodEnum.DEVICE_REGISTER.getMethod(), productKey, deviceName, true);
  444 + endpoint.publish(replyTopic, io.vertx.core.buffer.Buffer.buffer(encodedData),
  445 + MqttQoS.AT_LEAST_ONCE, false, false);
  446 + log.debug("[sendRegisterErrorResponse][发送注册错误响应,主题: {}]", replyTopic);
  447 + } catch (Exception e) {
  448 + log.error("[sendRegisterErrorResponse][发送注册错误响应异常,客户端 ID: {}]",
  449 + endpoint.clientIdentifier(), e);
  450 + }
  451 + }
  452 +
  453 + /**
249 * 处理业务请求 454 * 处理业务请求
250 */ 455 */
251 private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) { 456 private void handleBusinessRequest(IotDeviceMessage message, String productKey, String deviceName) {
@@ -257,9 +462,7 @@ public class IotMqttUpstreamHandler { @@ -257,9 +462,7 @@ public class IotMqttUpstreamHandler {
257 /** 462 /**
258 * 注册连接 463 * 注册连接
259 */ 464 */
260 - private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device,  
261 - String clientId) {  
262 - 465 + private void registerConnection(MqttEndpoint endpoint, IotDeviceRespDTO device, String clientId) {
263 IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo() 466 IotMqttConnectionManager.ConnectionInfo connectionInfo = new IotMqttConnectionManager.ConnectionInfo()
264 .setDeviceId(device.getId()) 467 .setDeviceId(device.getId())
265 .setProductKey(device.getProductKey()) 468 .setProductKey(device.getProductKey())
@@ -267,7 +470,6 @@ public class IotMqttUpstreamHandler { @@ -267,7 +470,6 @@ public class IotMqttUpstreamHandler {
267 .setClientId(clientId) 470 .setClientId(clientId)
268 .setAuthenticated(true) 471 .setAuthenticated(true)
269 .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); 472 .setRemoteAddress(connectionManager.getEndpointAddress(endpoint));
270 -  
271 connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); 473 connectionManager.registerConnection(endpoint, device.getId(), connectionInfo);
272 } 474 }
273 475
@@ -296,15 +498,13 @@ public class IotMqttUpstreamHandler { @@ -296,15 +498,13 @@ public class IotMqttUpstreamHandler {
296 IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); 498 IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline();
297 deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(), 499 deviceMessageService.sendDeviceMessage(offlineMessage, connectionInfo.getProductKey(),
298 connectionInfo.getDeviceName(), serverId); 500 connectionInfo.getDeviceName(), serverId);
299 - log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]",  
300 - connectionInfo.getDeviceId(), connectionInfo.getDeviceName()); 501 + log.info("[cleanupConnection][设备离线,设备 ID: {},设备名称: {}]", connectionInfo.getDeviceId(), connectionInfo.getDeviceName());
301 } 502 }
302 503
303 // 注销连接 504 // 注销连接
304 connectionManager.unregisterConnection(endpoint); 505 connectionManager.unregisterConnection(endpoint);
305 } catch (Exception e) { 506 } catch (Exception e) {
306 - log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]",  
307 - endpoint.clientIdentifier(), e.getMessage()); 507 + log.error("[cleanupConnection][清理连接失败,客户端 ID: {},错误: {}]", endpoint.clientIdentifier(), e.getMessage());
308 } 508 }
309 } 509 }
310 510
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsDownstreamSubscriber.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;  
2 -  
3 -import cn.hutool.core.util.StrUtil;  
4 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;  
5 -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;  
6 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
7 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;  
8 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;  
9 -import jakarta.annotation.PostConstruct;  
10 -import lombok.extern.slf4j.Slf4j;  
11 -  
12 -/**  
13 - * IoT MQTT WebSocket 下行消息订阅器  
14 - * <p>  
15 - * 订阅消息总线的设备下行消息,并通过 WebSocket 发送到设备  
16 - *  
17 - * @author 芋道源码  
18 - */  
19 -@Slf4j  
20 -public class IotMqttWsDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> {  
21 -  
22 - private final IotMqttWsUpstreamProtocol upstreamProtocol;  
23 - private final IotMqttWsDownstreamHandler downstreamHandler;  
24 - private final IotMessageBus messageBus;  
25 -  
26 - public IotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol upstreamProtocol,  
27 - IotMqttWsDownstreamHandler downstreamHandler,  
28 - IotMessageBus messageBus) {  
29 - this.upstreamProtocol = upstreamProtocol;  
30 - this.downstreamHandler = downstreamHandler;  
31 - this.messageBus = messageBus;  
32 - }  
33 -  
34 - @PostConstruct  
35 - public void init() {  
36 - messageBus.register(this);  
37 - log.info("[init][MQTT WebSocket 下行消息订阅器已启动,topic: {}]", getTopic());  
38 - }  
39 -  
40 - @Override  
41 - public String getTopic() {  
42 - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(upstreamProtocol.getServerId());  
43 - }  
44 -  
45 - @Override  
46 - public String getGroup() {  
47 - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group  
48 - return getTopic();  
49 - }  
50 -  
51 - @Override  
52 - public void onMessage(IotDeviceMessage message) {  
53 - log.debug("[onMessage][收到下行消息,deviceId: {},method: {}]",  
54 - message.getDeviceId(), message.getMethod());  
55 - try {  
56 - // 1. 校验  
57 - String method = message.getMethod();  
58 - if (StrUtil.isBlank(method)) {  
59 - log.warn("[onMessage][消息方法为空,deviceId: {}]", message.getDeviceId());  
60 - return;  
61 - }  
62 -  
63 - // 2. 委托给下行处理器处理业务逻辑  
64 - boolean success = downstreamHandler.handleDownstreamMessage(message);  
65 - if (success) {  
66 - log.debug("[onMessage][下行消息处理成功,deviceId: {},method: {}]",  
67 - message.getDeviceId(), message.getMethod());  
68 - } else {  
69 - log.warn("[onMessage][下行消息处理失败,deviceId: {},method: {}]",  
70 - message.getDeviceId(), message.getMethod());  
71 - }  
72 - } catch (Exception e) {  
73 - log.error("[onMessage][处理下行消息失败,deviceId: {},method: {}]",  
74 - message.getDeviceId(), message.getMethod(), e);  
75 - }  
76 - }  
77 -  
78 -}  
79 -  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/IotMqttWsUpstreamProtocol.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;  
2 -  
3 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;  
4 -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;  
5 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;  
6 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsUpstreamHandler;  
7 -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;  
8 -import io.vertx.core.Vertx;  
9 -import io.vertx.core.http.HttpServer;  
10 -import io.vertx.core.http.HttpServerOptions;  
11 -import io.vertx.core.http.ServerWebSocket;  
12 -import jakarta.annotation.PostConstruct;  
13 -import jakarta.annotation.PreDestroy;  
14 -import lombok.Getter;  
15 -import lombok.extern.slf4j.Slf4j;  
16 -  
17 -/**  
18 - * IoT 网关 MQTT WebSocket 协议:接收设备上行消息  
19 - * <p>  
20 - * 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:  
21 - * - 标准 MQTT 3.1.1 协议  
22 - * - WebSocket 协议升级  
23 - * - SSL/TLS 加密(wss://)  
24 - * - 设备认证与连接管理  
25 - * - QoS 0/1/2 消息质量保证  
26 - *  
27 - * @author 芋道源码  
28 - */  
29 -@Slf4j  
30 -public class IotMqttWsUpstreamProtocol {  
31 -  
32 - private final IotGatewayProperties.MqttWsProperties mqttWsProperties;  
33 -  
34 - private final IotDeviceMessageService messageService;  
35 -  
36 - private final IotMqttWsConnectionManager connectionManager;  
37 -  
38 - private final Vertx vertx;  
39 -  
40 - @Getter  
41 - private final String serverId;  
42 -  
43 - private HttpServer httpServer;  
44 -  
45 - public IotMqttWsUpstreamProtocol(IotGatewayProperties.MqttWsProperties mqttWsProperties,  
46 - IotDeviceMessageService messageService,  
47 - IotMqttWsConnectionManager connectionManager,  
48 - Vertx vertx) {  
49 - this.mqttWsProperties = mqttWsProperties;  
50 - this.messageService = messageService;  
51 - this.connectionManager = connectionManager;  
52 - this.vertx = vertx;  
53 - this.serverId = IotDeviceMessageUtils.generateServerId(mqttWsProperties.getPort());  
54 - }  
55 -  
56 - @PostConstruct  
57 - public void start() {  
58 - // 创建 HTTP 服务器选项  
59 - HttpServerOptions options = new HttpServerOptions()  
60 - .setPort(mqttWsProperties.getPort())  
61 - .setIdleTimeout(mqttWsProperties.getKeepAliveTimeoutSeconds())  
62 - .setMaxWebSocketFrameSize(mqttWsProperties.getMaxFrameSize())  
63 - .setMaxWebSocketMessageSize(mqttWsProperties.getMaxMessageSize())  
64 - // 配置 WebSocket 子协议支持  
65 - .addWebSocketSubProtocol(mqttWsProperties.getSubProtocol());  
66 -  
67 - // 配置 SSL(如果启用)  
68 - if (Boolean.TRUE.equals(mqttWsProperties.getSslEnabled())) {  
69 - options.setSsl(true)  
70 - .setKeyCertOptions(mqttWsProperties.getSslOptions().getKeyCertOptions())  
71 - .setTrustOptions(mqttWsProperties.getSslOptions().getTrustOptions());  
72 - log.info("[start][MQTT WebSocket 已启用 SSL/TLS (wss://)]");  
73 - }  
74 -  
75 - // 创建 HTTP 服务器  
76 - httpServer = vertx.createHttpServer(options);  
77 -  
78 - // 设置 WebSocket 处理器  
79 - httpServer.webSocketHandler(this::handleWebSocketConnection);  
80 -  
81 - // 启动服务器  
82 - try {  
83 - httpServer.listen().result();  
84 - log.info("[start][IoT 网关 MQTT WebSocket 协议启动成功,端口: {},路径: {},支持子协议: {}]",  
85 - mqttWsProperties.getPort(), mqttWsProperties.getPath(),  
86 - "mqtt, mqttv3.1, " + mqttWsProperties.getSubProtocol());  
87 - } catch (Exception e) {  
88 - log.error("[start][IoT 网关 MQTT WebSocket 协议启动失败]", e);  
89 - throw e;  
90 - }  
91 - }  
92 -  
93 - @PreDestroy  
94 - public void stop() {  
95 - if (httpServer != null) {  
96 - try {  
97 - // 关闭所有连接  
98 - connectionManager.closeAllConnections();  
99 -  
100 - // 关闭服务器  
101 - httpServer.close().result();  
102 - log.info("[stop][IoT 网关 MQTT WebSocket 协议已停止]");  
103 - } catch (Exception e) {  
104 - log.error("[stop][IoT 网关 MQTT WebSocket 协议停止失败]", e);  
105 - }  
106 - }  
107 - }  
108 -  
109 - /**  
110 - * 处理 WebSocket 连接请求  
111 - *  
112 - * @param socket WebSocket 连接  
113 - */  
114 - private void handleWebSocketConnection(ServerWebSocket socket) {  
115 - String path = socket.path();  
116 - String subProtocol = socket.subProtocol();  
117 -  
118 - log.info("[handleWebSocketConnection][收到 WebSocket 连接请求,path: {},subProtocol: {},remoteAddress: {}]",  
119 - path, subProtocol, socket.remoteAddress());  
120 -  
121 - // 验证路径  
122 - if (!mqttWsProperties.getPath().equals(path)) {  
123 - log.warn("[handleWebSocketConnection][WebSocket 路径不匹配,拒绝连接,path: {},期望: {}]",  
124 - path, mqttWsProperties.getPath());  
125 - socket.close();  
126 - return;  
127 - }  
128 -  
129 - // 验证子协议  
130 - // Vert.x 已经自动进行了子协议协商,这里只需要验证是否为 MQTT 相关协议  
131 - if (subProtocol != null && !subProtocol.startsWith("mqtt")) {  
132 - log.warn("[handleWebSocketConnection][WebSocket 子协议不支持,拒绝连接,subProtocol: {}]", subProtocol);  
133 - socket.close();  
134 - return;  
135 - }  
136 -  
137 - log.info("[handleWebSocketConnection][WebSocket 连接已接受,remoteAddress: {},subProtocol: {}]",  
138 - socket.remoteAddress(), subProtocol);  
139 -  
140 - // 创建处理器并处理连接  
141 - IotMqttWsUpstreamHandler handler = new IotMqttWsUpstreamHandler(  
142 - this, messageService, connectionManager);  
143 - handler.handle(socket);  
144 - }  
145 -  
146 -}  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/manager/IotMqttWsConnectionManager.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager;  
2 -  
3 -import cn.hutool.core.collection.CollUtil;  
4 -import io.vertx.core.http.ServerWebSocket;  
5 -import lombok.extern.slf4j.Slf4j;  
6 -import org.springframework.stereotype.Component;  
7 -  
8 -import java.util.Map;  
9 -import java.util.Set;  
10 -import java.util.concurrent.ConcurrentHashMap;  
11 -import java.util.concurrent.CopyOnWriteArraySet;  
12 -  
13 -/**  
14 - * IoT MQTT WebSocket 连接管理器  
15 - *  
16 - * @author 芋道源码  
17 - */  
18 -@Slf4j  
19 -@Component  
20 -public class IotMqttWsConnectionManager {  
21 -  
22 - /**  
23 - * 存储设备连接  
24 - * Key: 设备标识(deviceKey)  
25 - * Value: WebSocket 连接  
26 - */  
27 - private final Map<String, ServerWebSocket> connections = new ConcurrentHashMap<>();  
28 -  
29 - /**  
30 - * 存储设备标识与 Socket ID 的映射  
31 - * Key: 设备标识(deviceKey)  
32 - * Value: Socket ID(UUID)  
33 - */  
34 - private final Map<String, String> deviceKeyToSocketId = new ConcurrentHashMap<>();  
35 -  
36 - /**  
37 - * 存储 Socket ID 与设备标识的映射  
38 - * Key: Socket ID(UUID)  
39 - * Value: 设备标识(deviceKey)  
40 - */  
41 - private final Map<String, String> socketIdToDeviceKey = new ConcurrentHashMap<>();  
42 -  
43 - /**  
44 - * 存储设备订阅的主题  
45 - * Key: 设备标识(deviceKey)  
46 - * Value: 订阅的主题集合  
47 - */  
48 - private final Map<String, Set<String>> deviceSubscriptions = new ConcurrentHashMap<>();  
49 -  
50 - /**  
51 - * 添加连接  
52 - *  
53 - * @param deviceKey 设备标识  
54 - * @param socket WebSocket 连接  
55 - * @param socketId Socket ID(UUID)  
56 - */  
57 - public void addConnection(String deviceKey, ServerWebSocket socket, String socketId) {  
58 - connections.put(deviceKey, socket);  
59 - deviceKeyToSocketId.put(deviceKey, socketId);  
60 - socketIdToDeviceKey.put(socketId, deviceKey);  
61 - log.info("[addConnection][设备连接已添加,deviceKey: {},socketId: {},当前连接数: {}]",  
62 - deviceKey, socketId, connections.size());  
63 - }  
64 -  
65 - /**  
66 - * 移除连接  
67 - *  
68 - * @param deviceKey 设备标识  
69 - */  
70 - public void removeConnection(String deviceKey) {  
71 - ServerWebSocket socket = connections.remove(deviceKey);  
72 - String socketId = deviceKeyToSocketId.remove(deviceKey);  
73 - if (socketId != null) {  
74 - socketIdToDeviceKey.remove(socketId);  
75 - }  
76 - if (socket != null) {  
77 - log.info("[removeConnection][设备连接已移除,deviceKey: {},socketId: {},当前连接数: {}]",  
78 - deviceKey, socketId, connections.size());  
79 - }  
80 - }  
81 -  
82 - /**  
83 - * 根据 Socket ID 移除连接  
84 - *  
85 - * @param socketId WebSocket 文本框架 ID  
86 - */  
87 - public void removeConnectionBySocketId(String socketId) {  
88 - String deviceKey = socketIdToDeviceKey.remove(socketId);  
89 - if (deviceKey != null) {  
90 - connections.remove(deviceKey);  
91 - log.info("[removeConnectionBySocketId][设备连接已移除,socketId: {},deviceKey: {},当前连接数: {}]",  
92 - socketId, deviceKey, connections.size());  
93 - }  
94 - }  
95 -  
96 - /**  
97 - * 获取连接  
98 - *  
99 - * @param deviceKey 设备标识  
100 - * @return WebSocket 连接  
101 - */  
102 - public ServerWebSocket getConnection(String deviceKey) {  
103 - return connections.get(deviceKey);  
104 - }  
105 -  
106 - /**  
107 - * 根据 Socket ID 获取设备标识  
108 - *  
109 - * @param socketId WebSocket 文本框架 ID  
110 - * @return 设备标识  
111 - */  
112 - public String getDeviceKeyBySocketId(String socketId) {  
113 - return socketIdToDeviceKey.get(socketId);  
114 - }  
115 -  
116 - /**  
117 - * 检查设备是否在线  
118 - *  
119 - * @param deviceKey 设备标识  
120 - * @return 是否在线  
121 - */  
122 - public boolean isOnline(String deviceKey) {  
123 - return connections.containsKey(deviceKey);  
124 - }  
125 -  
126 - /**  
127 - * 获取当前连接数  
128 - *  
129 - * @return 连接数  
130 - */  
131 - public int getConnectionCount() {  
132 - return connections.size();  
133 - }  
134 -  
135 - /**  
136 - * 关闭所有连接  
137 - */  
138 - public void closeAllConnections() {  
139 - connections.forEach((deviceKey, socket) -> {  
140 - try {  
141 - socket.close();  
142 - log.info("[closeAllConnections][关闭设备连接,deviceKey: {}]", deviceKey);  
143 - } catch (Exception e) {  
144 - log.error("[closeAllConnections][关闭设备连接失败,deviceKey: {}]", deviceKey, e);  
145 - }  
146 - });  
147 - connections.clear();  
148 - deviceKeyToSocketId.clear();  
149 - socketIdToDeviceKey.clear();  
150 - deviceSubscriptions.clear();  
151 - log.info("[closeAllConnections][所有连接已关闭]");  
152 - }  
153 -  
154 - // ==================== 订阅管理方法 ====================  
155 -  
156 - /**  
157 - * 添加订阅  
158 - *  
159 - * @param deviceKey 设备标识  
160 - * @param topic 订阅主题  
161 - */  
162 - public void addSubscription(String deviceKey, String topic) {  
163 - deviceSubscriptions.computeIfAbsent(deviceKey, k -> new CopyOnWriteArraySet<>()).add(topic);  
164 - log.debug("[addSubscription][设备订阅主题,deviceKey: {},topic: {}]", deviceKey, topic);  
165 - }  
166 -  
167 - /**  
168 - * 移除订阅  
169 - *  
170 - * @param deviceKey 设备标识  
171 - * @param topic 订阅主题  
172 - */  
173 - public void removeSubscription(String deviceKey, String topic) {  
174 - Set<String> topics = deviceSubscriptions.get(deviceKey);  
175 - if (topics != null) {  
176 - topics.remove(topic);  
177 - log.debug("[removeSubscription][设备取消订阅,deviceKey: {},topic: {}]", deviceKey, topic);  
178 - }  
179 - }  
180 -  
181 - /**  
182 - * 检查设备是否订阅了指定主题  
183 - * 支持 MQTT 通配符匹配(+ 和 #)  
184 - *  
185 - * @param deviceKey 设备标识  
186 - * @param topic 发布主题  
187 - * @return 是否匹配  
188 - */  
189 - public boolean isSubscribed(String deviceKey, String topic) {  
190 - Set<String> subscriptions = deviceSubscriptions.get(deviceKey);  
191 - if (CollUtil.isEmpty(subscriptions)) {  
192 - return false;  
193 - }  
194 -  
195 - // 检查是否有匹配的订阅  
196 - for (String subscription : subscriptions) {  
197 - if (topicMatches(subscription, topic)) {  
198 - return true;  
199 - }  
200 - }  
201 - return false;  
202 - }  
203 -  
204 - /**  
205 - * 获取设备的所有订阅  
206 - *  
207 - * @param deviceKey 设备标识  
208 - * @return 订阅主题集合  
209 - */  
210 - public Set<String> getSubscriptions(String deviceKey) {  
211 - return deviceSubscriptions.get(deviceKey);  
212 - }  
213 -  
214 - // TODO @haohao:这个方法,是不是也可以考虑抽到 IotMqttTopicUtils 里面去哈;感觉更简洁一点?  
215 - /**  
216 - * MQTT 主题匹配  
217 - * 支持通配符:  
218 - * - +:匹配单层主题  
219 - * - #:匹配多层主题(必须在末尾)  
220 - *  
221 - * @param subscription 订阅主题(可能包含通配符)  
222 - * @param topic 发布主题(不包含通配符)  
223 - * @return 是否匹配  
224 - */  
225 - private boolean topicMatches(String subscription, String topic) {  
226 - // 完全匹配  
227 - if (subscription.equals(topic)) {  
228 - return true;  
229 - }  
230 -  
231 - // 不包含通配符  
232 - // TODO @haohao:这里要不要枚举下哈;+ #  
233 - if (!subscription.contains("+") && !subscription.contains("#")) {  
234 - return false;  
235 - }  
236 -  
237 - String[] subscriptionParts = subscription.split("/");  
238 - String[] topicParts = topic.split("/");  
239 - int i = 0;  
240 - for (; i < subscriptionParts.length && i < topicParts.length; i++) {  
241 - String subPart = subscriptionParts[i];  
242 - String topicPart = topicParts[i];  
243 -  
244 - // # 匹配剩余所有层级,且必须在末尾  
245 - if (subPart.equals("#")) {  
246 - return i == subscriptionParts.length - 1;  
247 - }  
248 -  
249 - // 不是通配符且不匹配  
250 - if (!subPart.equals("+") && !subPart.equals(topicPart)) {  
251 - return false;  
252 - }  
253 - }  
254 -  
255 - // 检查是否都匹配完  
256 - return i == subscriptionParts.length && i == topicParts.length;  
257 - }  
258 -  
259 -}  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/package-info.java deleted
1 -/**  
2 - * IoT 网关 MQTT WebSocket 协议实现  
3 - * <p>  
4 - * 基于 Vert.x 实现 MQTT over WebSocket 服务端,支持:  
5 - * - 标准 MQTT 3.1.1 协议  
6 - * - WebSocket 协议升级  
7 - * - SSL/TLS 加密(wss://)  
8 - * - 设备认证与连接管理  
9 - * - QoS 0/1/2 消息质量保证  
10 - * - 双向消息通信(上行/下行)  
11 - *  
12 - * @author 芋道源码  
13 - */  
14 -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws;  
15 -  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqttws/router/IotMqttWsDownstreamHandler.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router;  
2 -  
3 -import cn.hutool.core.util.StrUtil;  
4 -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;  
5 -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;  
6 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;  
7 -import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;  
8 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;  
9 -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;  
10 -import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;  
11 -import io.vertx.core.buffer.Buffer;  
12 -import io.vertx.core.http.ServerWebSocket;  
13 -import lombok.extern.slf4j.Slf4j;  
14 -  
15 -import java.util.concurrent.atomic.AtomicInteger;  
16 -  
17 -/**  
18 - * IoT MQTT WebSocket 下行消息处理器  
19 - * <p>  
20 - * 处理从消息总线发送到设备的消息,包括:  
21 - * - 属性设置  
22 - * - 服务调用  
23 - * - 事件通知  
24 - *  
25 - * @author 芋道源码  
26 - */  
27 -@Slf4j  
28 -public class IotMqttWsDownstreamHandler {  
29 -  
30 - private final IotDeviceMessageService deviceMessageService;  
31 -  
32 - private final IotDeviceService deviceService;  
33 -  
34 - private final IotMqttWsConnectionManager connectionManager;  
35 -  
36 - /**  
37 - * 消息 ID 生成器(用于发布消息)  
38 - */  
39 - private final AtomicInteger messageIdGenerator = new AtomicInteger(1);  
40 -  
41 - public IotMqttWsDownstreamHandler(IotDeviceMessageService deviceMessageService,  
42 - IotDeviceService deviceService,  
43 - IotMqttWsConnectionManager connectionManager) {  
44 - this.deviceMessageService = deviceMessageService;  
45 - this.deviceService = deviceService;  
46 - this.connectionManager = connectionManager;  
47 - }  
48 -  
49 - /**  
50 - * 处理下行消息  
51 - *  
52 - * @param message 设备消息  
53 - * @return 是否处理成功  
54 - */  
55 - public boolean handleDownstreamMessage(IotDeviceMessage message) {  
56 - try {  
57 - // 1. 基础校验  
58 - if (message == null || message.getDeviceId() == null) {  
59 - log.warn("[handleDownstreamMessage][消息或设备 ID 为空,忽略处理]");  
60 - return false;  
61 - }  
62 -  
63 - // 2. 获取设备信息  
64 - IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId());  
65 - if (deviceInfo == null) {  
66 - log.warn("[handleDownstreamMessage][设备不存在,设备 ID:{}]", message.getDeviceId());  
67 - return false;  
68 - }  
69 -  
70 - // 3. 构建设备标识  
71 - String deviceKey = deviceInfo.getProductKey() + ":" + deviceInfo.getDeviceName();  
72 -  
73 - // 4. 检查设备是否在线  
74 - if (!connectionManager.isOnline(deviceKey)) {  
75 - log.warn("[handleDownstreamMessage][设备离线,无法发送消息,deviceKey: {}]", deviceKey);  
76 - return false;  
77 - }  
78 -  
79 - // 5. 构建主题  
80 - String topic = buildDownstreamTopic(message, deviceInfo);  
81 - if (StrUtil.isBlank(topic)) {  
82 - log.warn("[handleDownstreamMessage][主题构建失败,设备 ID:{},方法:{}]",  
83 - message.getDeviceId(), message.getMethod());  
84 - return false;  
85 - }  
86 -  
87 - // 6. 检查设备是否订阅了该主题  
88 - if (!connectionManager.isSubscribed(deviceKey, topic)) {  
89 - log.warn("[handleDownstreamMessage][设备未订阅该主题,deviceKey: {},topic: {}]", deviceKey, topic);  
90 - return false;  
91 - }  
92 -  
93 - // 8. 编码消息  
94 - byte[] payload = deviceMessageService.encodeDeviceMessage(message,  
95 - deviceInfo.getProductKey(), deviceInfo.getDeviceName());  
96 - if (payload == null || payload.length == 0) {  
97 - log.warn("[handleDownstreamMessage][消息编码失败,设备 ID:{}]", message.getDeviceId());  
98 - return false;  
99 - }  
100 -  
101 - // 9. 发送消息到设备  
102 - return sendMessageToDevice(deviceKey, topic, payload, 1);  
103 - } catch (Exception e) {  
104 - if (message != null) {  
105 - log.error("[handleDownstreamMessage][处理下行消息异常,设备 ID:{},错误:{}]",  
106 - message.getDeviceId(), e.getMessage(), e);  
107 - }  
108 - return false;  
109 - }  
110 - }  
111 -  
112 - /**  
113 - * 构建下行消息主题  
114 - *  
115 - * @param message 设备消息  
116 - * @param deviceInfo 设备信息  
117 - * @return 主题  
118 - */  
119 - private String buildDownstreamTopic(IotDeviceMessage message, IotDeviceRespDTO deviceInfo) {  
120 - String method = message.getMethod();  
121 - if (StrUtil.isBlank(method)) {  
122 - return null;  
123 - }  
124 -  
125 - // 使用工具类构建主题,支持回复消息处理  
126 - boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);  
127 - return IotMqttTopicUtils.buildTopicByMethod(method, deviceInfo.getProductKey(),  
128 - deviceInfo.getDeviceName(), isReply);  
129 - }  
130 -  
131 - /**  
132 - * 发送消息到设备  
133 - *  
134 - * @param deviceKey 设备标识(productKey:deviceName)  
135 - * @param topic 主题  
136 - * @param payload 消息内容  
137 - * @param qos QoS 级别  
138 - * @return 是否发送成功  
139 - */  
140 - private boolean sendMessageToDevice(String deviceKey, String topic, byte[] payload, int qos) {  
141 - // 获取设备连接  
142 - ServerWebSocket socket = connectionManager.getConnection(deviceKey);  
143 - if (socket == null) {  
144 - log.warn("[sendMessageToDevice][设备未连接,deviceKey: {}]", deviceKey);  
145 - return false;  
146 - }  
147 -  
148 - try {  
149 - int messageId = qos > 0 ? generateMessageId() : 0;  
150 -  
151 - // 手动编码 MQTT PUBLISH 消息  
152 - io.netty.buffer.ByteBuf byteBuf = io.netty.buffer.Unpooled.buffer();  
153 -  
154 - // 固定头:消息类型(PUBLISH=3) + DUP(0) + QoS + RETAIN  
155 - int fixedHeaderByte1 = 0x30 | (qos << 1); // PUBLISH类型  
156 - byteBuf.writeByte(fixedHeaderByte1);  
157 -  
158 - // 计算剩余长度  
159 - int topicLength = topic.getBytes().length;  
160 - int remainingLength = 2 + topicLength + (qos > 0 ? 2 : 0) + payload.length;  
161 -  
162 - // 写入剩余长度(简化版本,假设小于 128 字节)  
163 - if (remainingLength < 128) {  
164 - byteBuf.writeByte(remainingLength);  
165 - } else {  
166 - // 处理大于 127 的情况  
167 - int x = remainingLength;  
168 - do {  
169 - int encodedByte = x % 128;  
170 - x = x / 128;  
171 - if (x > 0) {  
172 - encodedByte = encodedByte | 128;  
173 - }  
174 - byteBuf.writeByte(encodedByte);  
175 - } while (x > 0);  
176 - }  
177 -  
178 - // 可变头:主题名称  
179 - byteBuf.writeShort(topicLength);  
180 - byteBuf.writeBytes(topic.getBytes());  
181 -  
182 - // 可变头:消息 ID(仅 QoS > 0 时)  
183 - if (qos > 0) {  
184 - byteBuf.writeShort(messageId);  
185 - }  
186 -  
187 - // 有效载荷  
188 - byteBuf.writeBytes(payload);  
189 -  
190 - // 发送  
191 - byte[] bytes = new byte[byteBuf.readableBytes()];  
192 - byteBuf.readBytes(bytes);  
193 - byteBuf.release();  
194 - socket.writeBinaryMessage(Buffer.buffer(bytes));  
195 -  
196 - log.info("[sendMessageToDevice][消息已发送到设备,deviceKey: {},topic: {},qos: {},messageId: {}]",  
197 - deviceKey, topic, qos, messageId);  
198 - return true;  
199 - } catch (Exception e) {  
200 - log.error("[sendMessageToDevice][发送消息到设备失败,deviceKey: {},topic: {}]", deviceKey, topic, e);  
201 - return false;  
202 - }  
203 - }  
204 -  
205 - /**  
206 - * 生成消息 ID  
207 - *  
208 - * @return 消息 ID  
209 - */  
210 - private int generateMessageId() {  
211 - int id = messageIdGenerator.getAndIncrement();  
212 - // MQTT 消息 ID 范围是 1-65535  
213 - // TODO @haohao:并发可能有问题;  
214 - if (id > 65535) {  
215 - messageIdGenerator.set(1);  
216 - return 1;  
217 - }  
218 - return id;  
219 - }  
220 -  
221 -}