Commit a2750693ebbb6de7506b9d5e0de39a7c2d3d1bac

Authored by YunaiV
1 parent e765099a

feat(iot):【网关设备:50%】整体初步实现(未测试),基于 gateway-device-topic-design.md 规划

Showing 13 changed files with 903 additions and 10 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
... ... @@ -35,6 +35,16 @@ public interface ErrorCodeConstants {
35 35 ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
36 36 ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
37 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 + // TODO @AI:这里的错误码校验,要不要使用?
  44 + ErrorCode DEVICE_TOPO_SUB_DEVICE_NOT_BOUND = new ErrorCode(1_050_003_104, "子设备【{}/{}】未绑定到任何网关");
  45 + // 子设备注册相关错误码 1-050-003-200
  46 + ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
  47 + ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
38 48  
39 49 // ========== 产品分类 1-050-004-000 ==========
40 50 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/service/device/IotDeviceServiceImpl.java
... ... @@ -468,6 +468,13 @@ public class IotDeviceServiceImpl implements IotDeviceService {
468 468 log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName);
469 469 return false;
470 470 }
  471 +
  472 + // 3. 校验子设备拓扑关系:子设备必须先绑定到某网关才能认证上线
  473 + if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())
  474 + && device.getGatewayId() != null) {
  475 + log.warn("[authDevice][子设备({}/{}) 未绑定到任何网关,认证失败]", productKey, deviceName);
  476 + return false;
  477 + }
471 478 return true;
472 479 }
473 480  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
... ... @@ -10,17 +10,30 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 10 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
11 11 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  13 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
13 14 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
14 15 import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO;
15 16 import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO;
  17 +import cn.iocoder.yudao.module.iot.core.biz.dto.*;
16 18 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  19 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
17 20 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  21 +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO;
  22 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
  23 +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
  24 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO;
  25 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO;
  26 +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoRespDTO;
  27 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils;
18 28 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
19 29 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
20 30 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
21 31 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  32 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
22 33 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
  34 +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
23 35 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  36 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
24 37 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
25 38 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
26 39 import com.baomidou.mybatisplus.core.metadata.IPage;
... ... @@ -35,12 +48,11 @@ import org.springframework.validation.annotation.Validated;
35 48  
36 49 import java.sql.Timestamp;
37 50 import java.time.LocalDateTime;
38   -import java.util.List;
39   -import java.util.Map;
  51 +import java.util.*;
40 52  
41 53 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
42 54 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
43   -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL;
  55 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
44 56  
45 57 /**
46 58 * IoT 设备消息 Service 实现类
... ... @@ -59,6 +71,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
59 71 @Resource
60 72 @Lazy // 延迟加载,避免循环依赖
61 73 private IotOtaTaskRecordService otaTaskRecordService;
  74 + @Resource
  75 + @Lazy // 延迟加载,避免循环依赖
  76 + private IotProductService productService;
62 77  
63 78 @Resource
64 79 private IotDeviceMessageMapper deviceMessageMapper;
... ... @@ -168,9 +183,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
168 183 // 2. 记录消息
169 184 getSelf().createDeviceLogAsync(message);
170 185  
171   - // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息
  186 + // TODO @AI:我在想,是不是批量上传后,还是得 reply 。因为打包上传的时候,只是那条消息的回复。然后,需要单独给每个子消息回复,后续至于怎么使用,是不是得看具体业务了;例如说:1)批量上传属性,默认回复是批量上传的消息;然后,每个属性、事件拆包消息,单独回复,后续网关设备按需回复给子设备。
  187 + // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息、非拆包消息
172 188 if (IotDeviceMessageUtils.isReplyMessage(message)
173   - || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
  189 + || !message.needReply()
174 190 || StrUtil.isEmpty(message.getServerId())) {
175 191 return;
176 192 }
... ... @@ -185,15 +201,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
185 201 }
186 202  
187 203 // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器
188   - @SuppressWarnings("SameReturnValue")
189 204 private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) {
190 205 // 设备上下线
191 206 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) {
192 207 String stateStr = IotDeviceMessageUtils.getIdentifier(message);
193 208 assert stateStr != null;
194 209 Assert.notEmpty(stateStr, "设备状态不能为空");
195   - deviceService.updateDeviceState(device, Integer.valueOf(stateStr));
196   - // TODO 芋艿:子设备的关联
  210 + Integer state = Integer.valueOf(stateStr);
  211 + deviceService.updateDeviceState(device, state);
  212 + // 特殊:网关设备下线时,网关子设备联动下线
  213 + if (Objects.equal(state, IotDeviceStateEnum.OFFLINE.getState())
  214 + && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) {
  215 + handleGatewayOffline(device, message.getServerId());
  216 + }
197 217 return null;
198 218 }
199 219  
... ... @@ -202,6 +222,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
202 222 devicePropertyService.saveDeviceProperty(device, message);
203 223 return null;
204 224 }
  225 + // 批量上报(属性+事件+子设备)
  226 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())) {
  227 + handlePackMessage(message, device);
  228 + return null;
  229 + }
205 230  
206 231 // OTA 上报升级进度
207 232 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) {
... ... @@ -209,10 +234,435 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
209 234 return null;
210 235 }
211 236  
212   - // TODO @芋艿:这里可以按需,添加别的逻辑;
  237 + // 添加拓扑关系
  238 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())) {
  239 + return handleTopoAdd(message, device);
  240 + }
  241 + // 删除拓扑关系
  242 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) {
  243 + return handleTopoDelete(message, device);
  244 + }
  245 +
  246 + // 获取拓扑关系
  247 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) {
  248 + return handleTopoGet(device);
  249 + }
  250 +
  251 + // 子设备动态注册
  252 + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())) {
  253 + return handleSubDeviceRegister(message, device);
  254 + }
  255 +
  256 + return null;
  257 + }
  258 +
  259 + // ========== 拓扑管理处理方法 ==========
  260 +
  261 + // TODO @AI:是不是更适合在 deviceService 里面处理?
  262 + /**
  263 + * 处理添加拓扑关系请求
  264 + *
  265 + * @param message 消息
  266 + * @param gatewayDevice 网关设备
  267 + * @return 响应数据
  268 + */
  269 + private Object handleTopoAdd(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  270 + // TODO @AI:这里是不是 1.1 1.2 1.3 这样?修改前,我们确认下,有没模块是这么写的;iot 消息处理里;
  271 + // 1. 校验网关设备类型
  272 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  273 + throw exception(DEVICE_NOT_GATEWAY);
  274 + }
  275 +
  276 + // 2. 解析参数
  277 + // TODO @AI:是不是 parseObject 增加一个方法,允许传入 object 类型,避免先转 jsonString 再 parseObject ;
  278 + IotDeviceTopoAddReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()),
  279 + IotDeviceTopoAddReqDTO.class);
  280 + if (params == null) {
  281 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  282 + }
  283 +
  284 + // 3. 解析子设备信息
  285 + IotDeviceAuthUtils.DeviceInfo subDeviceInfo = IotDeviceAuthUtils.parseUsername(params.getUsername());
  286 + if (subDeviceInfo == null) {
  287 + throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
  288 + }
  289 +
  290 + // 4. 校验子设备认证信息
  291 + // TODO @AI:链式调用;
  292 + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO();
  293 + authReqDTO.setClientId(params.getClientId());
  294 + authReqDTO.setUsername(params.getUsername());
  295 + authReqDTO.setPassword(params.getPassword());
  296 + if (!deviceService.authDevice(authReqDTO)) {
  297 + throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED);
  298 + }
  299 +
  300 + // 5. 获取子设备
  301 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
  302 + if (subDevice == null) {
  303 + throw exception(DEVICE_NOT_EXISTS);
  304 + }
  305 +
  306 + // 6. 校验子设备类型
  307 + if (!IotProductDeviceTypeEnum.isGatewaySub(subDevice.getDeviceType())) {
  308 + throw exception(DEVICE_NOT_GATEWAY_SUB, subDevice.getProductKey(), subDevice.getDeviceName());
  309 + }
  310 +
  311 + // 7. 绑定拓扑关系
  312 + // TODO @AI:这里要考虑,校验是不是老设备已经绑定到其他网关了?
  313 + deviceService.bindDeviceGateway(Collections.singletonList(subDevice.getId()), gatewayDevice.getId());
  314 + log.info("[handleTopoAdd][网关({}/{}) 绑定子设备({}/{})]",
  315 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  316 + subDevice.getProductKey(), subDevice.getDeviceName());
  317 +
  318 + // 8. 发送拓扑变更通知
  319 + sendTopoChangeNotify(gatewayDevice, "add", subDevice);
213 320 return null;
214 321 }
215 322  
  323 + // TODO @AI:是不是更适合在 deviceService 里面处理?
  324 +
  325 + /**
  326 + * 处理删除拓扑关系请求
  327 + *
  328 + * @param message 消息
  329 + * @param gatewayDevice 网关设备
  330 + * @return 响应数据
  331 + */
  332 + private Object handleTopoDelete(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  333 + // 1. 校验网关设备类型
  334 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  335 + throw exception(DEVICE_NOT_GATEWAY);
  336 + }
  337 +
  338 + // 2. 解析参数
  339 + IotDeviceTopoDeleteReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()),
  340 + IotDeviceTopoDeleteReqDTO.class);
  341 + if (params == null) {
  342 + throw exception(DEVICE_TOPO_PARAMS_INVALID);
  343 + }
  344 +
  345 + // 3. 获取子设备
  346 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(params.getProductKey(), params.getDeviceName());
  347 + if (subDevice == null) {
  348 + throw exception(DEVICE_NOT_EXISTS);
  349 + }
  350 +
  351 + // 4. 校验子设备是否绑定到该网关
  352 + if (!Objects.equal(subDevice.getGatewayId(), gatewayDevice.getId())) {
  353 + throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY, params.getProductKey(), params.getDeviceName());
  354 + }
  355 +
  356 + // 5. 解绑拓扑关系
  357 + deviceService.unbindDeviceGateway(Collections.singletonList(subDevice.getId()));
  358 + log.info("[handleTopoDelete][网关({}/{}) 解绑子设备({}/{})]",
  359 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  360 + subDevice.getProductKey(), subDevice.getDeviceName());
  361 +
  362 + // 6. 子设备下线
  363 + if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  364 + deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  365 + }
  366 +
  367 + // 7. 发送拓扑变更通知
  368 + sendTopoChangeNotify(gatewayDevice, "delete", subDevice);
  369 +
  370 + return null;
  371 + }
  372 +
  373 + /**
  374 + * 处理获取拓扑关系请求
  375 + *
  376 + * @param gatewayDevice 网关设备
  377 + * @return 子设备列表
  378 + */
  379 + private Object handleTopoGet(IotDeviceDO gatewayDevice) {
  380 + // 1. 校验网关设备类型
  381 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  382 + throw exception(DEVICE_NOT_GATEWAY);
  383 + }
  384 +
  385 + // 2. 获取子设备列表
  386 + List<IotDeviceDO> subDevices = deviceService.getDeviceListByGatewayId(gatewayDevice.getId());
  387 +
  388 + // 3. 转换为响应格式
  389 + return convertList(subDevices, subDevice -> new IotDeviceTopoRespDTO()
  390 + .setProductKey(subDevice.getProductKey())
  391 + .setDeviceName(subDevice.getDeviceName()));
  392 + }
  393 +
  394 + /**
  395 + * 发送拓扑变更通知
  396 + *
  397 + * @param gatewayDevice 网关设备
  398 + * @param changeType 变更类型:add/delete
  399 + * @param subDevice 子设备
  400 + */
  401 + private void sendTopoChangeNotify(IotDeviceDO gatewayDevice, String changeType, IotDeviceDO subDevice) {
  402 + try {
  403 + String serverId = devicePropertyService.getDeviceServerId(gatewayDevice.getId());
  404 + if (StrUtil.isEmpty(serverId)) {
  405 + log.warn("[sendTopoChangeNotify][网关({}/{}) serverId 为空,无法发送拓扑变更通知]",
  406 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName());
  407 + return;
  408 + }
  409 +
  410 + Map<String, Object> params = MapUtil.builder(new HashMap<String, Object>())
  411 + .put("changeType", changeType)
  412 + .put("subDevice", MapUtil.builder(new HashMap<String, Object>())
  413 + .put("productKey", subDevice.getProductKey())
  414 + .put("deviceName", subDevice.getDeviceName())
  415 + .build())
  416 + .build();
  417 +
  418 + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf(
  419 + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params);
  420 + sendDeviceMessage(notifyMessage, gatewayDevice, serverId);
  421 + } catch (Exception ex) {
  422 + log.error("[sendTopoChangeNotify][发送拓扑变更通知失败,网关({}/{}), 子设备({}/{})]",
  423 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  424 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  425 + }
  426 + }
  427 +
  428 + // ========== 子设备注册处理方法 ==========
  429 +
  430 + /**
  431 + * 处理子设备动态注册请求
  432 + *
  433 + * @param message 消息
  434 + * @param gatewayDevice 网关设备
  435 + * @return 注册结果列表
  436 + */
  437 + private Object handleSubDeviceRegister(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
  438 + // 1. 校验网关设备类型
  439 + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
  440 + throw exception(DEVICE_NOT_GATEWAY);
  441 + }
  442 +
  443 + // 2. 解析参数(数组)
  444 + List<IotSubDeviceRegisterReqDTO> paramsList;
  445 + if (message.getParams() instanceof List) {
  446 + paramsList = JsonUtils.parseArray(JsonUtils.toJsonString(message.getParams()),
  447 + IotSubDeviceRegisterReqDTO.class);
  448 + } else {
  449 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  450 + }
  451 +
  452 + if (paramsList == null || paramsList.isEmpty()) {
  453 + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID);
  454 + }
  455 +
  456 + // 3. 遍历注册每个子设备
  457 + List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>();
  458 + for (IotSubDeviceRegisterReqDTO params : paramsList) {
  459 + try {
  460 + IotSubDeviceRegisterRespDTO result = registerSubDevice(params, gatewayDevice);
  461 + results.add(result);
  462 + } catch (Exception ex) {
  463 + log.error("[handleSubDeviceRegister][子设备({}/{}) 注册失败]",
  464 + params.getProductKey(), params.getDeviceName(), ex);
  465 + // 继续处理其他子设备,不影响整体流程
  466 + }
  467 + }
  468 +
  469 + return results;
  470 + }
  471 +
  472 + /**
  473 + * 注册单个子设备
  474 + *
  475 + * @param params 注册参数
  476 + * @param gatewayDevice 网关设备
  477 + * @return 注册结果
  478 + */
  479 + private IotSubDeviceRegisterRespDTO registerSubDevice(IotSubDeviceRegisterReqDTO params,
  480 + IotDeviceDO gatewayDevice) {
  481 + // 1. 查找产品
  482 + IotProductDO product = productService.getProductByProductKey(params.getProductKey());
  483 + if (product == null) {
  484 + throw exception(PRODUCT_NOT_EXISTS);
  485 + }
  486 +
  487 + // 2. 校验产品是否为网关子设备类型
  488 + if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) {
  489 + throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey());
  490 + }
  491 +
  492 + // 3. 查找设备是否已存在
  493 + IotDeviceDO existDevice = deviceService.getDeviceFromCache(params.getProductKey(), params.getDeviceName());
  494 + if (existDevice != null) {
  495 + // 已存在则返回设备信息
  496 + return new IotSubDeviceRegisterRespDTO()
  497 + .setProductKey(existDevice.getProductKey())
  498 + .setDeviceName(existDevice.getDeviceName())
  499 + .setDeviceSecret(existDevice.getDeviceSecret());
  500 + }
  501 +
  502 + // 4. 创建新设备
  503 + IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO()
  504 + .setDeviceName(params.getDeviceName())
  505 + .setProductId(product.getId())
  506 + .setGatewayId(gatewayDevice.getId());
  507 + Long deviceId = deviceService.createDevice(createReqVO);
  508 +
  509 + // 5. 获取新创建的设备信息
  510 + IotDeviceDO newDevice = deviceService.getDevice(deviceId);
  511 + log.info("[registerSubDevice][网关({}/{}) 注册子设备({}/{})]",
  512 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  513 + newDevice.getProductKey(), newDevice.getDeviceName());
  514 +
  515 + return new IotSubDeviceRegisterRespDTO()
  516 + .setProductKey(newDevice.getProductKey())
  517 + .setDeviceName(newDevice.getDeviceName())
  518 + .setDeviceSecret(newDevice.getDeviceSecret());
  519 + }
  520 +
  521 + // ========== 批量上报处理方法 ==========
  522 +
  523 + /**
  524 + * 处理批量上报消息
  525 + * <p>
  526 + * 将 pack 消息拆分成多条标准消息,递归处理
  527 + *
  528 + * @param packMessage 批量消息
  529 + * @param gatewayDevice 网关设备
  530 + */
  531 + private void handlePackMessage(IotDeviceMessage packMessage, IotDeviceDO gatewayDevice) {
  532 + // 1. 解析参数
  533 + IotDevicePropertyPackPostReqDTO params = JsonUtils.parseObject(
  534 + JsonUtils.toJsonString(packMessage.getParams()),
  535 + IotDevicePropertyPackPostReqDTO.class);
  536 + if (params == null) {
  537 + log.warn("[handlePackMessage][消息({}) 参数解析失败]", packMessage.getId());
  538 + return;
  539 + }
  540 +
  541 + // 2. 处理网关自身属性
  542 + if (params.getProperties() != null && !params.getProperties().isEmpty()) {
  543 + Map<String, Object> gatewayProperties = convertPackProperties(params.getProperties());
  544 + IotDeviceMessage gatewayMsg = IotDeviceMessage.builder()
  545 + .id(IotDeviceMessageUtils.generateMessageId())
  546 + .parentMessageId(packMessage.getId())
  547 + .deviceId(gatewayDevice.getId())
  548 + .tenantId(gatewayDevice.getTenantId())
  549 + .serverId(packMessage.getServerId())
  550 + .method(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  551 + .params(gatewayProperties)
  552 + .reportTime(LocalDateTime.now())
  553 + .build();
  554 + // 直接调用处理,不通过消息总线
  555 + try {
  556 + devicePropertyService.saveDeviceProperty(gatewayDevice, gatewayMsg);
  557 + getSelf().createDeviceLogAsync(gatewayMsg);
  558 + } catch (Exception ex) {
  559 + log.error("[handlePackMessage][网关({}) 属性处理失败]", gatewayDevice.getId(), ex);
  560 + }
  561 + }
  562 +
  563 + // 3. 处理子设备数据
  564 + if (params.getSubDevices() != null) {
  565 + for (IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData : params.getSubDevices()) {
  566 + try {
  567 + handleSubDevicePackData(packMessage, subDeviceData);
  568 + } catch (Exception ex) {
  569 + log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]",
  570 + subDeviceData.getIdentity().getProductKey(),
  571 + subDeviceData.getIdentity().getDeviceName(), ex);
  572 + }
  573 + }
  574 + }
  575 + }
  576 +
  577 + /**
  578 + * 处理子设备的 pack 数据
  579 + *
  580 + * @param packMessage 原始 pack 消息
  581 + * @param subDeviceData 子设备数据
  582 + */
  583 + private void handleSubDevicePackData(IotDeviceMessage packMessage,
  584 + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData) {
  585 + // 1. 获取子设备
  586 + IotDevicePropertyPackPostReqDTO.DeviceIdentity identity = subDeviceData.getIdentity();
  587 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(identity.getProductKey(), identity.getDeviceName());
  588 + if (subDevice == null) {
  589 + log.warn("[handleSubDevicePackData][子设备({}/{}) 不存在]",
  590 + identity.getProductKey(), identity.getDeviceName());
  591 + return;
  592 + }
  593 +
  594 + // 2. 处理子设备属性
  595 + if (subDeviceData.getProperties() != null && !subDeviceData.getProperties().isEmpty()) {
  596 + Map<String, Object> properties = convertPackProperties(subDeviceData.getProperties());
  597 + IotDeviceMessage subMsg = IotDeviceMessage.builder()
  598 + .id(IotDeviceMessageUtils.generateMessageId())
  599 + .parentMessageId(packMessage.getId())
  600 + .deviceId(subDevice.getId())
  601 + .tenantId(subDevice.getTenantId())
  602 + .serverId(packMessage.getServerId())
  603 + .method(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod())
  604 + .params(properties)
  605 + .reportTime(LocalDateTime.now())
  606 + .build();
  607 + devicePropertyService.saveDeviceProperty(subDevice, subMsg);
  608 + getSelf().createDeviceLogAsync(subMsg);
  609 + }
  610 +
  611 + // 3. 处理子设备事件(如果需要)
  612 + // TODO: 事件处理可以后续扩展
  613 + }
  614 +
  615 + /**
  616 + * 转换 pack 属性格式为标准属性格式
  617 + * <p>
  618 + * pack 格式:{"temperature": {"value": 25.5, "time": 1524448722000}}
  619 + * 标准格式:{"temperature": 25.5}
  620 + *
  621 + * @param packProperties pack 属性
  622 + * @return 标准属性
  623 + */
  624 + private Map<String, Object> convertPackProperties(Map<String, IotDevicePropertyPackPostReqDTO.PropertyValue> packProperties) {
  625 + Map<String, Object> result = new HashMap<>();
  626 + for (Map.Entry<String, IotDevicePropertyPackPostReqDTO.PropertyValue> entry : packProperties.entrySet()) {
  627 + if (entry.getValue() != null) {
  628 + result.put(entry.getKey(), entry.getValue().getValue());
  629 + }
  630 + }
  631 + return result;
  632 + }
  633 +
  634 + // ========== 网关下线联动处理 ==========
  635 +
  636 + // TODO @AI:是不是写到 deviceService 里更合适?更解耦。
  637 + /**
  638 + * 处理网关下线,联动所有子设备下线
  639 + *
  640 + * @param gatewayDevice 网关设备
  641 + * @param serverId 服务标识
  642 + */
  643 + private void handleGatewayOffline(IotDeviceDO gatewayDevice, String serverId) {
  644 + // 1. 获取网关下所有子设备
  645 + List<IotDeviceDO> subDevices = deviceService.getDeviceListByGatewayId(gatewayDevice.getId());
  646 + if (subDevices == null || subDevices.isEmpty()) {
  647 + return;
  648 + }
  649 +
  650 + // 2. 将在线的子设备设置为下线
  651 + for (IotDeviceDO subDevice : subDevices) {
  652 + if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
  653 + try {
  654 + deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
  655 + log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]",
  656 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  657 + subDevice.getProductKey(), subDevice.getDeviceName());
  658 + } catch (Exception ex) {
  659 + log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]",
  660 + subDevice.getProductKey(), subDevice.getDeviceName(), ex);
  661 + }
  662 + }
  663 + }
  664 + }
  665 +
216 666 @Override
217 667 public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) {
218 668 try {
... ...
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 24  
25 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/register-devices
  37 +
  38 + SUB_DEVICE_REGISTER("thing.sub.register", "子设备动态注册", true),
  39 +
27 40 // ========== 设备属性 ==========
28 41 // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
29 42  
30 43 PROPERTY_POST("thing.property.post", "属性上报", true),
31 44 PROPERTY_SET("thing.property.set", "属性设置", false),
32 45  
  46 + // TODO @AI:改成 thing.property.pack.post
  47 + PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
  48 +
33 49 // ========== 设备事件 ==========
34 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 66  
51 67 OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
52 68 OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
  69 +
53 70 ;
54 71  
55 72 public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
... ... @@ -60,7 +77,8 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
60 77 */
61 78 public static final Set<String> REPLY_DISABLED = SetUtils.asSet(
62 79 STATE_UPDATE.getMethod(),
63   - OTA_PROGRESS.getMethod() // 参考阿里云,OTA 升级进度上报,不进行回复
  80 + OTA_PROGRESS.getMethod(), // 参考阿里云,OTA 升级进度上报,不进行回复
  81 + TOPO_CHANGE.getMethod() // 拓扑变更通知,下行消息,不需要回复 TODO @AI:看看阿里云的文档,确认下是不是这样的
64 82 );
65 83  
66 84 private final String method;
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
... ... @@ -94,6 +94,42 @@ public class IotDeviceMessage {
94 94 */
95 95 private String msg;
96 96  
  97 + /**
  98 + * 父消息 ID
  99 + * <p>
  100 + * - null:原始消息,需要 reply
  101 + * - 非 null:从父消息(如 pack)拆分而来,不需要单独 reply
  102 + */
  103 + private String parentMessageId;
  104 +
  105 + // TODO @TODO @AI:抽到工具类里,具体哪个,一起讨论下;
  106 + /**
  107 + * 判断是否需要发送 reply
  108 + *
  109 + * @return 是否需要回复
  110 + */
  111 + public boolean needReply() {
  112 + // 1. 来自拆包的消息,不单独 reply
  113 + if (parentMessageId != null) {
  114 + return false;
  115 + }
  116 + // 2. 某些方法本身不需要 reply(如 STATE_UPDATE)
  117 + if (IotDeviceMessageMethodEnum.isReplyDisabled(method)) {
  118 + return false;
  119 + }
  120 + return true;
  121 + }
  122 +
  123 + // TODO @AI:没用的字段,删除删除
  124 + /**
  125 + * 获取原始消息 ID(用于日志追踪)
  126 + *
  127 + * @return 原始消息 ID
  128 + */
  129 + public String getOriginMessageId() {
  130 + return parentMessageId != null ? parentMessageId : id;
  131 + }
  132 +
97 133 // ========== 基础方法:只传递"codec(编解码)字段" ==========
98 134  
99 135 public static IotDeviceMessage requestOf(String method) {
... ...
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 +// TODO @AI:不用带 device 关键字;
  7 +// TODO @AI:挂个阿里云的链接,https://help.aliyun.com/zh/iot/user-guide/register-devices 的「子设备的 MQTT 动态注册」小节
  8 +/**
  9 + * IoT 子设备动态注册 Request DTO
  10 + * <p>
  11 + * 用于 thing.sub.register 消息的 params 数组元素
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +public class IotSubDeviceRegisterReqDTO {
  17 +
  18 + /**
  19 + * 子设备 ProductKey
  20 + */
  21 + @NotEmpty(message = "产品标识不能为空")
  22 + private String productKey;
  23 +
  24 + /**
  25 + * 子设备 DeviceName
  26 + */
  27 + @NotEmpty(message = "设备名称不能为空")
  28 + private String deviceName;
  29 +
  30 +}
... ...
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.Data;
  4 +
  5 +// TODO @AI:修复建议,参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
  6 +/**
  7 + * IoT 子设备动态注册 Response DTO
  8 + * <p>
  9 + * 用于 thing.sub.register 响应的设备信息
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +public class IotSubDeviceRegisterRespDTO {
  15 +
  16 + /**
  17 + * 子设备 ProductKey
  18 + */
  19 + private String productKey;
  20 +
  21 + /**
  22 + * 子设备 DeviceName
  23 + */
  24 + private String deviceName;
  25 +
  26 + /**
  27 + * 分配的 DeviceSecret
  28 + */
  29 + private String deviceSecret;
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java 0 → 100644
  1 +/**
  2 + * TODO @AI:写下注释
  3 + */
  4 +package cn.iocoder.yudao.module.iot.core.topic;
0 5 \ No newline at end of file
... ...
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 lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +
  9 +// TODO @AI:挂个阿里云的链接,http://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 的「设备批量上报属性、事件」小节
  10 +/**
  11 + * IoT 设备属性批量上报 Request DTO
  12 + * <p>
  13 + * 用于 thing.event.property.pack.post 消息的 params 参数
  14 + * 参考阿里云 Alink 协议
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Data
  19 +public class IotDevicePropertyPackPostReqDTO {
  20 +
  21 + // TODO @AI:去掉里面的 time,直接平铺值(可能就是直接的 map);例如说 "Power":value, 而不是 PropertyValue "properties": {
  22 + // "Power": [
  23 + // {
  24 + // "value": "on",
  25 + // "time": 1524448722000
  26 + // },
  27 + // {
  28 + // "value": "off",
  29 + // "time": 1524448722001
  30 + // }
  31 + // ],
  32 + // "WF": [
  33 + // {
  34 + // "value": 3,
  35 + // "time": 1524448722000
  36 + // },
  37 + // {
  38 + // "value": 4,
  39 + // "time": 1524448722009
  40 + // }
  41 + // ]
  42 + // }
  43 +
  44 + /**
  45 + * 网关自身属性
  46 + * <p>
  47 + * key: 属性标识符
  48 + * value: 属性值对象(包含 value 和 time)
  49 + */
  50 + private Map<String, PropertyValue> properties;
  51 +
  52 + // TODO @AI:EventValue {
  53 + //
  54 + // "method": "thing.event.post",
  55 + //
  56 + // "version": "1.0",
  57 + //
  58 + // "params": {
  59 + //
  60 + // "identifier": "eat",
  61 + //
  62 + // "params": {
  63 + //
  64 + // "rice": 100
  65 + //
  66 + // }
  67 + //
  68 + // }
  69 + //
  70 + //}
  71 +
  72 +
  73 + /**
  74 + * 网关自身事件
  75 + * <p>
  76 + * key: 事件标识符
  77 + * value: 事件值对象(包含 value 和 time)
  78 + */
  79 + private Map<String, EventValue> events;
  80 +
  81 + /**
  82 + * 子设备数据列表
  83 + */
  84 + private List<SubDeviceData> subDevices;
  85 +
  86 + /**
  87 + * 属性值对象
  88 + */
  89 + @Data
  90 + public static class PropertyValue {
  91 +
  92 + /**
  93 + * 属性值
  94 + */
  95 + private Object value;
  96 +
  97 + /**
  98 + * 上报时间(毫秒时间戳)
  99 + */
  100 + private Long time;
  101 +
  102 + }
  103 +
  104 + /**
  105 + * 事件值对象
  106 + */
  107 + @Data
  108 + public static class EventValue {
  109 +
  110 + /**
  111 + * 事件参数
  112 + */
  113 + private Object value;
  114 +
  115 + /**
  116 + * 上报时间(毫秒时间戳)
  117 + */
  118 + private Long time;
  119 +
  120 + }
  121 +
  122 + /**
  123 + * 子设备数据
  124 + */
  125 + @Data
  126 + public static class SubDeviceData {
  127 +
  128 + /**
  129 + * 子设备标识
  130 + */
  131 + private DeviceIdentity identity;
  132 +
  133 + /**
  134 + * 子设备属性
  135 + */
  136 + private Map<String, PropertyValue> properties;
  137 +
  138 + /**
  139 + * 子设备事件
  140 + */
  141 + private Map<String, EventValue> events;
  142 +
  143 + }
  144 +
  145 + /**
  146 + * 设备标识
  147 + */
  148 + @Data
  149 + @Accessors(chain = true)
  150 + public static class DeviceIdentity {
  151 +
  152 + /**
  153 + * 产品标识
  154 + */
  155 + private String productKey;
  156 +
  157 + /**
  158 + * 设备名称
  159 + */
  160 + private String deviceName;
  161 +
  162 + }
  163 +
  164 +}
... ...
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 jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +// TODO @AI:得一起讨论下,到底使用什么后缀合适:1)一方面要体现出请求、响应;2)一方面体现出上下行(设备 to server,还是 server to 设备),可以一起讨论?
  7 +// TODO @AI:文档地址:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships?spm=a2c4g.11186623.help-menu-30520.d_2_2_7_3_2.2e983f47Z2iGbo&scm=20140722.H_89299._.OR_help-T_cn~zh-V_1#section-w33-vyg-12b
  8 +/**
  9 + * IoT 设备拓扑添加 Request DTO
  10 + * <p>
  11 + * 用于 thing.topo.add 消息的 params 参数
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +public class IotDeviceTopoAddReqDTO {
  17 +
  18 + // TODO @AI:是个数组;
  19 + // TODO @AI:有响应结果的;
  20 +
  21 + /**
  22 + * 子设备客户端 ID
  23 + */
  24 + @NotEmpty(message = "客户端 ID 不能为空")
  25 + private String clientId;
  26 +
  27 + /**
  28 + * 子设备用户名
  29 + */
  30 + @NotEmpty(message = "用户名不能为空")
  31 + private String username;
  32 +
  33 + /**
  34 + * 子设备认证密码
  35 + */
  36 + @NotEmpty(message = "密码不能为空")
  37 + private String password;
  38 +
  39 +}
... ...
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 jakarta.validation.constraints.NotEmpty;
  4 +import lombok.Data;
  5 +
  6 +// TODO @AI:是个数组;https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships?spm=a2c4g.11186623.help-menu-30520.d_2_2_7_3_2.540c390beZSDOG&scm=20140722.H_89299._.OR_help-T_cn~zh-V_1#section-rb1-wzw-y2b
  7 +
  8 +// todo @AI:删除完,需要有个 reply 响应;
  9 +
  10 +/**
  11 + * IoT 设备拓扑删除 Request DTO
  12 + * <p>
  13 + * 用于 thing.topo.delete 消息的 params 参数
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Data
  18 +public class IotDeviceTopoDeleteReqDTO {
  19 +
  20 + /**
  21 + * 子设备 ProductKey
  22 + */
  23 + @NotEmpty(message = "产品标识不能为空")
  24 + private String productKey;
  25 +
  26 + /**
  27 + * 子设备 DeviceName
  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/topo/IotDeviceTopoRespDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic.topo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +// TODO @AI:是不是改成 IotDeviceTopoGetRespDTO
  6 +/**
  7 + * IoT 设备拓扑关系 Response DTO
  8 + * <p>
  9 + * 用于 thing.topo.get 响应的子设备信息
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +public class IotDeviceTopoRespDTO {
  15 +
  16 + /**
  17 + * 子设备 ProductKey
  18 + */
  19 + private String productKey;
  20 +
  21 + /**
  22 + * 子设备 DeviceName
  23 + */
  24 + private String deviceName;
  25 +
  26 +}
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
... ... @@ -63,4 +63,50 @@ public final class IotMqttTopicUtils {
63 63 return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix;
64 64 }
65 65  
  66 + /**
  67 + * 构建拓扑管理 Topic
  68 + * <p>
  69 + * 拓扑管理类 Topic 使用网关设备的 productKey/deviceName
  70 + *
  71 + * @param method 方法,如 thing.topo.add
  72 + * @param gatewayProductKey 网关 ProductKey
  73 + * @param gatewayDeviceName 网关 DeviceName
  74 + * @param isReply 是否为响应
  75 + * @return Topic
  76 + */
  77 + public static String buildTopoTopic(String method, String gatewayProductKey,
  78 + String gatewayDeviceName, boolean isReply) {
  79 + return buildTopicByMethod(method, gatewayProductKey, gatewayDeviceName, isReply);
  80 + }
  81 +
  82 + /**
  83 + * 判断是否为拓扑管理 Topic(通过 method 判断)
  84 + *
  85 + * @param method 消息方法
  86 + * @return 是否为拓扑管理 Topic
  87 + */
  88 + public static boolean isTopoMethod(String method) {
  89 + return method != null && method.startsWith("thing.topo.");
  90 + }
  91 +
  92 + /**
  93 + * 判断是否为子设备注册 Topic
  94 + *
  95 + * @param method 消息方法
  96 + * @return 是否为子设备注册 Topic
  97 + */
  98 + public static boolean isSubDeviceRegisterMethod(String method) {
  99 + return "thing.sub.register".equals(method);
  100 + }
  101 +
  102 + /**
  103 + * 判断是否为批量上报 Topic
  104 + *
  105 + * @param method 消息方法
  106 + * @return 是否为批量上报 Topic
  107 + */
  108 + public static boolean isPackPostMethod(String method) {
  109 + return "thing.event.property.pack.post".equals(method);
  110 + }
  111 +
66 112 }
67 113 \ No newline at end of file
... ...