Commit 18ed7b50beedd93331ee42bec0ccb4db7ca248c0

Authored by YunaiV
1 parent a2750693

feat(iot):【网关设备:60%】整体初步实现(优化部分代码)

Showing 14 changed files with 155 additions and 216 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
@@ -40,8 +40,6 @@ public interface ErrorCodeConstants { @@ -40,8 +40,6 @@ public interface ErrorCodeConstants {
40 ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); 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, "子设备认证失败"); 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, "子设备【{}/{}】未绑定到该网关"); 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 43 // 子设备注册相关错误码 1-050-003-200
46 ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); 44 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, "产品【{}】不是网关子设备类型"); 45 ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
@@ -471,7 +471,7 @@ public class IotDeviceServiceImpl implements IotDeviceService { @@ -471,7 +471,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
471 471
472 // 3. 校验子设备拓扑关系:子设备必须先绑定到某网关才能认证上线 472 // 3. 校验子设备拓扑关系:子设备必须先绑定到某网关才能认证上线
473 if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType()) 473 if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())
474 - && device.getGatewayId() != null) { 474 + && device.getGatewayId() == null) {
475 log.warn("[authDevice][子设备({}/{}) 未绑定到任何网关,认证失败]", productKey, deviceName); 475 log.warn("[authDevice][子设备({}/{}) 未绑定到任何网关,认证失败]", productKey, deviceName);
476 return false; 476 return false;
477 } 477 }
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;
3 import cn.hutool.core.date.LocalDateTimeUtil; 4 import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.hutool.core.lang.Assert; 5 import cn.hutool.core.lang.Assert;
5 import cn.hutool.core.map.MapUtil; 6 import cn.hutool.core.map.MapUtil;
  7 +import cn.hutool.core.util.ObjectUtil;
6 import cn.hutool.core.util.StrUtil; 8 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.extra.spring.SpringUtil; 9 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.exception.ServiceException; 10 import cn.iocoder.yudao.framework.common.exception.ServiceException;
@@ -183,10 +185,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -183,10 +185,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
183 // 2. 记录消息 185 // 2. 记录消息
184 getSelf().createDeviceLogAsync(message); 186 getSelf().createDeviceLogAsync(message);
185 187
186 - // TODO @AI:我在想,是不是批量上传后,还是得 reply 。因为打包上传的时候,只是那条消息的回复。然后,需要单独给每个子消息回复,后续至于怎么使用,是不是得看具体业务了;例如说:1)批量上传属性,默认回复是批量上传的消息;然后,每个属性、事件拆包消息,单独回复,后续网关设备按需回复给子设备。  
187 - // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息、非拆包消息 188 + // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息
188 if (IotDeviceMessageUtils.isReplyMessage(message) 189 if (IotDeviceMessageUtils.isReplyMessage(message)
189 - || !message.needReply() 190 + || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod())
190 || StrUtil.isEmpty(message.getServerId())) { 191 || StrUtil.isEmpty(message.getServerId())) {
191 return; 192 return;
192 } 193 }
@@ -242,7 +243,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -242,7 +243,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
242 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) { 243 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) {
243 return handleTopoDelete(message, device); 244 return handleTopoDelete(message, device);
244 } 245 }
245 -  
246 // 获取拓扑关系 246 // 获取拓扑关系
247 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) { 247 if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) {
248 return handleTopoGet(device); 248 return handleTopoGet(device);
@@ -267,57 +267,81 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -267,57 +267,81 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
267 * @return 响应数据 267 * @return 响应数据
268 */ 268 */
269 private Object handleTopoAdd(IotDeviceMessage message, IotDeviceDO gatewayDevice) { 269 private Object handleTopoAdd(IotDeviceMessage message, IotDeviceDO gatewayDevice) {
270 - // TODO @AI:这里是不是 1.1 1.2 1.3 这样?修改前,我们确认下,有没模块是这么写的;iot 消息处理里;  
271 - // 1. 校验网关设备类型 270 + // 1.1 校验网关设备类型
272 if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { 271 if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) {
273 throw exception(DEVICE_NOT_GATEWAY); 272 throw exception(DEVICE_NOT_GATEWAY);
274 } 273 }
275 -  
276 - // 2. 解析参数 274 + // 1.2 解析参数
277 // TODO @AI:是不是 parseObject 增加一个方法,允许传入 object 类型,避免先转 jsonString 再 parseObject ; 275 // TODO @AI:是不是 parseObject 增加一个方法,允许传入 object 类型,避免先转 jsonString 再 parseObject ;
278 IotDeviceTopoAddReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()), 276 IotDeviceTopoAddReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()),
279 IotDeviceTopoAddReqDTO.class); 277 IotDeviceTopoAddReqDTO.class);
280 - if (params == null) { 278 + if (params == null || CollUtil.isEmpty(params.getSubDevices())) {
281 throw exception(DEVICE_TOPO_PARAMS_INVALID); 279 throw exception(DEVICE_TOPO_PARAMS_INVALID);
282 } 280 }
283 281
284 - // 3. 解析子设备信息  
285 - IotDeviceAuthUtils.DeviceInfo subDeviceInfo = IotDeviceAuthUtils.parseUsername(params.getUsername()); 282 + // 2. 遍历处理每个子设备
  283 + // TODO @AI:processTopoAddSubDevice 不要抽成小方法;
  284 + List<IotDeviceDO> addedSubDevices = new ArrayList<>(params.getSubDevices().size());
  285 + for (IotDeviceAuthReqDTO subDeviceAuth : params.getSubDevices()) {
  286 + try {
  287 + IotDeviceDO subDevice = processTopoAddSubDevice(subDeviceAuth, gatewayDevice);
  288 + addedSubDevices.add(subDevice);
  289 + } catch (Exception ex) {
  290 + log.warn("[handleTopoAdd][网关({}/{}) 添加子设备失败,username={}]",
  291 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  292 + subDeviceAuth.getUsername(), ex);
  293 + }
  294 + }
  295 + // TODO @AI:http://help.aliyun.com/zh/marketplace/add-topological-relationship 要回复的!
  296 +
  297 + // 3. 发送拓扑变更通知
  298 + // TODO @AI:这里不应该发,它更多发生在,管理后台改动后,主动下发;http://help.aliyun.com/zh/marketplace/notify-gateway-topology-changes
  299 + for (IotDeviceDO subDevice : addedSubDevices) {
  300 + sendTopoChangeNotify(gatewayDevice, "add", subDevice);
  301 + }
  302 + return null;
  303 + }
  304 +
  305 + /**
  306 + * 处理单个子设备的拓扑添加
  307 + *
  308 + * @param subDeviceAuth 子设备认证信息
  309 + * @param gatewayDevice 网关设备
  310 + * @return 添加成功的子设备,失败返回 null
  311 + */
  312 + private IotDeviceDO processTopoAddSubDevice(IotDeviceAuthReqDTO subDeviceAuth, IotDeviceDO gatewayDevice) {
  313 + // 1.1 解析子设备信息
  314 + IotDeviceAuthUtils.DeviceInfo subDeviceInfo = IotDeviceAuthUtils.parseUsername(subDeviceAuth.getUsername());
286 if (subDeviceInfo == null) { 315 if (subDeviceInfo == null) {
287 throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID); 316 throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID);
288 } 317 }
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)) { 318 + // 1.2 校验子设备认证信息
  319 + if (!deviceService.authDevice(subDeviceAuth)) {
297 throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED); 320 throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED);
298 } 321 }
299 322
300 - // 5. 获取子设备 323 + // 1.3 获取子设备
301 IotDeviceDO subDevice = deviceService.getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName()); 324 IotDeviceDO subDevice = deviceService.getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
302 if (subDevice == null) { 325 if (subDevice == null) {
303 throw exception(DEVICE_NOT_EXISTS); 326 throw exception(DEVICE_NOT_EXISTS);
304 } 327 }
305 -  
306 - // 6. 校验子设备类型 328 + // 1.4 校验子设备类型
307 if (!IotProductDeviceTypeEnum.isGatewaySub(subDevice.getDeviceType())) { 329 if (!IotProductDeviceTypeEnum.isGatewaySub(subDevice.getDeviceType())) {
308 throw exception(DEVICE_NOT_GATEWAY_SUB, subDevice.getProductKey(), subDevice.getDeviceName()); 330 throw exception(DEVICE_NOT_GATEWAY_SUB, subDevice.getProductKey(), subDevice.getDeviceName());
309 } 331 }
  332 + // 1.5 校验子设备是否已绑定到其他网关
  333 + if (subDevice.getGatewayId() != null
  334 + && ObjectUtil.notEqual(subDevice.getGatewayId(), gatewayDevice.getId())) {
  335 + throw exception(DEVICE_GATEWAY_BINDTO_EXISTS, subDevice.getProductKey(), subDevice.getDeviceName());
  336 + }
310 337
311 - // 7. 绑定拓扑关系  
312 - // TODO @AI:这里要考虑,校验是不是老设备已经绑定到其他网关了? 338 + // 2. 绑定拓扑关系
  339 + // TODO @AI:上面的校验,貌似和 bindDeviceGateway 里的,有点重复;
313 deviceService.bindDeviceGateway(Collections.singletonList(subDevice.getId()), gatewayDevice.getId()); 340 deviceService.bindDeviceGateway(Collections.singletonList(subDevice.getId()), gatewayDevice.getId());
314 - log.info("[handleTopoAdd][网关({}/{}) 绑定子设备({}/{})]", 341 + log.info("[processTopoAddSubDevice][网关({}/{}) 绑定子设备({}/{})]",
315 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), 342 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
316 subDevice.getProductKey(), subDevice.getDeviceName()); 343 subDevice.getProductKey(), subDevice.getDeviceName());
317 -  
318 - // 8. 发送拓扑变更通知  
319 - sendTopoChangeNotify(gatewayDevice, "add", subDevice);  
320 - return null; 344 + return subDevice;
321 } 345 }
322 346
323 // TODO @AI:是不是更适合在 deviceService 里面处理? 347 // TODO @AI:是不是更适合在 deviceService 里面处理?
@@ -335,41 +359,70 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -335,41 +359,70 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
335 throw exception(DEVICE_NOT_GATEWAY); 359 throw exception(DEVICE_NOT_GATEWAY);
336 } 360 }
337 361
338 - // 2. 解析参数 362 + // 2. 解析参数(数组格式)
339 IotDeviceTopoDeleteReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()), 363 IotDeviceTopoDeleteReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()),
340 IotDeviceTopoDeleteReqDTO.class); 364 IotDeviceTopoDeleteReqDTO.class);
341 - if (params == null) { 365 + if (params == null || params.getSubDevices() == null || params.getSubDevices().isEmpty()) {
342 throw exception(DEVICE_TOPO_PARAMS_INVALID); 366 throw exception(DEVICE_TOPO_PARAMS_INVALID);
343 } 367 }
344 368
345 - // 3. 获取子设备  
346 - IotDeviceDO subDevice = deviceService.getDeviceFromCache(params.getProductKey(), params.getDeviceName()); 369 + // 3. 遍历处理每个子设备
  370 + List<IotDeviceDO> deletedSubDevices = new ArrayList<>();
  371 + for (IotDeviceTopoDeleteReqDTO.SubDevice subDeviceInfo : params.getSubDevices()) {
  372 + try {
  373 + IotDeviceDO subDevice = processTopoDeleteSubDevice(subDeviceInfo, gatewayDevice);
  374 + deletedSubDevices.add(subDevice);
  375 + } catch (Exception ex) {
  376 + log.warn("[handleTopoDelete][网关({}/{}) 删除子设备失败,productKey={}, deviceName={}]",
  377 + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
  378 + subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName(), ex);
  379 + }
  380 + }
  381 +
  382 + // 4. 发送拓扑变更通知
  383 + for (IotDeviceDO subDevice : deletedSubDevices) {
  384 + sendTopoChangeNotify(gatewayDevice, "delete", subDevice);
  385 + }
  386 + return null;
  387 + }
  388 +
  389 + // TODO @AI:是不是更适合在 deviceService 里面处理?
  390 +
  391 + /**
  392 + * 处理单个子设备的拓扑删除
  393 + *
  394 + * @param subDeviceInfo 子设备标识
  395 + * @param gatewayDevice 网关设备
  396 + * @return 删除成功的子设备,失败返回 null
  397 + */
  398 + private IotDeviceDO processTopoDeleteSubDevice(IotDeviceTopoDeleteReqDTO.SubDevice subDeviceInfo,
  399 + IotDeviceDO gatewayDevice) {
  400 + // 1. 获取子设备
  401 + IotDeviceDO subDevice = deviceService.getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
347 if (subDevice == null) { 402 if (subDevice == null) {
348 throw exception(DEVICE_NOT_EXISTS); 403 throw exception(DEVICE_NOT_EXISTS);
349 } 404 }
350 405
351 - // 4. 校验子设备是否绑定到该网关 406 + // 2. 校验子设备是否绑定到该网关
352 if (!Objects.equal(subDevice.getGatewayId(), gatewayDevice.getId())) { 407 if (!Objects.equal(subDevice.getGatewayId(), gatewayDevice.getId())) {
353 - throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY, params.getProductKey(), params.getDeviceName()); 408 + throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY, subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName());
354 } 409 }
355 410
356 - // 5. 解绑拓扑关系 411 + // 3. 解绑拓扑关系
357 deviceService.unbindDeviceGateway(Collections.singletonList(subDevice.getId())); 412 deviceService.unbindDeviceGateway(Collections.singletonList(subDevice.getId()));
358 - log.info("[handleTopoDelete][网关({}/{}) 解绑子设备({}/{})]", 413 + log.info("[processTopoDeleteSubDevice][网关({}/{}) 解绑子设备({}/{})]",
359 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), 414 gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(),
360 subDevice.getProductKey(), subDevice.getDeviceName()); 415 subDevice.getProductKey(), subDevice.getDeviceName());
361 416
362 - // 6. 子设备下线 417 + // 4. 子设备下线
363 if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) { 418 if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) {
364 deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState()); 419 deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState());
365 } 420 }
366 421
367 - // 7. 发送拓扑变更通知  
368 - sendTopoChangeNotify(gatewayDevice, "delete", subDevice);  
369 -  
370 - return null; 422 + return subDevice;
371 } 423 }
372 424
  425 + // TODO @AI:是不是更适合在 deviceService 里面处理?
373 /** 426 /**
374 * 处理获取拓扑关系请求 427 * 处理获取拓扑关系请求
375 * 428 *
@@ -391,6 +444,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -391,6 +444,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
391 .setDeviceName(subDevice.getDeviceName())); 444 .setDeviceName(subDevice.getDeviceName()));
392 } 445 }
393 446
  447 + // TODO @AI:是不是更适合在 deviceService 里面处理?
394 /** 448 /**
395 * 发送拓扑变更通知 449 * 发送拓扑变更通知
396 * 450 *
@@ -427,6 +481,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -427,6 +481,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
427 481
428 // ========== 子设备注册处理方法 ========== 482 // ========== 子设备注册处理方法 ==========
429 483
  484 + // TODO @AI:是不是更适合在 deviceService 里面处理?
430 /** 485 /**
431 * 处理子设备动态注册请求 486 * 处理子设备动态注册请求
432 * 487 *
@@ -469,6 +524,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -469,6 +524,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
469 return results; 524 return results;
470 } 525 }
471 526
  527 + // TODO @AI:是不是更适合在 deviceService 里面处理?
472 /** 528 /**
473 * 注册单个子设备 529 * 注册单个子设备
474 * 530 *
@@ -539,11 +595,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -539,11 +595,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
539 } 595 }
540 596
541 // 2. 处理网关自身属性 597 // 2. 处理网关自身属性
  598 + // TODO @AI:是不是经过总线会更好:
  599 + // TODO @AI:是不是少处理了 event 事件?
542 if (params.getProperties() != null && !params.getProperties().isEmpty()) { 600 if (params.getProperties() != null && !params.getProperties().isEmpty()) {
543 Map<String, Object> gatewayProperties = convertPackProperties(params.getProperties()); 601 Map<String, Object> gatewayProperties = convertPackProperties(params.getProperties());
544 IotDeviceMessage gatewayMsg = IotDeviceMessage.builder() 602 IotDeviceMessage gatewayMsg = IotDeviceMessage.builder()
545 .id(IotDeviceMessageUtils.generateMessageId()) 603 .id(IotDeviceMessageUtils.generateMessageId())
546 - .parentMessageId(packMessage.getId())  
547 .deviceId(gatewayDevice.getId()) 604 .deviceId(gatewayDevice.getId())
548 .tenantId(gatewayDevice.getTenantId()) 605 .tenantId(gatewayDevice.getTenantId())
549 .serverId(packMessage.getServerId()) 606 .serverId(packMessage.getServerId())
@@ -596,7 +653,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -596,7 +653,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
596 Map<String, Object> properties = convertPackProperties(subDeviceData.getProperties()); 653 Map<String, Object> properties = convertPackProperties(subDeviceData.getProperties());
597 IotDeviceMessage subMsg = IotDeviceMessage.builder() 654 IotDeviceMessage subMsg = IotDeviceMessage.builder()
598 .id(IotDeviceMessageUtils.generateMessageId()) 655 .id(IotDeviceMessageUtils.generateMessageId())
599 - .parentMessageId(packMessage.getId())  
600 .deviceId(subDevice.getId()) 656 .deviceId(subDevice.getId())
601 .tenantId(subDevice.getTenantId()) 657 .tenantId(subDevice.getTenantId())
602 .serverId(packMessage.getServerId()) 658 .serverId(packMessage.getServerId())
@@ -608,8 +664,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -608,8 +664,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
608 getSelf().createDeviceLogAsync(subMsg); 664 getSelf().createDeviceLogAsync(subMsg);
609 } 665 }
610 666
611 - // 3. 处理子设备事件(如果需要)  
612 - // TODO: 事件处理可以后续扩展 667 + // 3. 处理子设备事件
  668 + // TODO @AI:事件处理可以后续扩展
613 } 669 }
614 670
615 /** 671 /**
@@ -633,7 +689,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -633,7 +689,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
633 689
634 // ========== 网关下线联动处理 ========== 690 // ========== 网关下线联动处理 ==========
635 691
636 - // TODO @AI:是不是写到 deviceService 里更合适?更解耦。 692 + // TODO 芋艿:是不是写到 deviceService 里更合适?更解耦。
637 /** 693 /**
638 * 处理网关下线,联动所有子设备下线 694 * 处理网关下线,联动所有子设备下线
639 * 695 *
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
@@ -43,7 +43,6 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -43,7 +43,6 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
43 PROPERTY_POST("thing.property.post", "属性上报", true), 43 PROPERTY_POST("thing.property.post", "属性上报", true),
44 PROPERTY_SET("thing.property.set", "属性设置", false), 44 PROPERTY_SET("thing.property.set", "属性设置", false),
45 45
46 - // TODO @AI:改成 thing.property.pack.post  
47 PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有 46 PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
48 47
49 // ========== 设备事件 ========== 48 // ========== 设备事件 ==========
@@ -77,8 +76,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; { @@ -77,8 +76,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
77 */ 76 */
78 public static final Set<String> REPLY_DISABLED = SetUtils.asSet( 77 public static final Set<String> REPLY_DISABLED = SetUtils.asSet(
79 STATE_UPDATE.getMethod(), 78 STATE_UPDATE.getMethod(),
80 - OTA_PROGRESS.getMethod(), // 参考阿里云,OTA 升级进度上报,不进行回复  
81 - TOPO_CHANGE.getMethod() // 拓扑变更通知,下行消息,不需要回复 TODO @AI:看看阿里云的文档,确认下是不是这样的 79 + OTA_PROGRESS.getMethod() // 参考阿里云,OTA 升级进度上报,不进行回复
82 ); 80 );
83 81
84 private final String method; 82 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,42 +94,6 @@ public class IotDeviceMessage { @@ -94,42 +94,6 @@ public class IotDeviceMessage {
94 */ 94 */
95 private String msg; 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 -  
133 // ========== 基础方法:只传递"codec(编解码)字段" ========== 97 // ========== 基础方法:只传递"codec(编解码)字段" ==========
134 98
135 public static IotDeviceMessage requestOf(String method) { 99 public static IotDeviceMessage requestOf(String method) {
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentify.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.topic;
  2 +
  3 +// TODO @AI:增加 productKey、DeviceName
  4 +// TODO @AI:有更合适的类名么???
  5 +public class IotDeviceIdentify {
  6 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
@@ -3,14 +3,13 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; @@ -3,14 +3,13 @@ package cn.iocoder.yudao.module.iot.core.topic.auth;
3 import jakarta.validation.constraints.NotEmpty; 3 import jakarta.validation.constraints.NotEmpty;
4 import lombok.Data; 4 import lombok.Data;
5 5
6 -// TODO @AI:不用带 device 关键字;  
7 -// TODO @AI:挂个阿里云的链接,https://help.aliyun.com/zh/iot/user-guide/register-devices 的「子设备的 MQTT 动态注册」小节  
8 /** 6 /**
9 * IoT 子设备动态注册 Request DTO 7 * IoT 子设备动态注册 Request DTO
10 * <p> 8 * <p>
11 * 用于 thing.sub.register 消息的 params 数组元素 9 * 用于 thing.sub.register 消息的 params 数组元素
12 * 10 *
13 * @author 芋道源码 11 * @author 芋道源码
  12 + * @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a>
14 */ 13 */
15 @Data 14 @Data
16 public class IotSubDeviceRegisterReqDTO { 15 public class IotSubDeviceRegisterReqDTO {
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
@@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.iot.core.topic.auth; @@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.iot.core.topic.auth;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 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 /** 5 /**
7 * IoT 子设备动态注册 Response DTO 6 * IoT 子设备动态注册 Response DTO
8 * <p> 7 * <p>
9 * 用于 thing.sub.register 响应的设备信息 8 * 用于 thing.sub.register 响应的设备信息
10 * 9 *
11 * @author 芋道源码 10 * @author 芋道源码
  11 + * @see <a href="http://help.aliyun.com/zh/marketplace/dynamic-registration-of-sub-devices">阿里云 - 动态注册子设备</a>
12 */ 12 */
13 @Data 13 @Data
14 public class IotSubDeviceRegisterRespDTO { 14 public class IotSubDeviceRegisterRespDTO {
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
1 /** 1 /**
2 - * TODO @AI:写下注释 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>
3 */ 7 */
4 -package cn.iocoder.yudao.module.iot.core.topic;  
5 \ No newline at end of file 8 \ No newline at end of file
  9 +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
@@ -6,41 +6,18 @@ import lombok.experimental.Accessors; @@ -6,41 +6,18 @@ import lombok.experimental.Accessors;
6 import java.util.List; 6 import java.util.List;
7 import java.util.Map; 7 import java.util.Map;
8 8
9 -// TODO @AI:挂个阿里云的链接,http://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 的「设备批量上报属性、事件」小节  
10 /** 9 /**
11 * IoT 设备属性批量上报 Request DTO 10 * IoT 设备属性批量上报 Request DTO
12 * <p> 11 * <p>
13 * 用于 thing.event.property.pack.post 消息的 params 参数 12 * 用于 thing.event.property.pack.post 消息的 params 参数
14 - * 参考阿里云 Alink 协议  
15 * 13 *
16 * @author 芋道源码 14 * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/gateway-reports-data-in-batches">阿里云 - 网关批量上报数据</a>
17 */ 16 */
18 @Data 17 @Data
19 public class IotDevicePropertyPackPostReqDTO { 18 public class IotDevicePropertyPackPostReqDTO {
20 19
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 - 20 + // TODO @AI:不用 PropertyValue,直接使用 Object 接收就行!
44 /** 21 /**
45 * 网关自身属性 22 * 网关自身属性
46 * <p> 23 * <p>
@@ -49,27 +26,6 @@ public class IotDevicePropertyPackPostReqDTO { @@ -49,27 +26,6 @@ public class IotDevicePropertyPackPostReqDTO {
49 */ 26 */
50 private Map<String, PropertyValue> properties; 27 private Map<String, PropertyValue> properties;
51 28
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 /** 29 /**
74 * 网关自身事件 30 * 网关自身事件
75 * <p> 31 * <p>
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
1 package cn.iocoder.yudao.module.iot.core.topic.topo; 1 package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2
  3 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
3 import jakarta.validation.constraints.NotEmpty; 4 import jakarta.validation.constraints.NotEmpty;
4 import lombok.Data; 5 import lombok.Data;
5 6
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 7 +import java.util.List;
  8 +
8 /** 9 /**
9 * IoT 设备拓扑添加 Request DTO 10 * IoT 设备拓扑添加 Request DTO
10 * <p> 11 * <p>
11 * 用于 thing.topo.add 消息的 params 参数 12 * 用于 thing.topo.add 消息的 params 参数
12 * 13 *
13 * @author 芋道源码 14 * @author 芋道源码
  15 + * @see <a href="http://help.aliyun.com/zh/marketplace/add-topological-relationship">阿里云 - 添加拓扑关系</a>
14 */ 16 */
15 @Data 17 @Data
16 public class IotDeviceTopoAddReqDTO { 18 public class IotDeviceTopoAddReqDTO {
17 19
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 /** 20 /**
34 - * 子设备认证密码 21 + * 子设备认证信息列表
  22 + * <p>
  23 + * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
35 */ 24 */
36 - @NotEmpty(message = "密码不能为空")  
37 - private String password; 25 + @NotEmpty(message = "子设备认证信息列表不能为空")
  26 + private List<IotDeviceAuthReqDTO> subDevices;
38 27
39 } 28 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; @@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.iot.core.topic.topo;
3 import jakarta.validation.constraints.NotEmpty; 3 import jakarta.validation.constraints.NotEmpty;
4 import lombok.Data; 4 import lombok.Data;
5 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 响应; 6 +import java.util.List;
9 7
10 /** 8 /**
11 * IoT 设备拓扑删除 Request DTO 9 * IoT 设备拓扑删除 Request DTO
@@ -13,20 +11,35 @@ import lombok.Data; @@ -13,20 +11,35 @@ import lombok.Data;
13 * 用于 thing.topo.delete 消息的 params 参数 11 * 用于 thing.topo.delete 消息的 params 参数
14 * 12 *
15 * @author 芋道源码 13 * @author 芋道源码
  14 + * @see <a href="https://help.aliyun.com/zh/marketplace/delete-a-topological-relationship">阿里云 - 删除拓扑关系</a>
16 */ 15 */
17 @Data 16 @Data
18 public class IotDeviceTopoDeleteReqDTO { 17 public class IotDeviceTopoDeleteReqDTO {
19 18
  19 + // TODO @AI:应该是数组;IotDeviceIdentify
20 /** 20 /**
21 - * 子设备 ProductKey 21 + * 子设备标识列表
22 */ 22 */
23 - @NotEmpty(message = "产品标识不能为空")  
24 - private String productKey; 23 + private List<SubDevice> subDevices;
25 24
26 /** 25 /**
27 - * 子设备 DeviceName 26 + * 子设备标识
28 */ 27 */
29 - @NotEmpty(message = "设备名称不能为空")  
30 - private String deviceName; 28 + @Data
  29 + public static class SubDevice {
  30 +
  31 + /**
  32 + * 子设备 ProductKey
  33 + */
  34 + @NotEmpty(message = "产品标识不能为空")
  35 + private String productKey;
  36 +
  37 + /**
  38 + * 子设备 DeviceName
  39 + */
  40 + @NotEmpty(message = "设备名称不能为空")
  41 + private String deviceName;
  42 +
  43 + }
31 44
32 } 45 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoRespDTO.java
@@ -2,17 +2,19 @@ package cn.iocoder.yudao.module.iot.core.topic.topo; @@ -2,17 +2,19 @@ package cn.iocoder.yudao.module.iot.core.topic.topo;
2 2
3 import lombok.Data; 3 import lombok.Data;
4 4
5 -// TODO @AI:是不是改成 IotDeviceTopoGetRespDTO 5 +// TODO @AI:IotDeviceTopoGetRespDTO
6 /** 6 /**
7 * IoT 设备拓扑关系 Response DTO 7 * IoT 设备拓扑关系 Response DTO
8 * <p> 8 * <p>
9 * 用于 thing.topo.get 响应的子设备信息 9 * 用于 thing.topo.get 响应的子设备信息
10 * 10 *
11 * @author 芋道源码 11 * @author 芋道源码
  12 + * @see <a href="https://help.aliyun.com/zh/marketplace/obtain-topological-relationship">阿里云 - 获取拓扑关系</a>
12 */ 13 */
13 @Data 14 @Data
14 public class IotDeviceTopoRespDTO { 15 public class IotDeviceTopoRespDTO {
15 16
  17 + // TODO @AI:应该是数组;IotDeviceIdentify
16 /** 18 /**
17 * 子设备 ProductKey 19 * 子设备 ProductKey
18 */ 20 */
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
@@ -63,50 +63,4 @@ public final class IotMqttTopicUtils { @@ -63,50 +63,4 @@ public final class IotMqttTopicUtils {
63 return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; 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 -  
112 } 66 }
113 \ No newline at end of file 67 \ No newline at end of file