diff --git a/cn-universal-admin/pom.xml b/cn-universal-admin/pom.xml index 6bb577f929d92862cd10331f5291a029b22c4a92..4e137091732b474e9e8ba8f7c2e4b3f15271a989 100644 --- a/cn-universal-admin/pom.xml +++ b/cn-universal-admin/pom.xml @@ -114,7 +114,12 @@ ${project.version} compile - + + cn.universal.iot + cn-universal-notice + ${project.version} + compile + \ No newline at end of file diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java deleted file mode 100644 index 8066702996e173570cfffba6721dae6b48dec9cd..0000000000000000000000000000000000000000 --- a/cn-universal-admin/src/main/java/cn/universal/admin/network/doc/NetworkApiDoc.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * - * Copyright (c) 2025, IoT-Universal. All Rights Reserved. - * - * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 - * @Author: Aleo - * @Email: wo8335224@gmail.com - * @Wechat: outlookFil - * - * - */ - -package cn.universal.admin.network.doc; - -/** - * 网络组件API文档 - * - * @version 1.0 @Author Aleo - * @since 2025/1/20 - */ -public class NetworkApiDoc { - - /** - * 网络组件管理API文档 - * - *

基础路径: /admin/network - * - *

1. 查询网络组件列表 GET /admin/network/list 参数: - page: 页码 (可选,默认1) - size: 每页大小 (可选,默认10) - type: - * 网络类型 (可选,单个类型,如 TCP_CLIENT) - types: 网络类型列表 (可选,多个类型,如 ["MQTT_CLIENT", "MQTT_SERVER"]) - name: - * 网络组件名称 (可选,模糊查询) - productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选) - * - *

2. 查询网络组件列表(支持多个类型) GET /admin/network/list/multi-type 参数: - page: 页码 (可选,默认1) - size: 每页大小 - * (可选,默认10) - type: 网络类型 (可选,支持逗号分隔多个类型,如 "MQTT_CLIENT,MQTT_SERVER") - name: 网络组件名称 (可选,模糊查询) - - * productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选) - * - *

3. 根据ID查询网络组件 GET /admin/network/{id} 参数: - id: 网络组件ID (路径参数) - * - *

4. 新增网络组件 POST /admin/network 请求体: { "type": "TCP_SERVER", "unionId": "unique_id", - * "productKey": "product_key", "name": "网络组件名称", "description": "详细描述", "state": false, - * "configuration": "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}", "createUser": "admin" } - * - *

5. 修改网络组件 PUT /admin/network 请求体: { "id": 1, "type": "TCP_SERVER", "unionId": "unique_id", - * "productKey": "product_key", "name": "网络组件名称", "description": "详细描述", "state": false, - * "configuration": "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}" } - * - *

6. 删除网络组件 DELETE /admin/network/{id} 参数: - id: 网络组件ID (路径参数) - * - *

7. 批量删除网络组件 DELETE /admin/network/batch/{ids} 参数: - ids: 网络组件ID数组 (路径参数,逗号分隔) - * - *

8. 启动网络组件 POST /admin/network/start/{id} 参数: - id: 网络组件ID (路径参数) - * - *

9. 停止网络组件 POST /admin/network/stop/{id} 参数: - id: 网络组件ID (路径参数) - * - *

10. 重启网络组件 POST /admin/network/restart/{id} 参数: - id: 网络组件ID (路径参数) - * - *

11. 获取网络类型列表 GET /admin/network/types - * - *

12. 验证网络组件配置 POST /admin/network/validate 请求体: { "type": "TCP_SERVER", "unionId": - * "unique_id", "name": "网络组件名称", "configuration": - * "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}" } - * - *

响应格式: { "code": 200, "msg": "操作成功", "data": { // 具体数据 } } - * - *

网络类型说明: - TCP_CLIENT: TCP客户端 - TCP_SERVER: TCP服务端 - MQTT_CLIENT: MQTT客户端 - MQTT_SERVER: - * MQTT服务端 - * - *

配置示例: - * - *

TCP_SERVER配置: { "allIdleTime": 0, "allowInsert": false, "alwaysPreDecode": false, - * "decoderType": "STRING", "host": "0.0.0.0", "idleInterval": 0, "onlyCache": false, - * "parserConfiguration": { "byteOrderLittle": true, "delimited": "]", "delimitedMaxlength": 1024, - * "failFast": true }, "parserType": "DELIMITED", "port": 6372, "productKey": - * "630477f1cd68445d90b04653", "readerIdleTime": 360, "readTimeout": 0, "sendTimeout": 0, "ssl": - * false, "writerIdleTime": 0 } - * - *

MQTT_SERVER配置: { "autoReconnect": true, "cleanSession": true, "clientIdPrefix": "univ_cli_", - * "defaultQos": 1, "enabled": true, "host": "tcp://125.210.48.96:1883", "id": - * "6863896d75386b5c1e7e908c", "keepAliveInterval": 60, "password": "univiot@2025!", "productKey": - * "local", "ssl": false, "subscribeTopics": "$univ_cli/up/property/+/+", "username": "univ_cli", - * "connectTimeout": 30 } - */ -} diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java index 08dbe55cd0405ce1fc03d9da8ac688650785bf47..f91ba2cf2f1a77ef039b8e27a4f889bff4645060 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/INetworkService.java @@ -29,26 +29,16 @@ import java.util.List; */ public interface INetworkService { - boolean start(String productKey); - - boolean stop(String productKey); - - Object get(String productKey); - boolean del(String productKey); List selectNetworkList(NetworkBO bo); - NetworkVO getDetail(Long id); - List queryMileSightList(NetworkBO bo); R insertDevInstance(IoTDeviceBO devInstancebo); R deleteDevInstanceByIds(String[] ids); - void reloadTcpClient(String applicationId); - /** * 查询网络组件列表 * diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java index 5f4038c22ee6663b29e9554fc601ec0d430c6004..b3726ee2e3f29c78f54deb58efc98770b598b87e 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/service/impl/NetworkServiceImpl.java @@ -12,6 +12,7 @@ package cn.universal.admin.network.service.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; @@ -25,12 +26,15 @@ import cn.universal.common.domain.R; import cn.universal.common.enums.NetworkType; import cn.universal.common.exception.IoTException; import cn.universal.core.service.IoTDownlFactory; +import cn.universal.dm.device.service.protocol.ProtocolClusterService; +import cn.universal.dm.device.service.protocol.ProtocolServerManager; import cn.universal.mqtt.protocol.third.ThirdMQTTServerManager; import cn.universal.persistence.entity.IoTDevice; import cn.universal.persistence.entity.IoTProduct; import cn.universal.persistence.entity.IoTUser; import cn.universal.persistence.entity.Network; import cn.universal.persistence.entity.bo.IoTDeviceBO; +import cn.universal.persistence.entity.bo.IoTProductBO; import cn.universal.persistence.entity.bo.NetworkBO; import cn.universal.persistence.entity.vo.IoTProductVO; import cn.universal.persistence.entity.vo.NetworkVO; @@ -49,6 +53,7 @@ import java.util.Map; import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; @@ -63,11 +68,8 @@ import tk.mybatis.mapper.entity.Example; @Service public class NetworkServiceImpl implements INetworkService { - @Autowired - private NetworkMapper networkMapper; - @Resource - private NetworkMapper netWorkMapper; + private NetworkMapper networkMapper; @Resource private IoTDeviceMapper ioTDeviceMapper; @Resource @@ -75,42 +77,31 @@ public class NetworkServiceImpl implements INetworkService { @Resource private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; - + // 直接注入 TCP 和 MQTT 服务 + @Qualifier("tcpClusterService") @Autowired(required = false) - private ThirdMQTTServerManager mqttServerManager; - - @Override - public boolean start(String productKey) { - - return true; - } + private ProtocolClusterService tcpClusterService; - @Override - public boolean stop(String productKey) { + @Autowired(required = false) + private ProtocolServerManager tcpServerManager; - return true; - } + @Qualifier("udpClusterService") + @Autowired(required = false) + private ProtocolClusterService udpClusterService; - @Override - public Object get(String productKey) { - return null; - } + @Autowired(required = false) + private ThirdMQTTServerManager mqttServerManager; @Override public boolean del(String productKey) { Example ex = new Example(Network.class); ex.createCriteria().andEqualTo("productKey", productKey); - return netWorkMapper.deleteByExample(ex) > 0; + return networkMapper.deleteByExample(ex) > 0; } @Override public List selectNetworkList(NetworkBO bo) { - return netWorkMapper.selectNetworkListV1(bo); - } - - @Override - public NetworkVO getDetail(Long id) { - return netWorkMapper.selectById(id); + return networkMapper.selectNetworkListV1(bo); } @Override @@ -182,10 +173,6 @@ public class NetworkServiceImpl implements INetworkService { return R.ok("删除成功"); } - @Override - public void reloadTcpClient(String applicationId) { - } - @Override public List selectNetworkList(NetworkQuery query) { // 验证类型参数 @@ -214,9 +201,32 @@ public class NetworkServiceImpl implements INetworkService { String unionId = bo.getUnionId(); if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() .equals(type)) { + if (tcpClusterService != null && tcpServerManager != null) { + Object serverInstance = tcpServerManager.getServerInstance(productKey); + if (serverInstance != null && tcpServerManager.isAlive(serverInstance)) { + bo.setStateName("已启动"); + bo.setRunning(Boolean.TRUE); + } else { + bo.setStateName("未启动"); + bo.setRunning(Boolean.FALSE); + } + } else { + bo.setStateName("未启动"); + bo.setRunning(Boolean.FALSE); + } + // 查出哪些TCP-Server绑定了产品 + if (NetworkType.TCP_SERVER.getId().equals(type)) { + IoTProductBO ioTProductBO = ioTProductMapper.selectTcpProductsUseNetwork( + bo.getProductKey()); + bo.setBindTcpServerProductCount(ioTProductBO == null ? 0 : 1); + bo.setBindTcpServerProducts(ioTProductBO); + } } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() .equals(type)) { + List ioTProductBOS = ioTProductMapper.selectMqttProductsUseNetwork(unionId); + bo.setBindMqttServerProductCount(CollUtil.size(ioTProductBOS)); + bo.setBindMqttServerProducts(ioTProductBOS); if (mqttServerManager != null && mqttServerManager.isConnected(unionId)) { bo.setStateName("已启动"); bo.setRunning(Boolean.TRUE); @@ -224,6 +234,14 @@ public class NetworkServiceImpl implements INetworkService { bo.setStateName("未启动"); bo.setRunning(Boolean.FALSE); } + } else if (NetworkType.UDP.getId().equals(type)) { + if (udpClusterService != null && udpClusterService.isProductServerAlive(productKey)) { + bo.setStateName("已启动"); + bo.setRunning(Boolean.TRUE); + } else { + bo.setStateName("未启动"); + bo.setRunning(Boolean.FALSE); + } } else { bo.setStateName("未知"); bo.setRunning(Boolean.FALSE); @@ -264,6 +282,9 @@ public class NetworkServiceImpl implements INetworkService { case NetworkTypeConstants.MQTT_SERVER: vo.setTypeName(NetworkType.MQTT_SERVER.getDescription()); break; + case NetworkTypeConstants.UDP: + vo.setTypeName(NetworkType.UDP.getDescription()); + break; default: vo.setTypeName(network.getType()); } @@ -275,7 +296,19 @@ public class NetworkServiceImpl implements INetworkService { String unionId = network.getUnionId(); if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() .equals(type)) { - //TODO 开源版本无 + if (tcpClusterService != null && tcpServerManager != null) { + Object serverInstance = tcpServerManager.getServerInstance(productKey); + if (serverInstance != null && tcpServerManager.isAlive(serverInstance)) { + vo.setStateName("已启动"); + vo.setRunning(Boolean.TRUE); + } else { + vo.setStateName("未启动"); + vo.setRunning(Boolean.FALSE); + } + } else { + vo.setStateName("未启动"); + vo.setRunning(Boolean.FALSE); + } } else if (NetworkType.MQTT_CLIENT.getId().equals(type) || NetworkType.MQTT_SERVER.getId() .equals(type)) { @@ -286,6 +319,14 @@ public class NetworkServiceImpl implements INetworkService { vo.setStateName("未启动"); vo.setRunning(Boolean.FALSE); } + } else if (NetworkType.UDP.getId().equals(type)) { + if (udpClusterService != null && udpClusterService.isProductServerAlive(productKey)) { + vo.setStateName("已启动"); + vo.setRunning(Boolean.TRUE); + } else { + vo.setStateName("未启动"); + vo.setRunning(Boolean.FALSE); + } } else { vo.setStateName("未知"); vo.setRunning(Boolean.FALSE); @@ -347,11 +388,11 @@ public class NetworkServiceImpl implements INetworkService { network.setUnionId(IdUtil.objectId()); network.setState(false); - // 新增TCP_SERVER时,必须指定productKey,并且不能重复绑定 + // 新增TCP_SERVER/UDP时,必须指定productKey,并且不能重复绑定 if (NetworkType.TCP_SERVER.getId().equals(network.getType()) || NetworkType.TCP_CLIENT.getId() - .equals(network.getType())) { + .equals(network.getType()) || NetworkType.UDP.getId().equals(network.getType())) { if (StrUtil.isBlank(network.getProductKey())) { - throw new RuntimeException("TCP服务组件必须选择产品"); + throw new RuntimeException("TCP/UDP服务组件必须选择产品"); } network.setUnionId(network.getProductKey()); // 检查是否已被绑定 @@ -391,8 +432,8 @@ public class NetworkServiceImpl implements INetworkService { } String type = existNetwork.getType(); - if ((NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId() - .equals(type))) { + if ((NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId().equals(type) + || NetworkType.UDP.getId().equals(type))) { // 只有当unionId发生变化时,才做"已被产品关联且产品下有设备时禁止修改"的校验 String oldProductKey = existNetwork.getProductKey(); String networkProductKey = network.getProductKey(); @@ -423,9 +464,9 @@ public class NetworkServiceImpl implements INetworkService { throw new RuntimeException("网络组件不存在"); } String type = network.getType(); - // 只对TCP_SERVER和TCP_CLIENT做判断 - if (NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId() - .equals(type)) { + // 只对TCP_SERVER、TCP_CLIENT和UDP做判断 + if (NetworkType.TCP_SERVER.getId().equals(type) || NetworkType.TCP_CLIENT.getId().equals(type) + || NetworkType.UDP.getId().equals(type)) { // 查找是否有关联产品 String productKey = ioTProductMapper.findProductKeyByNetworkUnionId(network.getUnionId()); if (productKey != null) { @@ -466,6 +507,11 @@ public class NetworkServiceImpl implements INetworkService { if (StrUtil.isBlank(host)) { throw new RuntimeException("MQTT网络组件启动/重启时host不能为空"); } + } else if (NetworkType.UDP.getId().equals(type)) { + Integer port = config.getInt("port"); + if (port == null) { + throw new RuntimeException("UDP网络组件启动/重启时端口不能为空"); + } } try { // 根据网络类型调用相应的启动逻辑 @@ -527,6 +573,11 @@ public class NetworkServiceImpl implements INetworkService { if (StrUtil.isBlank(host)) { throw new RuntimeException("MQTT网络组件启动/重启时host不能为空"); } + } else if (NetworkType.UDP.getId().equals(type)) { + Integer port = config.getInt("port"); + if (port == null) { + throw new RuntimeException("UDP网络组件启动/重启时端口不能为空"); + } } try { // 根据网络类型调用相应的重启逻辑 @@ -548,7 +599,7 @@ public class NetworkServiceImpl implements INetworkService { @Override public List getNetworkTypes() { return Arrays.asList(NetworkType.TCP_CLIENT.getId(), NetworkType.TCP_SERVER.getId(), - NetworkType.MQTT_CLIENT.getId(), NetworkType.MQTT_SERVER.getId()); + NetworkType.MQTT_CLIENT.getId(), NetworkType.MQTT_SERVER.getId(), NetworkType.UDP.getId()); } /** @@ -608,9 +659,9 @@ public class NetworkServiceImpl implements INetworkService { try { JSONObject config = JSONUtil.parseObj(network.getConfiguration()); String type = network.getType(); - // TCP类型检查端口唯一性 - if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId() - .equals(type)) { + // TCP/UDP类型检查端口唯一性 + if (NetworkType.TCP_CLIENT.getId().equals(type) || NetworkType.TCP_SERVER.getId().equals(type) + || NetworkType.UDP.getId().equals(type)) { Integer port = config.getInt("port"); if (port != null) { List existingNetworks = networkMapper.selectTcpNetworkByPort(port, excludeId); @@ -663,6 +714,8 @@ public class NetworkServiceImpl implements INetworkService { case NetworkTypeConstants.MQTT_CLIENT: case NetworkTypeConstants.MQTT_SERVER: return startMqttNetwork(network); + case NetworkTypeConstants.UDP: + return startUdpNetwork(network); default: log.warn("不支持的网络类型: {}", type); return false; @@ -690,6 +743,8 @@ public class NetworkServiceImpl implements INetworkService { case NetworkTypeConstants.MQTT_CLIENT: case NetworkTypeConstants.MQTT_SERVER: return stopMqttNetwork(network); + case NetworkTypeConstants.UDP: + return stopUdpNetwork(network); default: log.warn("不支持的网络类型: {}", type); return false; @@ -708,7 +763,6 @@ public class NetworkServiceImpl implements INetworkService { */ private boolean restartNetworkByType(Network network) { String type = network.getType(); - try { switch (type) { case NetworkTypeConstants.TCP_CLIENT: @@ -717,6 +771,8 @@ public class NetworkServiceImpl implements INetworkService { case NetworkTypeConstants.MQTT_CLIENT: case NetworkTypeConstants.MQTT_SERVER: return restartMqttNetwork(network); + case NetworkTypeConstants.UDP: + return restartUdpNetwork(network); default: log.warn("不支持的网络类型: {}", type); return false; @@ -734,11 +790,28 @@ public class NetworkServiceImpl implements INetworkService { * @return 是否成功 */ private boolean startTcpNetwork(Network network) { + if (tcpClusterService == null) { + log.error("TCP服务管理器未注入,无法启动TCP网络组件: {}", network.getName()); + return false; + } + + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("TCP网络组件缺少productKey: {}", network.getName()); + return false; + } + try { - //TODO 开源版本无 - return true; + // 使用新的方法,从数据库加载配置并启动 + boolean success = tcpClusterService.start(productKey); + if (success) { + log.info("TCP网络组件启动成功: productKey={}", productKey); + } else { + log.error("TCP网络组件启动失败: productKey={}", productKey); + } + return success; } catch (Exception e) { - log.error("启动TCP网络组件失败: productKey={}", "", e); + log.error("启动TCP网络组件失败: productKey={}", productKey, e); return false; } } @@ -750,17 +823,27 @@ public class NetworkServiceImpl implements INetworkService { * @return 是否成功 */ private boolean stopTcpNetwork(Network network) { + if (tcpClusterService == null) { + log.error("TCP服务管理器未注入,无法停止TCP网络组件: {}", network.getName()); + return false; + } String productKey = network.getProductKey(); if (StrUtil.isBlank(productKey)) { log.error("TCP网络组件缺少productKey: {}", network.getName()); return false; } - try { - return true; + try { + boolean success = tcpClusterService.stop(productKey); + if (success) { + log.info("TCP网络组件停止成功: productKey={}", productKey); + } else { + log.error("TCP网络组件停止失败: productKey={}", productKey); + } + return success; } catch (Exception e) { - log.error("停止TCP网络组件失败: productKey={}", "", e); + log.error("停止TCP网络组件失败: productKey={}", productKey, e); return false; } } @@ -772,11 +855,27 @@ public class NetworkServiceImpl implements INetworkService { * @return 是否成功 */ private boolean restartTcpNetwork(Network network) { + if (tcpClusterService == null) { + log.error("TCP服务管理器未注入,无法重启TCP网络组件: {}", network.getName()); + return false; + } + + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("TCP网络组件缺少productKey: {}", network.getName()); + return false; + } try { - return true; + boolean success = tcpClusterService.restart(productKey); + if (success) { + log.info("TCP网络组件重启成功: productKey={}", productKey); + } else { + log.error("TCP网络组件重启失败: productKey={}", productKey); + } + return success; } catch (Exception e) { - log.error("重启TCP网络组件失败: productKey={}", "", e); + log.error("重启TCP网络组件失败: productKey={}", productKey, e); return false; } } @@ -881,4 +980,94 @@ public class NetworkServiceImpl implements INetworkService { public IoTDevice getDeviceById(String id) { return ioTDeviceMapper.selectDevInstanceById(id); } + + /** + * 启动UDP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean startUdpNetwork(Network network) { + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("UDP网络组件缺少productKey: {}", network.getName()); + return false; + } + + try { + boolean success; + if (udpClusterService != null) { + // 使用集群服务启动(会广播到其他节点) + success = udpClusterService.start(productKey); + log.info("UDP网络组件集群启动: productKey={}, success={}", productKey, success); + } else { + log.error("UDP集群服务未注入,无法启动UDP网络组件: {}", network.getName()); + return false; + } + return success; + } catch (Exception e) { + log.error("启动UDP网络组件失败: productKey={}", productKey, e); + return false; + } + } + + /** + * 停止UDP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean stopUdpNetwork(Network network) { + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("UDP网络组件缺少productKey: {}", network.getName()); + return false; + } + + try { + boolean success; + if (udpClusterService != null) { + // 使用集群服务停止(会广播到其他节点) + success = udpClusterService.stop(productKey); + log.info("UDP网络组件集群停止: productKey={}, success={}", productKey, success); + } else { + log.error("UDP集群服务未注入,无法停止UDP网络组件: {}", network.getName()); + return false; + } + return success; + } catch (Exception e) { + log.error("停止UDP网络组件失败: productKey={}", productKey, e); + return false; + } + } + + /** + * 重启UDP网络组件 + * + * @param network 网络组件 + * @return 是否成功 + */ + private boolean restartUdpNetwork(Network network) { + String productKey = network.getProductKey(); + if (StrUtil.isBlank(productKey)) { + log.error("UDP网络组件缺少productKey: {}", network.getName()); + return false; + } + + try { + boolean success; + if (udpClusterService != null) { + // 使用集群服务重启(会广播到其他节点) + success = udpClusterService.restart(productKey); + log.info("UDP网络组件集群重启: productKey={}, success={}", productKey, success); + } else { + log.error("UDP集群服务未注入,无法重启UDP网络组件: {}", network.getName()); + return false; + } + return success; + } catch (Exception e) { + log.error("重启UDP网络组件失败: productKey={}", productKey, e); + return false; + } + } } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java index dbeb0d0b15caeb65b4e1ba5f7227e2bf5ef092b6..b84fcbba0583737e4b769e833bf2db19966d603e 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/utils/NetworkTypeUtil.java @@ -29,7 +29,7 @@ public class NetworkTypeUtil { /** 支持的网络类型列表 */ public static final List SUPPORTED_TYPES = - Arrays.asList("TCP_CLIENT", "TCP_SERVER", "MQTT_CLIENT", "MQTT_SERVER"); + Arrays.asList("TCP_CLIENT", "TCP_SERVER", "MQTT_CLIENT", "MQTT_SERVER", "UDP"); /** * 解析网络类型字符串,支持逗号分隔的多个类型 diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java b/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java index c6e2bab2136a8f375e0d074dc849703f8e3cfc54..1cd031034edfbb96a3a9ace19c6d591cf889d6d2 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/network/web/NetworkController.java @@ -329,7 +329,7 @@ public class NetworkController extends BaseController { } /** 查询绑定指定星纵网关的设备 */ - @GetMapping("/milesight/list") + @GetMapping("/xz/gateway/list") public TableDataInfo queryMileSightList(NetworkBO bo) { startPage(); // 设置用户权限过滤 @@ -339,7 +339,7 @@ public class NetworkController extends BaseController { } /** 新增设备 */ - @PostMapping("/milesight/add") + @PostMapping("/xz/gateway/add") @Log(title = "新增设备", businessType = BusinessType.INSERT) public R add(@RequestBody IoTDeviceBO devInstancebo) { // 设置创建用户 @@ -356,7 +356,7 @@ public class NetworkController extends BaseController { } /** 删除设备 */ - @DeleteMapping("/milesight/{ids}") + @DeleteMapping("/xz/gateway/{ids}") @Log(title = "删除设备", businessType = BusinessType.DELETE) public R remove(@PathVariable String[] ids) { // 校验用户权限 @@ -385,7 +385,6 @@ public class NetworkController extends BaseController { public R reloadTcpClient(@PathVariable("applicationId") String applicationId) { // 校验用户权限 - 需要根据applicationId获取相关的信息进行权限校验 // 这里可能需要额外的业务逻辑来实现权限校验 - iNetworkService.reloadTcpClient(applicationId); return R.ok(); } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java index 0eeb24370038df6c0da5e347d296e6a3762c4f51..98035658123091e3dc4d7350b523d31116201fef 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTDeviceService.java @@ -27,6 +27,7 @@ import cn.universal.persistence.entity.vo.IoTDeviceModelVO; import cn.universal.persistence.entity.vo.IoTDeviceVO; import com.github.pagehelper.Page; import java.util.List; +import java.util.Map; /** * 设备Service接口 @@ -147,9 +148,15 @@ public interface IIoTDeviceService { List selectGwIotDevices(IoTGwDeviceBO bo); - IoTDevice selectDeviceByDeviceId(String deviceId); + IoTDevice selectIoTDevice(String productKey, String deviceId); List selectDevInstanceListWithTags(IoTDeviceBO ioTDeviceBO, IoTUser iotUser); List selectProductListInBatchFunction(Long applicationId, IoTUser iotUser); + + /** 新:按第三方平台过滤设备列表(不影响旧接口) */ + List selectDevInstanceListByPlatform( + IoTDevice ioTDevice, IoTUser iotUser, String thirdPlatform); + + } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java index aa2317f8bb36718ed84c333e3f968d2494a6ad33..b6b5078387e88937543613f4cca31cab983d09d1 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/IIoTProductService.java @@ -178,7 +178,9 @@ public interface IIoTProductService { Map countDevNumberByProductKey(String unionId); - AjaxResult selectDevProductByKey(String key); + AjaxResult selectIoTProductByKey(String key); + + AjaxResult> selectGatewaySubProductsByKey(String gwProductKey); int updateDevProductOtherConfig(String otherConfig); diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java index c922b2a4429bb8a3c76081e9fa992b81d38097e3..ad08cc725da1b3fbe66b1c5b442fa57587b9476d 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceGroupServiceImpl.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -84,7 +85,7 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe // 生成设备分组树 List roots = collect.get(0L); ioTDeviceGroupVO.setChildren(roots); - buildGroupTree(roots, collect); + groupTree(roots, collect); return groupVos; } @@ -192,12 +193,23 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe } /** - * 绑定设备到分组 + * 绑定设备到分组 iot_dev_action * * @param devGroup * @return */ @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) public AjaxResult bindDevToGroup(IoTDeviceGroupBO devGroup) { // 判断分组是否存在 Long groupId = devGroup.getId(); @@ -292,6 +304,17 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe */ @Override @Transactional + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) public AjaxResult unBindDevByGroupId(String groupId, String[] devId) { for (int i = 0; i < devId.length; i++) { ioTDeviceTagsMapper.delete(IoTDeviceTags.builder().iotId(devId[i]).value(groupId).build()); @@ -299,14 +322,8 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe return AjaxResult.success(); } - /** - * 生成设备分组树 - * - * @param ioTDeviceGroupVO - * @param groups - * @return - */ - private void buildGroupTree( + /** 生成设备分组列表 */ + private void groupTree( List ioTDeviceGroupVO, Map> groups) { if (ioTDeviceGroupVO == null) { return; @@ -315,7 +332,7 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe if (ioTDeviceGroupVO.get(i).getHasChild() == 1) { List devGroups = groups.get(ioTDeviceGroupVO.get(i).getId()); ioTDeviceGroupVO.get(i).setChildren(devGroups); - buildGroupTree(devGroups, groups); + groupTree(devGroups, groups); } } return; diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java index b907e9db930d185e5acfcf490328a58b848753e7..a695f6738d90d3a550471ffa5534c18d9019d858 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTDeviceServiceImpl.java @@ -45,7 +45,6 @@ import cn.universal.persistence.mapper.IoTDeviceProtocolMapper; import cn.universal.persistence.mapper.IoTDeviceShadowMapper; import cn.universal.persistence.mapper.IoTDeviceTagsMapper; import cn.universal.persistence.mapper.IoTProductMapper; -import cn.universal.persistence.query.IoTDeviceQuery; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import jakarta.annotation.Resource; @@ -149,6 +148,13 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS ioTDevice, iotUser.isAdmin() ? null : iotUser.getUnionId()); } + @Override + public List selectDevInstanceListByPlatform( + IoTDevice ioTDevice, IoTUser iotUser, String thirdPlatform) { + return ioTDeviceMapper.selectDevInstanceListByPlatform( + ioTDevice, thirdPlatform, iotUser.isAdmin() ? null : iotUser.getUnionId()); + } + /** * 查询设备列表 * @@ -229,8 +235,8 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS } @Override - public IoTDevice selectDeviceByDeviceId(String deviceId) { - return ioTDeviceMapper.getOneByDeviceId(IoTDeviceQuery.builder().deviceId(deviceId).build()); + public IoTDevice selectIoTDevice(String productKey, String deviceId) { + return ioTDeviceMapper.selectIoTDevice(productKey, deviceId); } @Override @@ -270,11 +276,12 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS if (ioTDeviceBO.getOtherParams().containsKey("deviceKey")) { ioTDeviceBO.setSecretKey(ioTDeviceBO.getOtherParams().get("deviceKey").toString()); } + if (cn.universal.common.utils.StringUtils.isEmpty(ioTDeviceBO.getLatitude())) { - ioTDeviceBO.setLatitude("30.4484801"); + ioTDeviceBO.setLatitude("44.61081"); } if (cn.universal.common.utils.StringUtils.isEmpty(ioTDeviceBO.getLongitude())) { - ioTDeviceBO.setLongitude("120.2922654"); + ioTDeviceBO.setLongitude("80.990372"); } if (StrUtil.isNotBlank(deviceId)) { ioTDeviceBO.setGwProductKey(deviceId); @@ -333,6 +340,17 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS } @Override + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) public int bindApp(String ids, String appUniqueId) { String[] a = ids.split(","); return ioTDeviceMapper.bindApp(a, appUniqueId); @@ -362,7 +380,7 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS downRequest.set("detail", devInstancebo.getDetail()); // 网关产品ProductKey downRequest.set("gwProductKey", devInstancebo.getGwProductKey()); - downRequest.set("gwDeviceId", devInstancebo.getExtDeviceId()); + downRequest.set("gwDeviceId", devInstancebo.getGwDeviceId()); downRequest.set("extDeviceId", devInstancebo.getExtDeviceId()); JSONObject ob = new JSONObject(); // 设备实例名称 @@ -506,9 +524,7 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS public R iotFunctionsDown(String productKey, String downRequest) { JSONObject jsonObject = JSONUtil.parseObj(downRequest); String deviceId = jsonObject.getStr("deviceId"); - IoTDevice ioTDeviceBo = - ioTDeviceMapper.getOneByDeviceId( - IoTDeviceQuery.builder().deviceId(deviceId).productKey(productKey).build()); + IoTDevice ioTDeviceBo = ioTDeviceMapper.selectIoTDevice(productKey, deviceId); if (ioTDeviceBo == null) { return R.error("设备不存在"); } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java index d471513fd371b77d70659128adedd948761ef3ec..72c2323b38112cb68f63d783821e233c5a227d1e 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/service/impl/IoTProductServiceImpl.java @@ -25,10 +25,12 @@ import cn.hutool.http.HttpStatus; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; +import cn.universal.dm.device.service.protocol.ProtocolClusterService; import cn.universal.admin.common.service.BaseServiceImpl; import cn.universal.admin.common.utils.SecurityUtils; import cn.universal.admin.platform.service.IIoTProductService; import cn.universal.admin.system.service.IoTDeviceProtocolService; +import cn.universal.cache.annotation.MultiLevelCacheable; import cn.universal.common.constant.IoTConstant; import cn.universal.common.constant.IoTConstant.DeviceNode; import cn.universal.common.constant.IoTConstant.ProductFlushType; @@ -192,31 +194,54 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc } @Override - // @Cacheable(cacheNames = "iot_dev_product_list", unless = "#result == null", keyGenerator = - // "redisKeyGenerate") + // 分页查询不使用缓存,因为分页参数会导致缓存键不同,效率低下 public List selectDevProductV4List(IoTProductQuery ioTProductQuery) { - Page page = - PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize()); + // 设置分页参数 + PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize()); + + // 优化:一次SQL查询获取产品列表和设备数量,避免N+1查询 List devProductVOS = ioTProductMapper.selectDevProductV3List(ioTProductQuery); - List results = - ioTProductMapper.countDevNumberByProductKey(ioTProductQuery.getCreatorId()); - if (CollUtil.isNotEmpty(results)) { - final Map collect = - results.stream() - .collect(Collectors.toMap(IoTProductVO::getProductKey, IoTProductVO::getDevNum)); + + // 处理图片URL + if (CollUtil.isNotEmpty(devProductVOS)) { for (IoTProductVO devProduct : devProductVOS) { - devProduct.setDevNum(collect.getOrDefault(devProduct.getProductKey(), 0)); if (JSONUtil.isTypeJSON(devProduct.getPhotoUrl())) { JSONObject photo = JSONUtil.parseObj(devProduct.getPhotoUrl()); devProduct.setImage(photo.getStr("img", "")); } } } - // 按设备数量倒序排序 - // if (CollUtil.isNotEmpty(devProductVOS)) { - // devProductVOS.sort(Comparator.comparingInt(IoTProductVO::getDevNum).reversed()); - // } - return page; + + return devProductVOS; + } + + /** 获取不分页的产品列表(用于缓存) 缓存基础产品数据,不包含分页信息 */ + @MultiLevelCacheable( + cacheNames = "selectDevProductV4ListNoPage", + keyGenerator = "redisKeyGenerate", + unless = "#result == null", + l1Expire = 30) + public List selectDevProductV4ListNoPage(IoTProductQuery ioTProductQuery) { + // 创建不分页的查询条件 + IoTProductQuery noPageQuery = new IoTProductQuery(); + BeanUtil.copyProperties(ioTProductQuery, noPageQuery); + noPageQuery.setPageNum(1); + noPageQuery.setPageSize(Integer.MAX_VALUE); // 获取所有数据 + + // 优化:一次SQL查询获取产品列表和设备数量,避免N+1查询 + List devProductVOS = ioTProductMapper.selectDevProductV3List(noPageQuery); + + // 处理图片URL + if (CollUtil.isNotEmpty(devProductVOS)) { + for (IoTProductVO devProduct : devProductVOS) { + if (JSONUtil.isTypeJSON(devProduct.getPhotoUrl())) { + JSONObject photo = JSONUtil.parseObj(devProduct.getPhotoUrl()); + devProduct.setImage(photo.getStr("img", "")); + } + } + } + + return devProductVOS; } @Override @@ -237,7 +262,9 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc cacheNames = { "iot_all_dev_product_list", "iot_dev_product_list", - "selectAllEnableNetworkProductKey" + "selectAllEnableNetworkProductKey", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" }, allEntries = true) public int insertDevProduct(IoTProduct ioTProduct) { @@ -257,6 +284,9 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc if (StrUtil.isBlank(ioTProduct.getProductSecret())) { ioTProduct.setProductSecret(IdUtil.fastSimpleUUID()); } + if (StrUtil.isBlank(ioTProduct.getConfiguration())) { + ioTProduct.setConfiguration(buildProductCfg(ioTProduct)); + } ioTProduct.setProductKey(productKey); ioTProduct.setMetadata(defaultMetadata); ioTProduct.setThirdConfiguration(thirdConfig); @@ -268,9 +298,30 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc return i; } + /** 默认创建mqtt设置自动注册 */ + private String buildProductCfg(IoTProduct ioTProduct) { + JSONObject cfg = new JSONObject(); + boolean isMqtt = false; + String thirdPlatform = ioTProduct == null ? null : ioTProduct.getThirdPlatform(); + if (thirdPlatform == null) { + return JSONUtil.toJsonStr(cfg); + } + if (ProtocolModule.tcp.name().equalsIgnoreCase(thirdPlatform) + || ProtocolModule.mqtt.name().equalsIgnoreCase(thirdPlatform) + || ProtocolModule.udp.name().equalsIgnoreCase(thirdPlatform)) { + cfg.set(IoTConstant.ALLOW_INSERT, true); + } + return JSONUtil.toJsonStr(cfg); + } + @Override @CacheEvict( - cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list"}, + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" + }, allEntries = true) public int insertList(List ioTProductList) { int i = ioTProductMapper.insertList(ioTProductList); @@ -280,7 +331,13 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc /** 产品协议导入 */ @Override @CacheEvict( - cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "supportMQTTNetwork", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" + }, allEntries = true) public String importProduct(List productImportBos, String unionId) { Long timeMillis = System.currentTimeMillis(); @@ -357,7 +414,9 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc devProduct -> { R r = IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", devProduct.getThirdDownRequest()); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + devProduct.getThirdDownRequest()); if (!R.SUCCESS.equals(r.getCode())) { failedList.add(devProduct); } else { @@ -395,7 +454,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc ioTProductAction.create(null, unionId); String result = String.format( - "导入普通产品总数:%s 个,成功:%s个;导入电信产品总数:%s 个,成功:%s 个;导入协议总数:%s 个,成功:%s 个。", + "导入普通产品总数:%s 个,成功:%s个;导入电信ctwing产品总数:%s 个,成功:%s 个;导入协议总数:%s 个,成功:%s 个。", customProductList.size() + duplicateNum.get(), customSuccess, ctwingProductList.size(), @@ -413,7 +472,13 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc */ @Override @CacheEvict( - cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "supportMQTTNetwork", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" + }, allEntries = true) public int updateDevProduct(IoTProduct ioTProduct) { IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); @@ -423,7 +488,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc String appUnionId = iotUser.getUnionId(); IoTProduct pro = ioTProductMapper.selectByPrimaryKey(ioTProduct.getId()); if (!appUnionId.equals(pro.getCreatorId()) && !iotUser.isAdmin()) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("没有产品权限"); } if (Objects.nonNull(ioTProduct.getClassifiedId())) { IoTProductSort ioTProductSort = @@ -432,7 +497,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc } int count = ioTProductMapper.updateDevProduct(ioTProduct); - log.info("修改产品记录数={},修改人={}", count, appUnionId); + log.info("updateDevProduct,操作成功={},userId={}", count, appUnionId); ioTProductAction.update(ioTProduct); return count; } @@ -445,7 +510,13 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc */ @Override @CacheEvict( - cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "supportMQTTNetwork", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" + }, allEntries = true) public int deleteDevProductByIds(String[] ids) { String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); @@ -453,7 +524,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc for (String id : ids) { IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(id); if (!appUnionId.equals(ioTProduct.getCreatorId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } count = +deleteDevProductById(id); } @@ -468,7 +539,13 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc */ @Override @CacheEvict( - cacheNames = {"iot_all_dev_product_list", "iot_dev_product_list", "supportMQTTNetwork"}, + cacheNames = { + "iot_all_dev_product_list", + "iot_dev_product_list", + "supportMQTTNetwork", + "selectDevProductV4ListNoPage", + "countDevNumberByProductKey" + }, allEntries = true) public int deleteDevProductById(String id) { String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); @@ -477,7 +554,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc throw new IoTException("产品不存在"); } if (!appUnionId.equals(ioTProduct.getCreatorId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } IoTDevice ioTDevice = IoTDevice.builder().productKey(ioTProduct.getProductKey()).build(); int count = ioTDeviceMapper.selectCount(ioTDevice); @@ -507,7 +584,8 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc "iot_dev_action", "selectDevCount", "supportMQTTNetwork", - "iot_dev_product_list" + "iot_dev_product_list", + "getProductConfiguration" }, allEntries = true) public int updateDevProductConfig(IoTProductVO devProduct) { @@ -518,7 +596,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc throw new IoTException("产品不存在"); } if (!appUnionId.equals(product.getCreatorId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } // 获取新的产品配置信息 JSONObject newConfig = JSONUtil.parseObj(devProduct.getConfiguration()); @@ -802,7 +880,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); IoTProduct product = ioTProductMapper.getProductByProductKey(ioTProductBO.getProductKey()); if (!appUnionId.equals(product.getCreatorId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } ioTProductBO.setPath(getPath(ioTProductBO)); int delCount = ioTProductMapper.deleteMetadata(ioTProductBO); @@ -840,7 +918,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId(); IoTProduct product = ioTProductMapper.getProductByProductKey(ioTProductBO.getProductKey()); if (!appUnionId.equals(product.getCreatorId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } ioTProductBO.beanToJson(); // if(getMetadata(ioTProductBO)!=null){ @@ -884,19 +962,41 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc } @Override + @MultiLevelCacheable( + cacheNames = "countDevNumberByProductKey", + keyGenerator = "redisKeyGenerate", + unless = "#result == null", + l1Expire = 30) public Map countDevNumberByProductKey(String unionId) { List results = ioTProductMapper.countDevNumberByProductKey(unionId); return results.stream() .collect(Collectors.toMap(IoTProductVO::getProductKey, IoTProductVO::getDevNum)); } + /** 设备数量变化时清除相关缓存 当设备新增、删除、修改时调用此方法 */ + @CacheEvict( + cacheNames = {"countDevNumberByProductKey", "selectDevProductV4ListNoPage"}, + allEntries = true) + public void evictDeviceCountCache() { + // 此方法用于手动清除设备数量相关缓存 + // 当设备数据发生变化时调用 + } + @Override - public AjaxResult selectDevProductByKey(String key) { + public AjaxResult selectIoTProductByKey(String key) { IoTProduct ioTProduct = new IoTProduct(); ioTProduct.setProductKey(key); return AjaxResult.success(ioTProductMapper.selectOne(ioTProduct)); } + @Override + public AjaxResult> selectGatewaySubProductsByKey(String gwProductKey) { + IoTProduct ioTProduct = new IoTProduct(); + ioTProduct.setGwProductKey(gwProductKey); + ioTProduct.setState((byte) 0); + return AjaxResult.success(ioTProductMapper.select(ioTProduct)); + } + /** 修改产品其他配置信息 */ @Override @CacheEvict( @@ -917,7 +1017,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc IoTProduct product = ioTProductMapper.selectDevProductById(newConfig.getStr("productId")); IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId()); if (!product.getCreatorId().equals(iotUser.getUnionId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("无产品权限"); } // 获取第三方配置 String thirdConfiguration = product.getThirdConfiguration(); @@ -1042,7 +1142,20 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc @Override public void flushNettyServer(String config, String productKey, TcpFlushType type) { - + JSONObject object = new JSONObject(); + object.set("type", ProductFlushType.server.name()); + object.set("productKey", productKey); + object.set("customType", type.name()); + eventPublisher.publishEvent(EventTopics.PRODUCT_CONFIG_UPDATED, object); + // 使用ProtocolClusterService进行集群操作 + ProtocolClusterService tcpClusterService = SpringUtil.getBean(ProtocolClusterService.class); + if (tcpClusterService != null) { + switch (type) { + case start -> tcpClusterService.start(productKey); + case reload -> tcpClusterService.restart(productKey); + case close -> tcpClusterService.stop(productKey); + } + } } @Override diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java index 84efb189975539b0042897b014211b287fdd933a..d9eb51276dea878245ee9283f8163a29f7ff57e2 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTDeviceController.java @@ -90,6 +90,18 @@ public class IoTDeviceController extends BaseController { return getDataTable(list); } + /** 新:按第三方平台过滤的设备列表(用于萤石/乐橙) */ + @GetMapping("/listByPlatform") + public TableDataInfo listByPlatform( + IoTDevice ioTDevice, + @RequestParam(value = "thirdPlatform", required = false) String thirdPlatform) { + IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); + startPage(); + List list = + iIotDeviceService.selectDevInstanceListByPlatform(ioTDevice, iotUser, thirdPlatform); + return getDataTable(list); + } + /** 查询分组未绑定设备列表 */ @GetMapping("/list/{groupId}") public TableDataInfo list(@PathVariable("groupId") String groupId, IoTDevice ioTDevice) { @@ -152,27 +164,18 @@ public class IoTDeviceController extends BaseController { /** 获取设备详细信息 */ @GetMapping(value = "/{id}") - public AjaxResult getInfo(@PathVariable("id") String id) { - + public AjaxResult getIotDeviceInfo(@PathVariable("id") String id) { IoTDevice ioTDevice = iIotDeviceService.selectDevInstanceById(id); - // if (ObjectUtil.isEmpty(ioTDevice)) { - // throw new IoTException("设备不存在"); - // } - // IoTUser iotUser = checkParent(SecurityUtils.getUnionId()); - // if (!iotUser.isAdmin() && !ioTDevice.getCreatorId() - // .equals(iotUser.getUnionId())) { - // throw new IoTException("你无权操作"); - // } - // 是否有编解码插件 int count = ioTDeviceProtocolService.countProtocol(ioTDevice.getProductKey()); IoTDeviceBO ioTDeviceBO = new IoTDeviceBO(); BeanUtil.copyProperties(ioTDevice, ioTDeviceBO); // 查询网关产品信息 if (StrUtil.isNotBlank(ioTDeviceBO.getGwProductKey())) { IoTDevice parentDevice = - iIotDeviceService.selectDeviceByDeviceId(ioTDevice.getGwProductKey()); + iIotDeviceService.selectIoTDevice( + ioTDeviceBO.getGwProductKey(), ioTDevice.getExtDeviceId()); if (ObjectUtil.isNotNull(parentDevice)) { - ioTDeviceBO.setParentName( + ioTDeviceBO.setGwDeviceInfo( parentDevice.getDeviceId() + "(" + parentDevice.getDeviceName() + ")"); } } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java index bab3f268f7673d70f4b2a2554cf1e825cf3998c0..b13b752e6962ea41ba21e20e99df5923e06e1b44 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTProductController.java @@ -20,16 +20,22 @@ import cn.universal.admin.common.annotation.Log; import cn.universal.admin.common.enums.BusinessType; import cn.universal.admin.common.utils.SecurityUtils; import cn.universal.admin.network.service.INetworkService; +import cn.universal.admin.platform.dto.ConnectionInfoDTO; +import cn.universal.admin.platform.service.ConnectionInfoService; import cn.universal.admin.platform.service.IIoTDeviceService; import cn.universal.admin.platform.service.IIoTProductService; import cn.universal.admin.platform.service.impl.IoTProductServiceImpl; import cn.universal.admin.system.service.ISysDictTypeService; import cn.universal.admin.system.service.IoTDeviceProtocolService; import cn.universal.admin.system.web.BaseController; +import cn.universal.common.constant.IoTConstant; import cn.universal.common.constant.IoTConstant.ProtocolModule; import cn.universal.common.constant.IoTConstant.TcpFlushType; +import cn.universal.common.event.EventTopics; +import cn.universal.common.event.processer.EventPublisher; import cn.universal.common.exception.IoTException; import cn.universal.core.service.IoTDownlFactory; +import cn.universal.ossm.service.ISysOssService; import cn.universal.persistence.entity.IoTDevice; import cn.universal.persistence.entity.IoTDeviceEvents; import cn.universal.persistence.entity.IoTDeviceFunction; @@ -55,7 +61,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -77,29 +82,20 @@ import org.springframework.web.multipart.MultipartFile; @RequestMapping("/admin/v1/product") public class IoTProductController extends BaseController { - @Autowired - private IIoTProductService devProductService; - @Autowired - private IIoTDeviceService iIotDeviceService; - @Autowired - private ISysDictTypeService dictTypeService; - @Resource - private INetworkService iNetworkService; - @Resource - private IoTDeviceProtocolService ioTDeviceProtocolService; - @Resource - private IoTProductMapper ioTProductMapper; - @Autowired - private IoTProductServiceImpl ioTProductServiceImpl; - @Autowired - private ApplicationEventPublisher eventPublisher; - - /** - * 查询设备产品列表 - */ + @Autowired private IIoTProductService devProductService; + @Autowired private IIoTDeviceService iIotDeviceService; + @Autowired private ISysDictTypeService dictTypeService; + @Resource private INetworkService iNetworkService; + @Resource private IoTDeviceProtocolService ioTDeviceProtocolService; + @Resource private IoTProductMapper ioTProductMapper; + @Autowired private IoTProductServiceImpl ioTProductServiceImpl; + @Autowired private EventPublisher eventPublisher; + @Autowired private ISysOssService iSysOssService; + @Autowired private ConnectionInfoService connectionInfoService; + + /** 查询设备产品列表 */ @GetMapping("/list") public TableDataInfo list(IoTProductQuery query) { - // 只能看自己设备,除非是特殊设置和超管 IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); if (!iotUser.viewAllProduct()) { query.setSelf(true); @@ -118,20 +114,21 @@ public class IoTProductController extends BaseController { return getDataTable(list); } - /** - * 获取产品详情通过 ProductKey - */ + /** 获取产品详情通过 ProductKey */ @GetMapping("/get/pro/{key}") public AjaxResult getProByKey(@PathVariable("key") String key) { - return devProductService.selectDevProductByKey(key); + return devProductService.selectIoTProductByKey(key); } - /** - * 查询设备产品列表 - */ + /** 获取网关绑定的子产品列表 */ + @GetMapping("/gateway/subProducts/{key}") + public AjaxResult> gatewaySubProducts(@PathVariable("key") String gwProductKey) { + return devProductService.selectGatewaySubProductsByKey(gwProductKey); + } + + /** 查询设备产品列表 */ @GetMapping("/v2/list") public TableDataInfo v2List(IoTProductQuery query) { - // 只能看自己设备,除非是特殊设置和超管 IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); if (!iotUser.viewAllProduct()) { query.setSelf(true); @@ -150,12 +147,10 @@ public class IoTProductController extends BaseController { return getDataTable(list); } - /** - * 查询设备产品列表 - */ + /** 查询设备产品列表 */ @GetMapping("/v3/list") public TableDataInfo v3List(IoTProductQuery query) { - // 只能看自己设备,除非是特殊设置和超管 + // IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); if (!iotUser.viewAllProduct()) { query.setSelf(true); @@ -175,12 +170,10 @@ public class IoTProductController extends BaseController { return getDataTable(list); } - /** - * 查询设备产品列表 - */ + /** 查询设备产品列表 */ @GetMapping("/v4/list") public TableDataInfo v4List(IoTProductQuery query) { - // 只能看自己设备,除非是特殊设置和超管 + // IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); if (!iotUser.viewAllProduct()) { query.setSelf(true); @@ -200,70 +193,47 @@ public class IoTProductController extends BaseController { return getDataTable(list); } - /** - * 查询所有设备产品列表 - */ + /** 查询所有设备产品列表 */ @GetMapping("/all/list") public TableDataInfo v3List() { List list = devProductService.selectDevProductAllList(); return getDataTable(list); } - /** - * 物模型新增内容 - */ + /** 物模型新增内容 */ @PostMapping("/metadata/add") @Log(title = "物模型新增内容", businessType = BusinessType.INSERT) public AjaxResult metadataAdd(@RequestBody IoTProductBO ioTProductBO) { return toAjax(devProductService.insertMetadata(ioTProductBO)); } - /** - * 物模型修改内容 - */ + /** 物模型修改内容 */ @PostMapping("/metadata/update") @Log(title = "物模型修改内容", businessType = BusinessType.UPDATE) public AjaxResult metadataUpdate(@RequestBody IoTProductBO ioTProductBO) { return toAjax(devProductService.updateMetadata(ioTProductBO)); } - /** - * 物模型删除内容 - */ + /** 物模型删除内容 */ @PostMapping("/metadata/delete") @Log(title = "物模型删除内容", businessType = BusinessType.DELETE) public AjaxResult metadataDelete(@RequestBody IoTProductBO ioTProductBO) { return toAjax(devProductService.deleteMetadata(ioTProductBO)); } - /** - * 物模型查询内容 - */ + /** 物模型查询内容 */ @PostMapping("/metadata/get") public AjaxResult metadataGet(@RequestBody IoTProductBO ioTProductBO) { return AjaxResult.success(devProductService.getMetadata(ioTProductBO)); } - /** 导出设备产品列表 */ - // @PostMapping("/export") - // public void export(HttpServletResponse response, IoTProduct ioTProduct) - // { - // List list = devProductService.selectDevProductList(ioTProduct); - // ExcelUtil util = new ExcelUtil(IoTProduct.class); - // util.exportExcel(response, list, "设备产品数据"); - // } - - /** - * 获取设备产品详细信息 - */ + /** 获取设备产品详细信息 */ @GetMapping(value = "/{id}") public AjaxResult getInfo(@PathVariable("id") String id) { return AjaxResult.success(devProductService.selectDevProductById(id)); } - /** - * 查询可绑定网关列表 - */ + /** 查询可绑定网关列表 */ @Operation(summary = "查询可绑定网关产品列表") @GetMapping("/gateway/list") public AjaxResult> getGatewayList() { @@ -271,20 +241,16 @@ public class IoTProductController extends BaseController { return AjaxResult.success(list); } - /** - * 查询可绑定网关子设备产品列表 - */ + /** 查询可绑定网关子设备产品列表 */ @Operation(summary = "查询可绑定网关子设备产品列表") - @GetMapping("/subdevice/{productKey}") + @GetMapping("/sub/device/{productKey}") public AjaxResult> getGatewaySubDeviceList( @PathVariable("productKey") String productKey) { List list = devProductService.getGatewaySubDeviceList(productKey); return AjaxResult.success(list); } - /** - * 查询电信公共产品列表 - */ + /** 查询电信公共产品列表 */ @PostMapping(value = "/ctwing/pubpro") public AjaxResult getCtwingPubPro(@RequestBody String downRequest) { String creatorId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); @@ -299,23 +265,27 @@ public class IoTProductController extends BaseController { downProRequest.set("data", data); return AjaxResult.success( IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + downProRequest.toString())); } - /** - * 新增设备产品 - */ + /** 新增设备产品 */ @PostMapping @Log(title = "新增产品", businessType = BusinessType.INSERT) public AjaxResult add(@RequestBody IoTProduct ioTProduct) { String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); ioTProduct.setCreatorId(unionId); + // 发布产品配置更新事件 + Map eventData = new HashMap<>(); + eventData.put("type", "product"); + eventData.put("productKey", ioTProduct.getProductKey()); + eventData.put("operation", "ADD"); + eventPublisher.publishEvent(EventTopics.PRODUCT_CONFIG_UPDATED, eventData); return toAjax(devProductService.insertDevProduct(ioTProduct)); } - /** - * 新增设备产品NB - */ + /** 新增设备产品NB */ @PostMapping(value = "/nb") @Log(title = "新增产品NB", businessType = BusinessType.INSERT) public AjaxResult addNb(@RequestBody String downRequest) { @@ -343,12 +313,12 @@ public class IoTProductController extends BaseController { downProRequest.set("data", data); return AjaxResult.success( IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + downProRequest.toString())); } - /** - * 新增电信公共产品 - */ + /** 新增电信公共产品 */ @PostMapping(value = "/ctwing/pubproadd") @Log(title = "新增电信公共产品", businessType = BusinessType.INSERT) public AjaxResult addCtwingPubPro(@RequestBody String downRequest) { @@ -367,12 +337,12 @@ public class IoTProductController extends BaseController { downProRequest.set("data", data); return AjaxResult.success( IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + downProRequest.toString())); } - /** - * 修改设备产品 - */ + /** 修改设备产品 */ @PostMapping(value = "/updateNetworkUnionId") @Log(title = "修改网络组件", businessType = BusinessType.UPDATE) public AjaxResult editNetwork(@RequestBody IoTProductBO ioTProductBO) { @@ -387,9 +357,7 @@ public class IoTProductController extends BaseController { ioTProductBO.getId(), ioTProductBO.getNetworkUnionId())); } - /** - * 子设备绑定网关 - */ + /** 子设备绑定网关 */ @Operation(summary = "子设备绑定网关") @PutMapping("/gateway") @Log(title = "子设备绑定网关", businessType = BusinessType.UPDATE) @@ -407,9 +375,7 @@ public class IoTProductController extends BaseController { return toAjax(devProductService.updateDevProduct(productVO)); } - /** - * 修改设备产品 - */ + /** 修改设备产品 */ @PutMapping @Log(title = "修改设备产品", businessType = BusinessType.UPDATE) public AjaxResult edit(@RequestBody IoTProductBO ioTProductBO) { @@ -434,42 +400,45 @@ public class IoTProductController extends BaseController { downProRequest.set("productKey", ioTProductBO.getProductKey()); return AjaxResult.success( IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + downProRequest.toString())); } IoTProduct ioTProduct = BeanUtil.toBean(ioTProductBO, IoTProduct.class); - return toAjax(devProductService.updateDevProduct(ioTProduct)); + int result = devProductService.updateDevProduct(ioTProduct); + if (result > 0) { + // 发布产品配置更新事件 + Map eventData = new HashMap<>(); + eventData.put("type", "product"); + eventData.put("productKey", ioTProduct.getProductKey()); + eventData.put("operation", "UPDATE"); + eventPublisher.publishEvent(EventTopics.PRODUCT_CONFIG_UPDATED, eventData); + } + return toAjax(result); } - /** - * 修改产品配置信息 - */ + /** 修改产品配置信息 */ @PutMapping("/config") @Log(title = "修改产品配置信息", businessType = BusinessType.UPDATE) public AjaxResult editConfig(@RequestBody IoTProductVO devProduct) { return toAjax(devProductService.updateDevProductConfig(devProduct)); } - /** - * 修改产品其他配置信息 - */ + /** 修改产品其他配置信息 */ @PutMapping("/otherConfig") @Log(title = "修改产品其他配置信息", businessType = BusinessType.UPDATE) public AjaxResult editOtherConfig(@RequestBody String otherConfig) { return toAjax(devProductService.updateDevProductOtherConfig(otherConfig)); } - /** - * 修改产品存储策略 - */ + /** 修改产品存储策略 */ @PutMapping("/storeConfig") @Log(title = "修改产品存储策略", businessType = BusinessType.UPDATE) public AjaxResult editOtherConfig(@RequestBody IoTProductVO devProduct) { return toAjax(devProductService.updateDevProductStoreConfig(devProduct)); } - /** - * 删除设备产品 - */ + /** 删除设备产品 */ @DeleteMapping("/{ids}") @Log(title = "删除设备产品", businessType = BusinessType.DELETE) public AjaxResult remove(@PathVariable String[] ids) { @@ -480,7 +449,7 @@ public class IoTProductController extends BaseController { throw new IoTException("没有该产品"); } if (!iotUser.isAdmin() && !ioTProduct.getCreatorId().equals(iotUser.getUnionId())) { - throw new IoTException("您没有权限操作此产品!"); + throw new IoTException("没有产品权限"); } IoTDevice ioTDevice = IoTDevice.builder().productKey(ioTProduct.getProductKey()).build(); List ioTDeviceList = @@ -501,7 +470,9 @@ public class IoTProductController extends BaseController { return AjaxResult.success( IoTDownlFactory.safeInvokeDown( - ProtocolModule.ctaiot.name(), "downPro", downProRequest.toString())); + ProtocolModule.ctaiot.name(), + IoTConstant.DOWN_TO_THIRD_PLATFORM, + downProRequest.toString())); } // tcp产品删除时同时删除network表 if (ProtocolModule.tcp.name().equals(ioTProduct.getThirdPlatform())) { @@ -512,22 +483,25 @@ public class IoTProductController extends BaseController { } // 删除协议 ioTDeviceProtocolService.deleteDevProtocolById(ioTProduct.getProductKey()); + + // 发布产品删除事件 + Map eventData = new HashMap<>(); + eventData.put("type", "product"); + eventData.put("productKey", ioTProduct.getProductKey()); + eventData.put("operation", "DELETE"); + eventPublisher.publishEvent(EventTopics.PRODUCT_CONFIG_UPDATED, eventData); } return toAjax(devProductService.deleteDevProductByIds(ids)); } - /** - * 查询设备产品物模型属性列表 - */ + /** 查询设备产品物模型属性列表 */ @GetMapping("/properties/{id}") public AjaxResult getPropertiesList(@PathVariable("id") String id) { List list = devProductService.selectDevProperties(id); return AjaxResult.success(list); } - /** - * 查询设备产品物模型属性列表(功能下发属性) - */ + /** 查询设备产品物模型属性列表(功能下发属性) */ @GetMapping("/functionProperties/{id}") public AjaxResult getFunctionPropertiesList(@PathVariable("id") String id) { List list = devProductService.selectDevProperties(id); @@ -535,7 +509,8 @@ public class IoTProductController extends BaseController { List dataRw = list.stream().filter((item) -> "rw".equals(item.getMode())).collect(Collectors.toList()); // 获取设备公共字段 - List data = dictTypeService.selectDictDataByType("device_common_property"); + List data = + dictTypeService.selectDictDataByType(IoTConstant.DEVICE_SHADOW_CUSTOMIZED_PROPERTY); // 根据公共字段过滤 if (Validator.isNotNull(data)) { for (SysDictData datum : data) { @@ -558,27 +533,21 @@ public class IoTProductController extends BaseController { return AjaxResult.success(dataRw); } - /** - * 查询设备产品物模型事件列表 - */ + /** 查询设备产品物模型事件列表 */ @GetMapping("/events/{id}") public AjaxResult getEventsList(@PathVariable("id") String id) { List list = devProductService.selectDevEvents(id); return AjaxResult.success(list); } - /** - * 查询设备产品物模型方法列表 - */ + /** 查询设备产品物模型方法列表 */ @GetMapping("/functions/{id}") public AjaxResult getFunctionsList(@PathVariable("id") String id) { List list = devProductService.selectDevFunctions(id); return AjaxResult.success(list); } - /** - * 根据产品key查model配置 - */ + /** 根据产品key查model配置 */ @GetMapping("/model/{id}") public AjaxResult getmodel(@PathVariable("id") String id) { IoTDeviceModelVO ioTDeviceModelVO = devProductService.getModelByProductKey(id); @@ -590,7 +559,7 @@ public class IoTProductController extends BaseController { return AjaxResult.success(devProductService.selectMetadataByDevId(devId)); } - @GetMapping("/devnumber") + @GetMapping("/device/count") public AjaxResult countDevNumberByProductKey() { IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); Map products = @@ -599,13 +568,11 @@ public class IoTProductController extends BaseController { return AjaxResult.success(products); } - /** - * 导出设备产品列表 - */ + /** 导出设备产品列表 */ @PostMapping("/export") @Log(title = "产品导出", businessType = BusinessType.EXPORT) public void export(HttpServletResponse response, IoTProductQuery query) { - // 只能看自己设备,除非是特殊设置和超管 + // IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId()); query.setSelf(!iotUser.isAdmin()); if (query.isSelf()) { @@ -646,15 +613,13 @@ public class IoTProductController extends BaseController { } } - /** - * 批量导入产品 - */ + /** 批量导入产品 */ @PostMapping("/import") @Log(title = "产品批量导入", businessType = BusinessType.IMPORT) @Transactional(rollbackFor = Exception.class) public AjaxResult importProduct(MultipartFile file) throws Exception { // 模板 - // 只能看自己设备,除非是特殊设置和超管 + // String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); try { // 读取JSON文件内容 @@ -675,9 +640,41 @@ public class IoTProductController extends BaseController { } } - /** - * 下载导入模版 - */ + /** 上传产品图片并保存到产品信息 */ + @Operation(summary = "上传产品图片") + @PostMapping("/uploadImage") + @Log(title = "上传产品图片", businessType = BusinessType.UPDATE) + public AjaxResult> uploadProductImage( + @RequestParam("id") String id, @RequestParam("file") MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new IoTException("上传文件不能为空"); + } + IoTProductVO product = devProductService.selectDevProductById(id); + if (product == null) { + throw new IoTException("产品不存在"); + } + String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId(); + // 上传到 OSS,沿用系统统一上传逻辑 + var oss = iSysOssService.upload(file, unionId); + String url = oss.getUrl(); + + // 更新产品 photoUrl 字段(保持前端期望结构) + IoTProduct update = new IoTProduct(); + update.setId(product.getId()); + JSONObject photo = new JSONObject(); + photo.set("img", url); + photo.set("detail", ""); + update.setPhotoUrl(photo.toString()); + devProductService.updateDevProduct(update); + + Map data = new HashMap<>(2); + data.put("url", url); + String name = oss.getOriginalName(); + data.put("fileName", name.substring(name.lastIndexOf('/') + 1, name.lastIndexOf("."))); + return AjaxResult.success(data); + } + + /** 下载导入模版 */ @PostMapping("/import/template") public void downloadTemplate(HttpServletResponse response) { List list = new ArrayList<>(); @@ -723,9 +720,7 @@ public class IoTProductController extends BaseController { return getDataTable(maps); } - /** - * 绑定证书到产品 - */ + /** 绑定证书到产品 */ @PostMapping("/bindCertificate") @Log(title = "绑定证书", businessType = BusinessType.UPDATE) public AjaxResult bindCertificate(@RequestParam String productKey, @RequestParam String sslKey) { @@ -733,13 +728,35 @@ public class IoTProductController extends BaseController { return toAjax(result); } - /** - * 解绑证书 - */ + /** 解绑证书 */ @PostMapping("/unbindCertificate") @Log(title = "解绑证书", businessType = BusinessType.UPDATE) public AjaxResult unbindCertificate(@RequestParam String productKey) { int result = devProductService.unbindCertificate(productKey); return toAjax(result); } + + /** 查看连接信息 */ + @PostMapping("/connect/info") + @Log(title = "查看连接信息", businessType = BusinessType.OTHER) + public AjaxResult connectInfo(@RequestParam String productKey) { + try { + ConnectionInfoDTO connectionInfo = connectionInfoService.getConnectionInfo(productKey); + return AjaxResult.success(connectionInfo); + } catch (Exception e) { + return AjaxResult.error("获取连接信息失败:" + e.getMessage()); + } + } + + /** 查看MQTT密码 */ + @PostMapping("/connect/mqtt/password") + @Log(title = "查看连接信息密码", businessType = BusinessType.GRANT) + public AjaxResult connectInfoPassword(@RequestParam String productKey) { + try { + ConnectionInfoDTO passwordInfo = connectionInfoService.getMqttPasswordInfo(productKey); + return AjaxResult.success(passwordInfo); + } catch (Exception e) { + return AjaxResult.error("获取密码信息失败:" + e.getMessage()); + } + } } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java index a6164d5a92e44324eb753e65c7c17c62e54602b4..1cf010f1b081a0ff10be1dfd8c093a34c334457d 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/platform/web/IoTUserAppController.java @@ -16,11 +16,12 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; -import cn.hutool.core.util.StrUtil; + import cn.hutool.http.Header; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; + import cn.hutool.json.JSONUtil; import cn.universal.admin.common.annotation.Log; import cn.universal.admin.common.enums.BusinessType; @@ -137,6 +138,28 @@ public class IoTUserAppController extends BaseController { util.exportExcel(response, list, "用户应用信息数据"); } + /** 获取用户应用信息详细信息 */ + @Operation(summary = "启用推送") + @PostMapping(value = "/{appUniqueId}/{pushType}/{enable}") + public AjaxResult enableOrDisablePush( + @PathVariable("appUniqueId") String appUniqueId, + @PathVariable("pushType") String pushType, + @PathVariable("enable") boolean enable) { + IoTUserApplication application = + iotUserApplicationService.selectIotUserApplicationById(appUniqueId); + IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId()); + if (!user.isAdmin() + && ObjectUtil.isNotEmpty(application) + && !application.getUnionId().equals(user.getUnionId())) { + throw new IoTException("你无权操作"); + } + boolean flag = iotUserApplicationService.enableOrDisablePushCfg(appUniqueId, pushType, enable); + if (flag) { + return AjaxResult.success(); + } + return AjaxResult.error("失败"); + } + /** 获取用户应用信息详细信息 */ @Operation(summary = "获取用户应用信息详细信息") @GetMapping(value = "/{appUniqueId}") @@ -198,21 +221,21 @@ public class IoTUserAppController extends BaseController { @Transactional @Log(title = "重置密钥", businessType = BusinessType.UPDATE) public AjaxResult resetSecret(@RequestBody IoTUserApplication iotUserApplication) { - IoTUserApplication oldapplication = - iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId()); IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId()); + + // 权限检查 + IoTUserApplication oldApplication = + iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId()); if (!user.isAdmin() - && ObjectUtil.isNotEmpty(oldapplication) - && !oldapplication.getUnionId().equals(user.getUnionId())) { + && ObjectUtil.isNotEmpty(oldApplication) + && !oldApplication.getUnionId().equals(user.getUnionId())) { throw new IoTException("你无权操作"); } - // iotUserApplication.setAppId(RandomUtil.randomString(16)); - iotUserApplication.setAppSecret(RandomUtil.randomString(32)); - iotUserApplication.setUnionId(user.getUnionId()); - if (iotUserApplicationService.updateIotUserApplication(iotUserApplication) == 0) { + // 调用service层方法完成密钥重置和MQTT配置同步 + if (!iotUserApplicationService.resetAppSecretAndSyncMqtt( + iotUserApplication.getAppUniqueId(), user.getUnionId())) { throw new IoTException("应用密钥重置失败"); } - return AjaxResult.success(); } @@ -238,7 +261,6 @@ public class IoTUserAppController extends BaseController { Map map = new HashMap<>(); map.put("message", "success"); iotUserApplication.setUnionId(app.getUnionId()); - iotUserApplication.setUpTopic(app.getUnionId() + StrUtil.C_SLASH + app.getAppId()); iotUserApplicationService.updateIotUserApplication(iotUserApplication); return AjaxResult.success(map); } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java index cafd4a58cb74698b113242a73ede98f8357e33a5..8aa4ea97ebd34c7a49a54e0f65deb5d5daf1dd6a 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/IIoTUserApplicationService.java @@ -67,4 +67,15 @@ public interface IIoTUserApplicationService { int deleteIotUserApplicationById(String appUniqueId); List selectApplicationList(IoTUserApplication application, IoTUser iotUser); + + boolean enableOrDisablePushCfg(String appUniqueId, String pushType, boolean isEnable); + + /** + * 重置应用密钥并同步更新MQTT配置 + * + * @param appUniqueId 应用唯一标识 + * @param unionId 用户唯一标识 + * @return 是否成功 + */ + boolean resetAppSecretAndSyncMqtt(String appUniqueId, String unionId); } diff --git a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java index 6586b105b025cc4fcc95f4f9106740b17c119a37..de64d02eab474e565fa90eef54d6c091d14c49c0 100644 --- a/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java +++ b/cn-universal-admin/src/main/java/cn/universal/admin/system/service/impl/IoTUserApplicationServiceImpl.java @@ -13,6 +13,9 @@ package cn.universal.admin.system.service.impl; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import cn.universal.admin.common.service.BaseServiceImpl; import cn.universal.admin.common.utils.SecurityUtils; import cn.universal.admin.system.service.IIoTUserApplicationService; @@ -28,9 +31,12 @@ import cn.universal.persistence.mapper.OauthClientDetailsMapper; import jakarta.annotation.Resource; import java.util.List; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import tk.mybatis.mapper.entity.Example; /** @@ -39,6 +45,7 @@ import tk.mybatis.mapper.entity.Example; * @since 2025-12-30 */ @Service +@Slf4j public class IoTUserApplicationServiceImpl extends BaseServiceImpl implements IIoTUserApplicationService { @@ -47,6 +54,12 @@ public class IoTUserApplicationServiceImpl extends BaseServiceImpl @Resource private OauthClientDetailsMapper oauthClientDetailsMapper; + @Value("${mqtt.cfg.enable:true}") + private boolean sysMqttEnabled; + + @Value("${mqtt.cfg.host:tcp://localhost:1883}") + private String sysMqttHost; + @Override public boolean EnDisableIoTUser(String unionId, boolean isEnable) { List select = @@ -212,4 +225,111 @@ public class IoTUserApplicationServiceImpl extends BaseServiceImpl application.setUnionId(iotUser.isAdmin() ? null : iotUser.getUnionId()); return iotUserApplicationMapper.selectApplicationList(application); } + + @Override + public boolean enableOrDisablePushCfg(String appUniqueId, String pushType, boolean isEnable) { + IoTUserApplication application = + iotUserApplicationMapper.selectOne( + IoTUserApplication.builder().appUniqueId(appUniqueId).build()); + if (application == null) { + return false; + } + // 获取当前推送配置 + String pushCfgStr = application.getCfg(); + JSONObject pushCfg; + if (StrUtil.isBlank(pushCfgStr)) { + pushCfg = new JSONObject(); + } else { + pushCfg = JSONUtil.parseObj(pushCfgStr); + } + + switch (pushType) { + case "mqtt": + if (isEnable) { + // MQTT启用:设置url为sysMqttHost、support为true、enable为true + // password为app_secret、username为app_id + JSONObject mqttConfig = new JSONObject(); + mqttConfig.set("enable", true); + mqttConfig.set("support", true); + mqttConfig.set("url", sysMqttHost); + mqttConfig.set("password", application.getAppSecret()); + mqttConfig.set("username", application.getAppId()); + mqttConfig.set("clientId", application.getAppId()); + + pushCfg.set("mqtt", mqttConfig); + } else { + // MQTT禁用:只设置enable为false + if (pushCfg.containsKey("mqtt")) { + JSONObject mqttConfig = pushCfg.getJSONObject("mqtt"); + mqttConfig.set("enable", false); + pushCfg.set("mqtt", mqttConfig); + } + } + break; + case "http": + if (isEnable) { + // HTTP启用:设置enable和support为true + JSONObject httpConfig = pushCfg.getJSONObject("http"); + httpConfig.set("enable", true); + httpConfig.set("support", true); + pushCfg.set("http", httpConfig); + } else { + // HTTP禁用:设置enable为false + if (pushCfg.containsKey("http")) { + JSONObject httpConfig = pushCfg.getJSONObject("http"); + httpConfig.set("enable", false); + pushCfg.set("http", httpConfig); + } + } + break; + default: + return false; + } + // 更新推送配置 + application.setCfg(pushCfg.toString()); + int result = iotUserApplicationMapper.updateIotUserApplication(application); + return result > 0; + } + + @Override + @Transactional + public boolean resetAppSecretAndSyncMqtt(String appUniqueId, String unionId) { + // 查询应用信息 + IoTUserApplication application = + iotUserApplicationMapper.selectOne( + IoTUserApplication.builder().appUniqueId(appUniqueId).build()); + + if (application == null) { + return false; + } + // 生成新的应用密钥 + String newAppSecret = cn.hutool.core.util.RandomUtil.randomString(32); + application.setAppSecret(newAppSecret); + + // 更新应用信息 + if (iotUserApplicationMapper.updateByPrimaryKeySelective(application) == 0) { + return false; + } + // 检查MQTT是否开启,如果开启则同步更新MQTT配置中的密码 + String oldCfg = application.getCfg(); + if (StrUtil.isNotBlank(oldCfg)) { + JSONObject cfg = JSONUtil.parseObj(oldCfg); + if (cfg.containsKey("mqtt")) { + JSONObject mqttConfig = cfg.getJSONObject("mqtt"); + if (mqttConfig.getBool("enable", false)) { + // MQTT已启用,更新密码 + mqttConfig.set("password", newAppSecret); + cfg.set("mqtt", mqttConfig); + + // 更新配置 + IoTUserApplication updateCfg = new IoTUserApplication(); + updateCfg.setAppUniqueId(appUniqueId); + updateCfg.setCfg(cfg.toString()); + iotUserApplicationMapper.updateByPrimaryKeySelective(updateCfg); + log.info("应用 {} 密钥重置后,MQTT配置密码已同步更新", appUniqueId); + } + } + } + return true; + } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java index b76de516757937d3df55948206eac758874a51c5..e95471b1b49a4546738130b127665e2ce9727993 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfig.java @@ -20,16 +20,23 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import java.util.Arrays; +import java.util.List; + /** * Redisson 配置类 用于配置分布式锁和 Redis 连接 + * 支持单机模式、集群模式、哨兵模式、主从模式 * - * @version 1.0 @Author Aleo + * @version 2.0 @Author Aleo * @since 2025/1/20 */ @Slf4j @Configuration public class RedissonConfig { + // ================================ + // 基础配置 + // ================================ @Value("${spring.data.redis.host:localhost}") private String redisHost; @@ -45,6 +52,9 @@ public class RedissonConfig { @Value("${spring.data.redis.timeout:3000}") private int redisTimeout; + // ================================ + // 连接池配置 + // ================================ @Value("${spring.data.redis.lettuce.pool.max-active:8}") private int maxActive; @@ -57,7 +67,12 @@ public class RedissonConfig { @Value("${spring.data.redis.lettuce.pool.max-wait:-1}") private long maxWait; + // ================================ // Redisson 特定配置 + // ================================ + @Value("${redisson.mode:single}") + private String redissonMode; + @Value("${redisson.lock.watchdog-timeout:30000}") private int lockWatchdogTimeout; @@ -82,43 +97,212 @@ public class RedissonConfig { @Value("${redisson.connection.retry-interval:1500}") private int redissonRetryInterval; + // ================================ + // 集群模式配置 + // ================================ + @Value("${redisson.cluster.nodes:}") + private String clusterNodes; + + @Value("${redisson.cluster.scan-interval:2000}") + private int clusterScanInterval; + + @Value("${redisson.cluster.slave-connection-minimum-idle-size:1}") + private int clusterSlaveMinIdle; + + @Value("${redisson.cluster.slave-connection-pool-size:64}") + private int clusterSlavePoolSize; + + @Value("${redisson.cluster.master-connection-minimum-idle-size:1}") + private int clusterMasterMinIdle; + + @Value("${redisson.cluster.master-connection-pool-size:64}") + private int clusterMasterPoolSize; + + // ================================ + // 哨兵模式配置 + // ================================ + @Value("${redisson.sentinel.master-name:mymaster}") + private String sentinelMasterName; + + @Value("${redisson.sentinel.nodes:}") + private String sentinelNodes; + + @Value("${redisson.sentinel.password:}") + private String sentinelPassword; + + @Value("${redisson.sentinel.database:0}") + private int sentinelDatabase; + + // ================================ + // 主从模式配置 + // ================================ + @Value("${redisson.master-slave.master-address:}") + private String masterAddress; + + @Value("${redisson.master-slave.slave-addresses:}") + private String slaveAddresses; + + @Value("${redisson.master-slave.database:0}") + private int masterSlaveDatabase; + /** 配置 Redisson 客户端 */ @Bean @Primary public RedissonClient redissonClient() { try { Config config = new Config(); - - // 单机模式配置 - config - .useSingleServer() - .setAddress("redis://" + redisHost + ":" + redisPort) - .setDatabase(redisDatabase) - .setConnectionPoolSize(redissonPoolSize) - .setConnectionMinimumIdleSize(redissonMinIdle) - .setConnectTimeout(redissonTimeout) - .setIdleConnectionTimeout(redissonTimeout) - .setRetryAttempts(redissonRetryAttempts) - .setRetryInterval(redissonRetryInterval) - .setKeepAlive(true) - .setTcpNoDelay(true); - - // 如果设置了密码 - if (redisPassword != null && !redisPassword.trim().isEmpty()) { - config.useSingleServer().setPassword(redisPassword); + + // 根据配置模式选择不同的连接方式 + switch (redissonMode.toLowerCase()) { + case "cluster": + configClusterMode(config); + break; + case "sentinel": + configSentinelMode(config); + break; + case "master-slave": + configMasterSlaveMode(config); + break; + case "single": + default: + configSingleMode(config); + break; } // 分布式锁配置 - config.setLockWatchdogTimeout(lockWatchdogTimeout); // 看门狗超时时间 + config.setLockWatchdogTimeout(lockWatchdogTimeout); RedissonClient redissonClient = Redisson.create(config); - log.info("Redisson 客户端初始化成功: {}:{}", redisHost, redisPort); + log.info("Redisson 客户端初始化成功: mode={}, host={}:{}", redissonMode, redisHost, redisPort); return redissonClient; } catch (Exception e) { - log.error("Redisson 客户端初始化失败: {}:{}, error={}", redisHost, redisPort, e.getMessage(), e); + log.error("Redisson 客户端初始化失败: mode={}, host={}:{}, error={}", + redissonMode, redisHost, redisPort, e.getMessage(), e); throw new RuntimeException("Redisson 初始化失败", e); } } + + /** 配置单机模式 */ + private void configSingleMode(Config config) { + log.info("配置 Redisson 单机模式: {}:{}", redisHost, redisPort); + + config.useSingleServer() + .setAddress("redis://" + redisHost + ":" + redisPort) + .setDatabase(redisDatabase) + .setConnectionPoolSize(redissonPoolSize) + .setConnectionMinimumIdleSize(redissonMinIdle) + .setConnectTimeout(redissonTimeout) + .setIdleConnectionTimeout(redissonTimeout) + .setRetryAttempts(redissonRetryAttempts) + .setRetryInterval(redissonRetryInterval) + .setKeepAlive(true) + .setTcpNoDelay(true); + + // 如果设置了密码 + if (redisPassword != null && !redisPassword.trim().isEmpty()) { + config.useSingleServer().setPassword(redisPassword); + } + } + + /** 配置集群模式 */ + private void configClusterMode(Config config) { + if (clusterNodes == null || clusterNodes.trim().isEmpty()) { + throw new IllegalArgumentException("集群模式需要配置 redisson.cluster.nodes"); + } + + log.info("配置 Redisson 集群模式: {}", clusterNodes); + + List nodes = Arrays.asList(clusterNodes.split(",")); + config.useClusterServers() + .setScanInterval(clusterScanInterval) + .setMasterConnectionMinimumIdleSize(clusterMasterMinIdle) + .setMasterConnectionPoolSize(clusterMasterPoolSize) + .setSlaveConnectionMinimumIdleSize(clusterSlaveMinIdle) + .setSlaveConnectionPoolSize(clusterSlavePoolSize) + .setConnectTimeout(redissonTimeout) + .setIdleConnectionTimeout(redissonTimeout) + .setRetryAttempts(redissonRetryAttempts) + .setRetryInterval(redissonRetryInterval) + .setKeepAlive(true) + .setTcpNoDelay(true); + + // 添加集群节点 + for (String node : nodes) { + config.useClusterServers().addNodeAddress("redis://" + node.trim()); + } + + // 如果设置了密码 + if (redisPassword != null && !redisPassword.trim().isEmpty()) { + config.useClusterServers().setPassword(redisPassword); + } + } + + /** 配置哨兵模式 */ + private void configSentinelMode(Config config) { + if (sentinelNodes == null || sentinelNodes.trim().isEmpty()) { + throw new IllegalArgumentException("哨兵模式需要配置 redisson.sentinel.nodes"); + } + + log.info("配置 Redisson 哨兵模式: master={}, sentinels={}", sentinelMasterName, sentinelNodes); + + List sentinels = Arrays.asList(sentinelNodes.split(",")); + config.useSentinelServers() + .setMasterName(sentinelMasterName) + .setDatabase(sentinelDatabase) + .setConnectTimeout(redissonTimeout) + .setIdleConnectionTimeout(redissonTimeout) + .setRetryAttempts(redissonRetryAttempts) + .setRetryInterval(redissonRetryInterval) + .setKeepAlive(true) + .setTcpNoDelay(true); + + // 添加哨兵节点 + for (String sentinel : sentinels) { + config.useSentinelServers().addSentinelAddress("redis://" + sentinel.trim()); + } + + // 如果设置了密码 + if (redisPassword != null && !redisPassword.trim().isEmpty()) { + config.useSentinelServers().setPassword(redisPassword); + } + + // 如果设置了哨兵密码 + if (sentinelPassword != null && !sentinelPassword.trim().isEmpty()) { + config.useSentinelServers().setSentinelPassword(sentinelPassword); + } + } + + /** 配置主从模式 */ + private void configMasterSlaveMode(Config config) { + if (masterAddress == null || masterAddress.trim().isEmpty()) { + throw new IllegalArgumentException("主从模式需要配置 redisson.master-slave.master-address"); + } + + log.info("配置 Redisson 主从模式: master={}, slaves={}", masterAddress, slaveAddresses); + + config.useMasterSlaveServers() + .setMasterAddress("redis://" + masterAddress.trim()) + .setDatabase(masterSlaveDatabase) + .setConnectTimeout(redissonTimeout) + .setIdleConnectionTimeout(redissonTimeout) + .setRetryAttempts(redissonRetryAttempts) + .setRetryInterval(redissonRetryInterval) + .setKeepAlive(true) + .setTcpNoDelay(true); + + // 添加从节点 + if (slaveAddresses != null && !slaveAddresses.trim().isEmpty()) { + List slaves = Arrays.asList(slaveAddresses.split(",")); + for (String slave : slaves) { + config.useMasterSlaveServers().addSlaveAddress("redis://" + slave.trim()); + } + } + + // 如果设置了密码 + if (redisPassword != null && !redisPassword.trim().isEmpty()) { + config.useMasterSlaveServers().setPassword(redisPassword); + } + } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfigTest.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfigTest.java new file mode 100644 index 0000000000000000000000000000000000000000..84ce3bd7106ce3da34c20754d846aec91e9baea9 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/config/RedissonConfigTest.java @@ -0,0 +1,151 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.dm.device.config; + +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * Redisson 配置测试类 + * 用于验证不同模式的连接是否正常 + * + * @version 1.0 @Author Aleo + * @since 2025/1/20 + */ +@Slf4j +//@Component +public class RedissonConfigTest implements CommandLineRunner { + + @Autowired + private RedissonClient redissonClient; + + @Override + public void run(String... args) throws Exception { + log.info("开始测试 Redisson 连接..."); + + try { + // 测试基本连接 + testBasicConnection(); + + // 测试分布式锁 + testDistributedLock(); + + // 测试Redis操作 + testRedisOperations(); + + log.info("Redisson 连接测试完成,所有测试通过!"); + + } catch (Exception e) { + log.error("Redisson 连接测试失败: {}", e.getMessage(), e); + // 不抛出异常,避免影响应用启动 + } + } + + /** 测试基本连接 */ + private void testBasicConnection() { + log.info("测试基本连接..."); + + // 测试基本操作 + String testKey = "test:redisson:connection"; + redissonClient.getBucket(testKey).set("test"); + Object value = redissonClient.getBucket(testKey).get(); + log.info("Redis 连接测试通过: {} = {}", testKey, value); + + // 清理测试数据 + redissonClient.getBucket(testKey).delete(); + } + + /** 测试分布式锁 */ + private void testDistributedLock() { + log.info("测试分布式锁..."); + + String lockKey = "test:redisson:lock"; + RLock lock = redissonClient.getLock(lockKey); + + try { + // 尝试获取锁 + boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS); + if (locked) { + log.info("成功获取分布式锁: {}", lockKey); + + // 模拟业务处理 + Thread.sleep(1000); + + // 释放锁 + lock.unlock(); + log.info("成功释放分布式锁: {}", lockKey); + } else { + log.warn("获取分布式锁失败: {}", lockKey); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("分布式锁测试被中断: {}", lockKey); + } catch (Exception e) { + log.error("分布式锁测试失败: {}", e.getMessage()); + } + } + + /** 测试Redis操作 */ + private void testRedisOperations() { + log.info("测试Redis操作..."); + + try { + // 测试字符串操作 + String testKey = "test:redisson:string"; + String testValue = "Hello Redisson!"; + + redissonClient.getBucket(testKey).set(testValue); + String retrievedValue = redissonClient.getBucket(testKey).get().toString(); + + if (testValue.equals(retrievedValue)) { + log.info("字符串操作测试通过: {} = {}", testKey, retrievedValue); + } else { + log.warn("字符串操作测试失败: 期望={}, 实际={}", testValue, retrievedValue); + } + + // 清理测试数据 + redissonClient.getBucket(testKey).delete(); + + // 测试Hash操作 + String hashKey = "test:redisson:hash"; + redissonClient.getMap(hashKey).put("field1", "value1"); + redissonClient.getMap(hashKey).put("field2", "value2"); + + int hashSize = redissonClient.getMap(hashKey).size(); + log.info("Hash操作测试通过: {} 包含 {} 个字段", hashKey, hashSize); + + // 清理测试数据 + redissonClient.getMap(hashKey).delete(); + + // 测试Set操作 + String setKey = "test:redisson:set"; + redissonClient.getSet(setKey).add("item1"); + redissonClient.getSet(setKey).add("item2"); + redissonClient.getSet(setKey).add("item3"); + + int setSize = redissonClient.getSet(setKey).size(); + log.info("Set操作测试通过: {} 包含 {} 个元素", setKey, setSize); + + // 清理测试数据 + redissonClient.getSet(setKey).delete(); + + } catch (Exception e) { + log.error("Redis操作测试失败: {}", e.getMessage()); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java deleted file mode 100644 index 243ac2d9a797dca6705e39641a9a3a2d338cc8cc..0000000000000000000000000000000000000000 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractCodecService.java +++ /dev/null @@ -1,15 +0,0 @@ -package cn.universal.dm.device.service; - -import cn.universal.dm.device.service.impl.IoTProductDeviceService; -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; - -/** - * @version 1.0 @Author Aleo - * @since 2025/6/26 20:10 - */ -@Slf4j -public abstract class AbstractCodecService { - - @Resource private IoTProductDeviceService iotProductDeviceService; -} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java index 35d0554c18da77c1fd93813fcd8f8c5a86427cc7..93b00945df3828a3ed82237dcd696ece79a3d647 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractDownService.java @@ -12,7 +12,7 @@ package cn.universal.dm.device.service; -import cn.universal.core.service.ICodec; +import cn.hutool.json.JSONObject; import cn.universal.core.service.ICodecService; import cn.universal.core.service.IDown; import lombok.extern.slf4j.Slf4j; @@ -25,9 +25,9 @@ import org.springframework.beans.factory.annotation.Autowired; * @since 2025/8/9 10:50 */ @Slf4j -public abstract class AbstractDownService extends AbstratIoTService implements IDown, ICodec { +public abstract class AbstractDownService extends AbstratIoTService implements IDown { - @Autowired protected ICodecService codecService; + @Autowired protected ICodecService iCodecService; /** * 消息转换和编解码 @@ -37,9 +37,17 @@ public abstract class AbstractDownService extends AbstratIoTService implement */ protected abstract T convert(String request); - @Override - public String spliceDown(String productKey, String payload) { + protected String encodeWithShadow(String productKey, String deviceId, String payload) { + JSONObject jsonObject = getProductConfiguration(productKey); + JSONObject context = null; + if (jsonObject != null) { + // 上行报文是否需要附加影子 + Boolean requireDownShadow = jsonObject.getBool("requireDownShadow", false); + if (requireDownShadow) { + context = iotDeviceShadowService.getDeviceShadowObj(productKey, deviceId); + } + } // 使用新的统一编解码服务 - return codecService.encode(productKey, payload); + return iCodecService.encode(productKey, payload, context); } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java index 72dd308cbba80a97ca91e9fdb1ecf1a97d997433..77fad20212e14012fe31a26969d0939f1f12483b 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstractUPService.java @@ -15,7 +15,6 @@ package cn.universal.dm.device.service; import cn.hutool.core.util.IdUtil; import cn.universal.common.constant.IoTConstant; import cn.universal.core.message.UPRequest; -import cn.universal.core.service.ICodec; import cn.universal.core.service.ICodecService; import cn.universal.core.service.IUP; import java.util.List; @@ -33,7 +32,7 @@ import org.springframework.scheduling.annotation.Async; */ @Slf4j public abstract class AbstractUPService extends AbstratIoTService - implements IUP, ICodec { + implements IUP { @Autowired protected ICodecService codecService; @@ -112,10 +111,4 @@ public abstract class AbstractUPService extends AbstratIoTS /** 当前iot组件名称 */ protected abstract String currentComponent(); - - @Override - public UPRequest preDecode(String productKey, String message) { - // 使用新的统一编解码服务 - return codecService.preDecode(productKey, message); - } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java index a00c0a99b337c7e9ae3ae3ffa0944fe7dc030b4f..bd596834e651e0738b41200417cf3c9f03fb3639 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/AbstratIoTService.java @@ -16,22 +16,15 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.universal.common.constant.IoTConstant; import cn.universal.common.constant.IoTConstant.DeviceNode; import cn.universal.common.constant.IoTConstant.MessageType; -import cn.universal.common.utils.AESOperator; import cn.universal.core.message.UPRequest; import cn.universal.core.metadata.AbstractEventMetadata; import cn.universal.core.metadata.AbstractFunctionMetadata; import cn.universal.core.metadata.DeviceMetadata; -import cn.universal.core.protocol.jar.ProtocolCodecJar; -import cn.universal.core.protocol.jscrtipt.ProtocolCodecJscript; -import cn.universal.core.protocol.magic.ProtocolCodecMagic; -import cn.universal.core.protocol.support.ProtocolCodecSupport; -import cn.universal.core.protocol.support.ProtocolSupportDefinition; import cn.universal.core.service.ICodecService; import cn.universal.dm.device.service.action.IoTDeviceActionAfterService; import cn.universal.dm.device.service.impl.IoTDeviceService; @@ -94,6 +87,47 @@ public abstract class AbstratIoTService { protected IoTProduct getProduct(String productKey) { return iotProductDeviceService.getProduct(productKey); } + + protected JSONObject getProductConfiguration(String productKey) { + return iotProductDeviceService.getProductConfiguration(productKey); + } + + /** + * 安全解析产品配置JSON + * + *

处理以下情况: + *

+ * + * @param product 产品信息 + * @return 解析后的JSONObject,如果解析失败返回空的JSONObject + */ + protected JSONObject parseProductConfigurationSafely(IoTProduct product) { + if (product == null) { + log.warn("产品信息为空,返回空配置"); + return new JSONObject(); + } + + String configuration = product.getConfiguration(); + if (StrUtil.isBlank(configuration)) { + log.debug("产品配置为空,返回空配置: productKey={}", product.getProductKey()); + return new JSONObject(); + } + + try { + JSONObject config = JSONUtil.parseObj(configuration); + log.debug("产品配置解析成功: productKey={}", product.getProductKey()); + return config; + } catch (Exception e) { + log.error("产品配置解析失败,返回空配置: productKey={}, configuration={}", + product.getProductKey(), configuration, e); + return new JSONObject(); + } + } + protected IoTDeviceDTO getIoTDeviceDTO(IoTDeviceQuery query) { IoTDeviceDTO instanceBO = iotDeviceService.lifeCycleDevInstance(query); if (instanceBO == null) { @@ -101,17 +135,6 @@ public abstract class AbstratIoTService { } return instanceBO; } - /** - * 查询产品是否配置了离线周期阈值 - * - *

用于判断产品是否需要监控设备离线状态 - * - * @param productKey 产品唯一标识 - * @return true表示配置了离线周期,false表示未配置 - */ - protected boolean offlineThreshold(String productKey) { - return iotProductDeviceService.offlineThreshold(productKey); - } /** * 查询产品的消息订阅URL列表 @@ -185,30 +208,6 @@ public abstract class AbstratIoTService { return instanceBO; } - /** - * 查询产品的协议解码定义 - * - *

获取产品配置的协议解码规则,用于消息的编解码处理 - * - * @param productKey 产品唯一标识 - * @return 协议支持定义,包含解码规则和配置 - */ - protected ProtocolSupportDefinition selectProtocolDef(String productKey) { - return iotProductDeviceService.selectProtocolDef(productKey); - } - - /** - * 查询产品的协议解码定义(不带超长脚本) - * - *

获取产品配置的协议解码规则,用于消息的编解码处理 - * - * @param productKey 产品唯一标识 - * @return 协议支持定义,包含解码规则和配置 - */ - protected ProtocolSupportDefinition selectProtocolDefNoScript(String productKey) { - return iotProductDeviceService.selectProtocolDefNoScript(productKey); - } - /** * 保存设备上行日志和影子数据 * @@ -231,73 +230,8 @@ public abstract class AbstratIoTService { } } - /** - * 获取编解码插件提供者 - * - *

根据支持类型获取对应的协议编解码实现,支持: - jar: Java插件模式 - jscript: JavaScript脚本模式 - magic: 魔法字节模式 - * - * @param supportType 支持类型 - * @return 协议编解码支持实例,如果类型不支持返回null - */ - protected ProtocolCodecSupport getProtocolCodecProvider(String supportType) { - if (supportType == null) { - return null; - } - if (supportType.equalsIgnoreCase("jar")) { - return ProtocolCodecJar.getInstance(); - } else if (supportType.equalsIgnoreCase("jscript")) { - return ProtocolCodecJscript.getInstance(); - } else if (supportType.equalsIgnoreCase("magic")) { - return ProtocolCodecMagic.getInstance(); - } - return null; - } - - /** - * 透传上层应用原始报文AES加密 - * - *

使用设备唯一标识作为密钥对原始报文进行AES加密 密钥生成规则:使用iotId的MD5值作为密钥 - * - * @param payload 原始报文内容 - * @param iotId 设备唯一标识 - * @return 加密后的报文 - */ - protected String playloadEncode(String payload, String iotId) { - return AESOperator.getInstance() - .encrypt(payload, DigestUtil.md5Hex16(iotId), DigestUtil.md5Hex(iotId)); - } - - /** - * AES解密 - * - *

使用设备唯一标识作为密钥对加密报文进行AES解密 密钥生成规则:使用iotId的MD5值作为密钥 - * - * @param payload 加密的报文内容 - * @param iotId 设备唯一标识 - * @return 解密后的原始报文 - */ - protected String playloadDecode(String payload, String iotId) { - return AESOperator.getInstance() - .decrypt(payload, DigestUtil.md5Hex16(iotId), DigestUtil.md5Hex(iotId)); - } - - /** - * 根据设备序列号查询设备信息 - * - *

通过设备序列号获取设备的完整信息,包括设备状态、配置等 - * - * @param deviceId 设备序列号 - * @return 设备实例信息 - */ - public IoTDeviceDTO getIotDeviceByDeviceIdLimitOne(String deviceId) { - IoTDeviceDTO devInstance = - iotDeviceService.lifeCycleDevInstance(IoTDeviceQuery.builder().deviceId(deviceId).build()); - return devInstance; - } - protected List decode( String productKey, String payload, Object context, Class elementType) { - // 使用新的统一编解码服务 return codecService.decode(productKey, payload, context, elementType); } @@ -309,6 +243,14 @@ public abstract class AbstratIoTService { return decode(productKey, payload, null, UPRequest.class); } + protected String encode(String productKey, String payload, Object context) { + return codecService.encode(productKey, payload, context); + } + + protected String encode(String productKey, String payload) { + return encode(productKey, payload, null); + } + protected void buildCodecNotNullBean( BaseUPRequest hasData, BaseUPRequest.BaseUPRequestBuilder builder) { if (CollUtil.isNotEmpty(hasData.getProperties())) { diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java deleted file mode 100644 index 4aac2653b5e015f309634ea78f9740aaf75a063c..0000000000000000000000000000000000000000 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/CodecServiceExample.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * - * Copyright (c) 2025, IoT-Universal. All Rights Reserved. - * - * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 - * @Author: Aleo - * @Email: wo8335224@gmail.com - * @Wechat: outlookFil - * - * - */ - -package cn.universal.dm.device.service; - -import cn.universal.core.message.UPRequest; -import cn.universal.core.protocol.support.ProtocolCodecSupport.CodecMethod; -import cn.universal.core.service.ICodecService; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -/** - * 编解码服务使用示例 - * - *

展示如何使用统一的编解码服务进行各种编解码操作 - * - * @version 1.0 @Author Aleo - * @since 2025/01/20 - */ -@Slf4j -@Service -public class CodecServiceExample { - - @Autowired private ICodecService codecService; - - /** - * 示例:解码设备上报数据 - * - * @param productKey 产品Key - * @param payload 原始数据 - * @return 解码后的UPRequest列表 - */ - public List decodeDeviceData(String productKey, String payload) { - log.info("开始解码设备数据: productKey={}, payload={}", productKey, payload); - - // 使用统一的解码服务 - List result = codecService.decode(productKey, payload); - - log.info("解码完成: 结果数量={}", result.size()); - return result; - } - - /** - * 示例:编码下行指令 - * - * @param productKey 产品Key - * @param payload 原始指令数据 - * @return 编码后的指令 - */ - public String encodeDownCommand(String productKey, String payload) { - log.info("开始编码下行指令: productKey={}, payload={}", productKey, payload); - - // 使用统一的编码服务 - String result = codecService.encode(productKey, payload); - - log.info("编码完成: result={}", result); - return result; - } - - /** - * 示例:预解码TCP数据 - * - * @param productKey 产品Key - * @param payload 原始TCP数据 - * @return 预解码后的UPRequest - */ - public UPRequest preDecodeTcpData(String productKey, String payload) { - log.info("开始预解码TCP数据: productKey={}, payload={}", productKey, payload); - - // 使用统一的预解码服务 - UPRequest result = codecService.preDecode(productKey, payload); - - log.info("预解码完成: result={}", result); - return result; - } - - /** - * 示例:通用编解码方法 - * - * @param productKey 产品Key - * @param payload 原始数据 - * @param codecMethod 编解码方法类型 - * @return 编解码结果 - */ - public String generalCodec(String productKey, String payload, CodecMethod codecMethod) { - log.info("开始通用编解码: productKey={}, payload={}, method={}", productKey, payload, codecMethod); - - // 使用统一的通用编解码服务 - String result = codecService.codec(productKey, payload, codecMethod); - - log.info("通用编解码完成: result={}", result); - return result; - } - - /** - * 示例:检查是否支持特定编解码方法 - * - * @param productKey 产品Key - * @param codecMethod 编解码方法类型 - * @return 是否支持 - */ - public boolean checkSupport(String productKey, CodecMethod codecMethod) { - boolean supported = codecService.isSupported(productKey, codecMethod); - log.info("检查编解码支持: productKey={}, method={}, supported={}", productKey, codecMethod, supported); - return supported; - } - - /** - * 示例:泛型解码 - * - * @param productKey 产品Key - * @param payload 原始数据 - * @param elementType 目标类型 - * @param 泛型类型 - * @return 解码后的对象列表 - */ - public List decodeWithType(String productKey, String payload, Class elementType) { - log.info("开始泛型解码: productKey={}, payload={}, elementType={}", productKey, payload, elementType); - - // 使用统一的泛型解码服务 - List result = codecService.decode(productKey, payload, elementType); - - log.info("泛型解码完成: 结果数量={}", result.size()); - return result; - } - - /** - * 示例:IoT到第三方数据转换 - * - * @param productKey 产品Key - * @param payload 原始数据 - * @return 转换后的数据 - */ - public String iotToYour(String productKey, String payload) { - log.info("开始IoT到第三方数据转换: productKey={}, payload={}", productKey, payload); - - // 使用统一的IoT到第三方转换服务 - String result = codecService.iotToYour(productKey, payload); - - log.info("IoT到第三方数据转换完成: result={}", result); - return result; - } - - /** - * 示例:第三方到IoT数据转换 - * - * @param productKey 产品Key - * @param payload 原始数据 - * @return 转换后的数据 - */ - public String yourToIot(String productKey, String payload) { - log.info("开始第三方到IoT数据转换: productKey={}, payload={}", productKey, payload); - - // 使用统一的第三方到IoT转换服务 - String result = codecService.yourToIot(productKey, payload); - - log.info("第三方到IoT数据转换完成: result={}", result); - return result; - } -} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java index 4c2fdca853c24c2ba1e2506bade3f9e6d0944018..25086b04e4c561dd4842f239c49133dc88a0f104 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/DashboardStatisticsTask.java @@ -60,7 +60,8 @@ public class DashboardStatisticsTask implements ApplicationRunner { // 如果本地缓存中没有平台数据,使用默认的平台列表 if (platforms.isEmpty()) { - platforms = Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "lechen", "tcp", "snitcp"); + platforms = + Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "imoulife", "tcp", "snitcp"); log.warn("[仪表盘统计] 本地缓存中无活跃平台,使用默认平台列表"); } @@ -70,7 +71,7 @@ public class DashboardStatisticsTask implements ApplicationRunner { } catch (Exception e) { log.error("[仪表盘统计] 获取活跃平台失败", e); // 异常时返回默认平台列表 - return Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "lechen", "tcp", "snitcp"); + return Arrays.asList("ctaiot", "lvzhou", "ezviz", "onenet", "imoulife", "tcp", "snitcp"); } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java index 7c55d948c22b20948bb6159a24cbb1795d2f817d..2bde1d93de278b82685521b77fd6c30c05649e08 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/IoTUPPushAdapter.java @@ -162,7 +162,7 @@ public abstract class IoTUPPushAdapter { String applicationId = deviceDTO.getApplicationId(); if (applicationId == null || applicationId.trim().isEmpty()) { - log.warn("[分组] 应用ID为空,设备ID: {}, 丢弃消息", deviceDTO.getIotId()); + log.warn("[分组] 应用ID为空,设备ID: {}, 丢弃消息", deviceDTO.getDeviceId()); return false; } return true; diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java index a629e51832d74e7b394b49aee93eb40277f136f1..7d5a4c063c924fc2f304b3b7ff1853a3c275577c 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTDeviceActionAfterService.java @@ -38,6 +38,7 @@ import cn.universal.persistence.mapper.IoTDeviceMapper; import cn.universal.persistence.mapper.IoTUserMapper; import jakarta.annotation.Resource; import java.nio.charset.Charset; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -444,14 +445,12 @@ public class IoTDeviceActionAfterService extends IoTUPPushAdapter .deviceId(instanceBO.getDeviceId()) .deviceName(instanceBO.getDeviceName()) .productKey(instanceBO.getProductKey()) - // .extDeviceId(instanceBO.getExtDeviceId()) .messageType(MessageType.FUNCTIONS.name()) .iotId(instanceBO.getIotId()) - .createId(instanceBO.getUserUnionId()) - .createTime(DateUtil.currentSeconds()) + .createTime(LocalDateTime.now()) .commandStatus(0) // 使用此字段作为校验标识 - .commandId(StrUtil.length(commandId) > 10 ? StrUtil.sub(commandId, 0, 10) : commandId) + .commandId(commandId) .build(); return ioTDeviceLog; } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java index 02295f81071259a8df5270c1b1e6ab82b919d4a5..78bd06efbe426bbadb700fbda1cdc400bc70cddc 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/action/IoTProductActionService.java @@ -44,6 +44,7 @@ public class IoTProductActionService implements IoTProductAction { "iot_dev_action", "selectDevCount", "iot_dev_product_list", + "selectDevProductV4List", "iot_product_device" }, allEntries = true) @@ -62,6 +63,7 @@ public class IoTProductActionService implements IoTProductAction { "iot_dev_action", "selectDevCount", "iot_dev_product_list", + "selectDevProductV4List", "iot_product_device" }, allEntries = true) @@ -76,6 +78,7 @@ public class IoTProductActionService implements IoTProductAction { "iot_dev_action", "selectDevCount", "iot_dev_product_list", + "selectDevProductV4List", "iot_product_device" }, allEntries = true) @@ -99,6 +102,7 @@ public class IoTProductActionService implements IoTProductAction { "iot_dev_action", "selectDevCount", "iot_dev_product_list", + "selectDevProductV4List", "iot_product_device" }, allEntries = true) @@ -116,6 +120,7 @@ public class IoTProductActionService implements IoTProductAction { "iot_dev_action", "selectDevCount", "iot_dev_product_list", + "selectDevProductV4List", "iot_product_device" }, allEntries = true) diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java deleted file mode 100644 index 2d00ac4b863cfa20a2324a240fd560052fab75fc..0000000000000000000000000000000000000000 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/codec/CodecImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * - * Copyright (c) 2025, IoT-Universal. All Rights Reserved. - * - * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 - * @Author: Aleo - * @Email: wo8335224@gmail.com - * @Wechat: outlookFil - * - * - */ - -package cn.universal.dm.device.service.codec; - -import cn.universal.core.message.UPRequest; -import cn.universal.core.service.ICodec; -import cn.universal.core.service.ICodecService; -import cn.universal.dm.device.service.AbstratIoTService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -/** - * @version 1.0 @Author Aleo - * @since 2023/6/20 - */ -@Service("codecImpl") -@Slf4j -public class CodecImpl extends AbstratIoTService implements ICodec { - - @Autowired private ICodecService codecService; - - @Override - public UPRequest preDecode(String productKey, String message) { - // 使用新的统一编解码服务 - return codecService.preDecode(productKey, message); - } -} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java index 7b924c4f63ec7bcfcb118919551d6eff2cfee417..0b40577576d0558da80adbbb83e73001c1d442b3 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTCacheRemoveService.java @@ -43,6 +43,7 @@ public class IoTCacheRemoveService { "iot_dev_shadow_bo", "iot_dev_action", "iot_dev_product_list", + "selectDevProductV4List", "selectDevCount" }, allEntries = true) diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java index d34effe8484028a85adf55d0c0c39c25a111da59..874971ef33301d84d59eb67936bbfacaaf95c6ce 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceService.java @@ -43,35 +43,33 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; - +/** + * 设备和产品鉴权 + * + * @version 1.0 @Author Aleo + * @since 2025/8/12 16:10 + */ @Component @Slf4j public class IoTDeviceService { - @Resource - private SupportMapAreasMapper supportMapAreasMapper; - @Resource - private IoTDeviceMapper ioTDeviceMapper; + @Resource private SupportMapAreasMapper supportMapAreasMapper; + @Resource private IoTDeviceMapper ioTDeviceMapper; - @Resource - private IoTDeviceTagsMapper ioTDeviceTagsMapper; + @Resource private IoTDeviceTagsMapper ioTDeviceTagsMapper; - @Resource - private IoTDeviceShadowMapper ioTDeviceShadowMapper; + @Resource private IoTDeviceShadowMapper ioTDeviceShadowMapper; - @Resource - private IoTDeviceSubscribeMapper ioTDeviceSubscribeMapper; - @Resource - private IoTUserApplicationMapper iotUserApplicationMapper; - @Resource - private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; + @Resource private IoTDeviceSubscribeMapper ioTDeviceSubscribeMapper; + @Resource private IoTUserApplicationMapper iotUserApplicationMapper; + @Resource private IoTDeviceFenceRelMapper ioTDeviceFenceRelMapper; - @Resource - private IoTCacheRemoveService iotCacheRemoveService; + @Resource private IoTCacheRemoveService iotCacheRemoveService; public Page apiDeviceList(IoTAPIQuery iotAPIQuery) { Page page = PageHelper.startPage(iotAPIQuery.getPage(), iotAPIQuery.getSize()); @@ -79,12 +77,21 @@ public class IoTDeviceService { return page; } - /** - * 设备绑定应用 对外API - */ + /** 设备绑定应用 对外API */ + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) public int apiAppBind(String appid, String iotId) { - IoTUserApplication iotUserApplication = iotUserApplicationMapper.selectIotUserApplicationByAppId( - appid); + IoTUserApplication iotUserApplication = + iotUserApplicationMapper.selectIotUserApplicationByAppId(appid); if (iotUserApplication == null) { throw new IoTException("应用不存在"); } @@ -92,9 +99,18 @@ public class IoTDeviceService { return i; } - /** - * 设备解绑应用 对外API - */ + /** 设备解绑应用 对外API */ + @CacheEvict( + cacheNames = { + "iot_dev_instance_bo", + "iot_dev_metadata_bo", + "iot_dev_shadow_bo", + "iot_dev_action", + "selectDevCount", + "iot_dev_product_list", + "iot_product_device" + }, + allEntries = true) public int apiAppUnBind(String iotId) { if (StrUtil.isBlank(iotId)) { throw new IoTException("设备编号不能为空"); @@ -102,16 +118,17 @@ public class IoTDeviceService { return ioTDeviceMapper.apiUnBindApp(iotId); } - /** - * 更新设备信息 - */ + /** 更新设备信息 */ public Map apiUpdateDevInfo(IoTAPIQuery iotAPIQuery) { - IoTDevice instance = IoTDevice.builder().creatorId(iotAPIQuery.getIotUnionId()) - .iotId(iotAPIQuery.getIotId()).build(); + IoTDevice instance = + IoTDevice.builder() + .creatorId(iotAPIQuery.getIotUnionId()) + .iotId(iotAPIQuery.getIotId()) + .build(); instance = ioTDeviceMapper.selectOne(instance); if (instance == null) { - throw new IoTException(IoTErrorCode.DEV_NOT_FIND.getName(), - IoTErrorCode.DEV_NOT_FIND.getCode()); + throw new IoTException( + IoTErrorCode.DEV_NOT_FIND.getName(), IoTErrorCode.DEV_NOT_FIND.getCode()); } if (StrUtil.isNotBlank(iotAPIQuery.getDeviceName())) { instance.setDeviceName(iotAPIQuery.getDeviceName()); @@ -119,18 +136,18 @@ public class IoTDeviceService { if (StrUtil.isNotBlank(iotAPIQuery.getDetail())) { instance.setDetail(iotAPIQuery.getDetail()); } - if (StrUtil.isNotBlank(iotAPIQuery.getLatitude()) && StrUtil.isNotBlank( - iotAPIQuery.getLongitude())) { + if (StrUtil.isNotBlank(iotAPIQuery.getLatitude()) + && StrUtil.isNotBlank(iotAPIQuery.getLongitude())) { instance.setCoordinate( StrUtil.join(",", iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude())); // 更新设备所属经纬度 - SupportMapAreas supportMapAreas = supportMapAreasMapper.selectMapAreas( - iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude()); + SupportMapAreas supportMapAreas = + supportMapAreasMapper.selectMapAreas( + iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude()); if (supportMapAreas == null) { - log.info("查询区域id为空,lot={},lat={}", iotAPIQuery.getLongitude(), - iotAPIQuery.getLatitude()); + log.info("查询区域id为空,lot={},lat={}", iotAPIQuery.getLongitude(), iotAPIQuery.getLatitude()); } else { instance.setAreasId(supportMapAreas.getId()); } @@ -144,20 +161,31 @@ public class IoTDeviceService { return result; } - @Cacheable(cacheNames = "selectDevCount", unless = "#result == null", keyGenerator = "redisKeyGenerate") + @Cacheable( + cacheNames = "selectDevCount", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") public boolean selectDevCount(IoTAPIQuery iotAPIQuery) { if (iotAPIQuery == null) { throw new IoTException("参数不能为空"); } - IoTDevice instance = IoTDevice.builder().creatorId(iotAPIQuery.getIotUnionId()) - .deviceId(iotAPIQuery.getDeviceId()).application(iotAPIQuery.getApplicationId()) - .productKey(iotAPIQuery.getProductKey()).iotId(iotAPIQuery.getIotId()).build(); + IoTDevice instance = + IoTDevice.builder() + .creatorId(iotAPIQuery.getIotUnionId()) + .deviceId(iotAPIQuery.getDeviceId()) + .application(iotAPIQuery.getApplicationId()) + .productKey(iotAPIQuery.getProductKey()) + .iotId(iotAPIQuery.getIotId()) + .build(); int count = ioTDeviceMapper.selectCount(instance); return count > 0; } - @Cacheable(cacheNames = "iot_dev_action", unless = "#result == null", keyGenerator = "redisKeyGenerate") + @Cacheable( + cacheNames = "iot_dev_action", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") public IoTDeviceDTO lifeCycleDevInstance(IoTDeviceQuery query) { if (query == null || query.emptyParams()) { log.warn("no query condition"); @@ -177,19 +205,24 @@ public class IoTDeviceService { * *

productKey 产品唯一编号 */ - @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", keyGenerator = "redisKeyGenerate") + @Cacheable( + cacheNames = "iot_dev_instance_bo", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") public IoTDeviceDTO selectDevInstanceBO(Map map) { if (map == null || map.isEmpty()) { throw new IoTException("deviceId can not be null"); } IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); - if (ioTDeviceDTO != null) { - } + if (ioTDeviceDTO != null) {} return ioTDeviceDTO; } - @MultiLevelCacheable(cacheNames = "apiIoTDeviceVOInfo", unless = "#result == null", keyGenerator = "redisKeyGenerate") + @MultiLevelCacheable( + cacheNames = "apiIoTDeviceVOInfo", + unless = "#result == null", + keyGenerator = "redisKeyGenerate") public IoTDeviceVO apiIoTDeviceVOInfo(IoTAPIQuery query) { IoTDeviceVO ioTDeviceVO = ioTDeviceMapper.apiDeviceInfo(query); return ioTDeviceVO; @@ -221,8 +254,10 @@ public class IoTDeviceService { * *

productKey 产品唯一编号 */ - @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", key = - "'selectDevInstanceBO" + ":'+#productKey+#deviceId") + @Cacheable( + cacheNames = "iot_dev_instance_bo", + unless = "#result == null", + key = "'selectDevInstanceBO" + ":'+#productKey+#deviceId") public IoTDeviceDTO selectDevInstanceBO(String productKey, String deviceId) { if (StrUtil.isEmpty(productKey) || StrUtil.isEmpty(deviceId)) { throw new IoTException("productKey or deviceId can not be null"); @@ -237,7 +272,10 @@ public class IoTDeviceService { return null; } - @Cacheable(cacheNames = "iot_dev_instance_bo", unless = "#result == null", key = "''+#productKey+#deviceId") + @Cacheable( + cacheNames = "iot_dev_instance_bo", + unless = "#result == null", + key = "''+#productKey+#deviceId") public IoTDevice selectDevInstance(String productKey, String deviceId) { if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceId)) { return null; @@ -267,8 +305,9 @@ public class IoTDeviceService { if (StrUtil.isBlank(iotId)) { throw new IoTException("iotId can not be null"); } - IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO( - BeanUtil.beanToMap(IoTAPIQuery.builder().iotId(iotId).build())); + IoTDeviceDTO ioTDeviceDTO = + ioTDeviceMapper.selectIoTDeviceBO( + BeanUtil.beanToMap(IoTAPIQuery.builder().iotId(iotId).build())); return ioTDeviceDTO; } @@ -278,9 +317,7 @@ public class IoTDeviceService { return metadataBO; } - /** - * 根据 extDeviceId 删除设备信息 - */ + /** 根据 extDeviceId 删除设备信息 */ @Transactional public int delDevInstance(String iotId) { if (StrUtil.isBlank(iotId)) { @@ -290,11 +327,16 @@ public class IoTDeviceService { int dev = ioTDeviceMapper.delete(IoTDevice.builder().iotId(iotId).build()); int tags = ioTDeviceTagsMapper.delete(IoTDeviceTags.builder().iotId(iotId).build()); int sha = ioTDeviceShadowMapper.delete(IoTDeviceShadow.builder().iotId(iotId).build()); - int subscribe = ioTDeviceSubscribeMapper.delete( - IoTDeviceSubscribe.builder().iotId(iotId).build()); + int subscribe = + ioTDeviceSubscribeMapper.delete(IoTDeviceSubscribe.builder().iotId(iotId).build()); int fence = ioTDeviceFenceRelMapper.deleteFenceInstance(iotId); - log.info("删除设备 tag={},platform={},shadow={},subscribe={},fence={}", tags, dev, sha, - subscribe, fence); + log.info( + "删除设备 tag={},platform={},shadow={},subscribe={},fence={}", + tags, + dev, + sha, + subscribe, + fence); iotCacheRemoveService.removeDevProtocolCache(); return dev; } @@ -302,13 +344,19 @@ public class IoTDeviceService { public void addDevHistory(String iotId) { IoTDevice ioTDevice = ioTDeviceMapper.selectOne(IoTDevice.builder().iotId(iotId).build()); if (ioTDevice != null && ioTDevice.getRegistryTime() != null) { - IoTDeviceHistoryBO ioTDeviceHistoryBO = IoTDeviceHistoryBO.builder() - .deviceId(ioTDevice.getDeviceId()).deviceName(ioTDevice.getDeviceName()) - .productKey(ioTDevice.getProductKey()) - .firstOnlineTime(ioTDevice.getRegistryTime().longValue()) - .creater(ioTDevice.getCreatorId()).createTime(ioTDevice.getCreateTime()) - .deleteTime(System.currentTimeMillis()).coordinate(ioTDevice.getCoordinate()).build(); -// ioTDeviceMapper.insertDevInstanceHistory(ioTDeviceHistoryBO); + IoTDeviceHistoryBO ioTDeviceHistoryBO = + IoTDeviceHistoryBO.builder() + .deviceId(ioTDevice.getDeviceId()) + .deviceName(ioTDevice.getDeviceName()) + .productKey(ioTDevice.getProductKey()) + .firstOnlineTime(ioTDevice.getRegistryTime().longValue()) + .creater(ioTDevice.getCreatorId()) + .createTime(ioTDevice.getCreateTime()) + .deleteTime(System.currentTimeMillis()) + .coordinate(ioTDevice.getCoordinate()) + .build(); + // ioTDeviceMapper.insertDevInstanceHistory(ioTDeviceHistoryBO); } } + } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java index 8cb95c49ebb82b61c9b765b89ee1aa685da4aa59..83bf50dff3b87aaa29f6cacbe6d2558e6fe518c8 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTDeviceShadowService.java @@ -41,10 +41,13 @@ import java.util.concurrent.TimeUnit; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; /** + * 设备影子处理 + * * @version 1.0 @Author Aleo * @since 2025/9/17 */ @@ -67,14 +70,128 @@ public class IoTDeviceShadowService { // 注入优化版服务 @Resource private IoTDeviceShadowServiceOptimized ioTDeviceShadowServiceOptimized; - public IoTDeviceShadow getDeviceShadow(String iotId) { - return ioTDeviceShadowMapper.getDeviceShadow(iotId); + @Value("${shadow.cache.enabled:true}") + private boolean shadowCacheEnabled; + + @Value("${shadow.cache.key-prefix:shadow}") + private String shadowKeyPrefix; + + @Value("${shadow.cache.ttl-seconds:604800}") // 7 days + private long shadowCacheTtlSeconds; + + @Value("${shadow.flush.zset-key:shadow:flush}") + private String shadowFlushZsetKey; + + @Value("${shadow.flush.base-interval-ms:7200000}") // 24h + private long flushBaseIntervalMs; + + @Value("${shadow.flush.jitter-rate:0.2}") // ±20% + private double flushJitterRate; + + // 强制刷盘配置 - 使用默认值,不增加环境配置 + private static final long MAX_DELAY_MS = 86400000L; // 24小时最大延迟 + private static final long MIN_INTERVAL_MS = 3600000L; // 最小间隔:1小时 + private static final int VERSION_THRESHOLD = 10; // 版本号阈值:10 + private static final long FORCE_FLUSH_DELAY_MS = 30000L; // 强制刷盘延迟:30秒(确保能被5分钟扫描间隔捕获) + + private String buildShadowKey(String iotId) { + return shadowKeyPrefix + ":" + iotId; + } + + private Long calcNextFlushAtMs(String iotId) { + long now = System.currentTimeMillis(); + + // 获取当前已设置的刷盘时间 + Double currentScore = stringRedisTemplate.opsForZSet().score(shadowFlushZsetKey, iotId); + + if (currentScore != null) { + long currentFlushTime = currentScore.longValue(); + long timeSinceLastFlush = now - (currentFlushTime - flushBaseIntervalMs); + + // 1. 最大延迟时间检查 + if (timeSinceLastFlush > MAX_DELAY_MS) { + log.info("[ShadowFlush] 最大延迟强制刷盘: iotId={}, 延迟时间={}ms", iotId, timeSinceLastFlush); + return now + FORCE_FLUSH_DELAY_MS; + } + + // 2. 最小间隔检查 + if (timeSinceLastFlush >= MIN_INTERVAL_MS) { + log.info("[ShadowFlush] 最小间隔强制刷盘: iotId={}, 间隔时间={}ms", iotId, timeSinceLastFlush); + return now + FORCE_FLUSH_DELAY_MS; + } + + // 3. 版本号检查 + if (checkVersionThreshold(iotId)) { + log.info("[ShadowFlush] 版本号强制刷盘: iotId={}", iotId); + return now + FORCE_FLUSH_DELAY_MS; + } + + // 如果当前刷盘时间还没到,保持原时间 + if (currentFlushTime > now) { + return currentFlushTime; + } + } + + // 正常计算新的刷盘时间 + double jitter = (Math.random() * 2 * flushJitterRate) - flushJitterRate; + long offset = (long) (flushBaseIntervalMs * (1 + jitter)); + return now + Math.max(0L, offset); + } + + private Shadow readShadowFromCache(String iotId) { + if (!shadowCacheEnabled) { + return null; + } + final String json = buildAndGetFromRedis(iotId); + if (json == null) return null; + try { + return JSONUtil.toBean(json, Shadow.class); + } catch (Exception e) { + log.warn("[Shadow][Cache] parse error iotId={}, err={}", iotId, e.getMessage()); + return null; + } + } + + private String buildAndGetFromRedis(String iotId) { + String key = buildShadowKey(iotId); + String json = stringRedisTemplate.opsForValue().get(key); + if (StrUtil.isBlank(json)) { + return null; + } + return json; + } + + private void writeShadowToCache(String iotId, Shadow shadow) { + if (!shadowCacheEnabled || shadow == null) { + return; + } + String key = buildShadowKey(iotId); + stringRedisTemplate + .opsForValue() + .set(key, JSONUtil.toJsonStr(shadow), shadowCacheTtlSeconds, TimeUnit.SECONDS); + } + + private void markShadowDirtyForFlush(String iotId) { + if (!shadowCacheEnabled) { + return; + } + try { + Long nextAt = calcNextFlushAtMs(iotId); + stringRedisTemplate.opsForZSet().add(shadowFlushZsetKey, iotId, nextAt.doubleValue()); + } catch (Exception e) { + log.warn("[Shadow][Flush] mark dirty failed iotId={}, err={}", iotId, e.getMessage()); + } } public JSONObject getDeviceShadowObj(String productKey, String deviceId) { if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceId)) { return null; } + String s = buildAndGetFromRedis(productKey + deviceId); + // 缓存拿到,直接返回缓存的 + if (StrUtil.isNotBlank(s)) { + return JSONUtil.parseObj(s); + } String shadowMetadata = ioTDeviceShadowMapper.getShadowMetadata(productKey, deviceId); if (StrUtil.isBlank(shadowMetadata)) { return null; @@ -82,20 +199,34 @@ public class IoTDeviceShadowService { return JSONUtil.parseObj(shadowMetadata); } - // @Cacheable(cacheNames = "iot_dev_shadow_bo", key = "''+#iotId", unless = "#result==null") + /** 格式化的属性 */ public List getDevState(String iotId) { List result = new ArrayList<>(); - IoTDeviceShadow ioTDeviceShadow = - ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); - if (ioTDeviceShadow == null) { - // throw new IoTException("设备=[" + iotId + "]影子不存在,清检查"); - log.warn("设备=[" + iotId + "]影子不存在,清检查"); - return result; - } - Shadow shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); - JSONObject properties = shadow.getState().getReported(); - JSONObject desireProperties = shadow.getState().getDesired(); + // Cache-first + Shadow cached = readShadowFromCache(iotId); + Shadow shadow = null; + IoTDeviceShadow ioTDeviceShadow = null; + if (cached != null) { + shadow = cached; + } else { + ioTDeviceShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (ioTDeviceShadow == null) { + log.warn("设备=[{}]影子不存在,清检查", iotId); + return result; + } + shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + // backfill cache + writeShadowToCache(iotId, shadow); + } + JSONObject properties = + shadow.getState() != null ? shadow.getState().getReported() : new JSONObject(); + JSONObject desireProperties = + shadow.getState() != null ? shadow.getState().getDesired() : new JSONObject(); + Shadow finalShadow = shadow; + JSONObject finalProperties = properties; + JSONObject finalDesireProperties = desireProperties; Map map = new HashMap<>(); map.put("iotId", iotId); IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); @@ -106,24 +237,32 @@ public class IoTDeviceShadowService { } LogStorePolicyDTO storePolicyDTO = iotProductDeviceService.getProductLogStorePolicy(ioTDeviceDTO.getProductKey()); - // 过滤没有上报过的属性 propertyMetadataList.stream() - .filter(s -> properties.get(s.getId()) != null) + .filter(s -> finalProperties.get(s.getId()) != null) .forEach( s -> { - Object obj = properties.get(s.getId()); + Object obj = finalProperties.get(s.getId()); IoTDevicePropertiesBO entity = new IoTDevicePropertiesBO(); - entity.setDesireValue(desireProperties.get(s.getId())); + Long ts = + finalShadow.getMetadata() != null + && finalShadow.getMetadata().getReported() != null + && finalShadow.getMetadata().getReported().getJSONObject(s.getId()) + != null + ? finalShadow + .getMetadata() + .getReported() + .getJSONObject(s.getId()) + .getLong("timestamp") + : DateUtil.currentSeconds(); + entity.setDesireValue(finalDesireProperties.get(s.getId())); entity.withValue(s.getValueType(), obj); entity.setPropertyName(s.getName()); entity.setIotId(iotId); entity.setDeviceId(ioTDeviceDTO.getDeviceId()); - entity.setTimestamp( - shadow.getMetadata().getReported().getJSONObject(s.getId()).getLong("timestamp")); + entity.setTimestamp(ts); entity.setProperty(s.getId()); entity.setStoragePolicy(storePolicyDTO.getProperties().containsKey(s.getId())); - // 期望值 - if (desireProperties.get(s.getId()) != null) { + if (finalDesireProperties.get(s.getId()) != null) { entity.setCustomized(IoTConstant.DEVICE_SHADOW_DESIRED_PROPERTY); } result.add(entity); @@ -132,26 +271,26 @@ public class IoTDeviceShadowService { return result; } - /** 数据为空也返回结果 */ + /** 数据为空也返回结果 - cache first */ public List getDevStateWithNullResult(String iotId) { List result = new ArrayList<>(); - IoTDeviceShadow ioTDeviceShadow = - ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); - Shadow shadow = null; - JSONObject properties = null; - JSONObject desireProperties = null; - - if (ioTDeviceShadow == null) { - // throw new IoTException("设备=[" + iotId + "]影子不存在,清检查"); - log.warn("设备=[" + iotId + "]影子不存在,清检查"); - shadow = Shadow.builder().build(); - properties = new JSONObject(); - desireProperties = new JSONObject(); - } else { - shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); - properties = shadow.getState().getReported(); - desireProperties = shadow.getState().getDesired(); + Shadow shadow = readShadowFromCache(iotId); + if (shadow == null) { + IoTDeviceShadow ioTDeviceShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (ioTDeviceShadow != null && StrUtil.isNotBlank(ioTDeviceShadow.getMetadata())) { + shadow = JSONUtil.toBean(ioTDeviceShadow.getMetadata(), Shadow.class); + writeShadowToCache(iotId, shadow); + } else { + shadow = Shadow.builder().build(); + } } + + JSONObject properties = + shadow.getState() != null ? shadow.getState().getReported() : new JSONObject(); + JSONObject desireProperties = + shadow.getState() != null ? shadow.getState().getDesired() : new JSONObject(); + Map map = new HashMap<>(); map.put("iotId", iotId); IoTDeviceDTO ioTDeviceDTO = ioTDeviceMapper.selectIoTDeviceBO(map); @@ -163,7 +302,6 @@ public class IoTDeviceShadowService { if (CollectionUtil.isEmpty(propertyMetadataList)) { return result; } - // 过滤没有上报过的属性 Shadow finalShadow = shadow; JSONObject finalProperties = properties; JSONObject finalDesireProperties = desireProperties; @@ -171,25 +309,26 @@ public class IoTDeviceShadowService { s -> { Object obj = finalProperties.get(s.getId()); IoTDevicePropertiesBO entity = new IoTDevicePropertiesBO(); - long times = 0; if (obj != null) { entity.withValue(s.getValueType(), obj); entity.setTimestamp( - finalShadow - .getMetadata() - .getReported() - .getJSONObject(s.getId()) - .getLong("timestamp")); + finalShadow.getMetadata() != null + && finalShadow.getMetadata().getReported() != null + && finalShadow.getMetadata().getReported().getJSONObject(s.getId()) != null + ? finalShadow + .getMetadata() + .getReported() + .getJSONObject(s.getId()) + .getLong("timestamp") + : DateUtil.currentSeconds()); } else { entity.withValue(s.getValueType(), null); - entity.setTimestamp(System.currentTimeMillis()); + entity.setTimestamp(DateUtil.currentSeconds()); } - entity.withValue(s.getValueType(), obj); entity.setPropertyName(s.getName()); entity.setIotId(iotId); entity.setDeviceId(ioTDeviceDTO.getDeviceId()); entity.setProperty(s.getId()); - // 期望值 if (finalDesireProperties.get(s.getId()) != null) { entity.setDesireValue(finalDesireProperties.get(s.getId())); entity.setCustomized(IoTConstant.DEVICE_SHADOW_DESIRED_PROPERTY); @@ -292,7 +431,7 @@ public class IoTDeviceShadowService { ioTDeviceShadow.setMetadata(JSONUtil.toJsonStr(shadow)); ioTDeviceShadowMapper.insertSelective(ioTDeviceShadow); // 代理调用内部方法,清除缓存 - iotCacheRemoveService.removeDevInstanceBOCache(); + // iotCacheRemoveService.removeDevInstanceBOCache(); } else { // 更新最后通信时间 IoTDeviceShadow ioTDeviceShadow = @@ -398,7 +537,6 @@ public class IoTDeviceShadowService { metadata.getReported() != null ? metadata.getReported() : new JSONObject(); JSONObject metadataDesired = metadata.getReported() != null ? metadata.getDesired() : new JSONObject(); - Timestamp timestamp = new Timestamp(DateUtil.currentSeconds()); data.forEach( (key, value) -> { @@ -407,12 +545,12 @@ public class IoTDeviceShadowService { // 删除当前reply回复的期望 stateDesired.remove(key); metadataDesired.remove(key); - metadataReported.set(key, timestamp); + metadataReported.set(key, new Timestamp(DateUtil.currentSeconds())); } // 回复内有数据时以回复为准 if (!"".equals(value)) { stateReported.set(key, value); - metadataReported.set(key, timestamp); + metadataReported.set(key, new Timestamp(DateUtil.currentSeconds())); } }); state.setReported(stateReported); @@ -441,20 +579,71 @@ public class IoTDeviceShadowService { /** 优化的设备影子处理方法 - 直接调用优化类 */ public void doShadow(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + if (shadowCacheEnabled) { + doShadowCache(upRequest, ioTDeviceDTO); + return; + } doShadowOriginal(upRequest, ioTDeviceDTO); - // try { - // // 直接调用优化类的doShadow方法 - // if (ioTDeviceShadowServiceOptimized != null) { - // ioTDeviceShadowServiceOptimized.doShadow(upRequest, ioTDeviceDTO); - // } else { - // log.warn("优化版服务未注入,使用原方法"); - // doShadowOriginal(upRequest, ioTDeviceDTO); - // } - // } catch (Exception e) { - // log.error("优化方法处理失败,降级到原方法: iotId={}, error={}", upRequest.getIotId(), e.getMessage(), - // e); - // // 降级处理:使用原来的同步方式 - // doShadowOriginal(upRequest, ioTDeviceDTO); - // } + } + + /** 新增:仅缓存与标记刷盘的影子处理,不改动原有 doShadowOriginal */ + public void doShadowCache(UPRequest upRequest, IoTDeviceDTO ioTDeviceDTO) { + if (!shadowCacheEnabled) { + // 回退到原始逻辑 + doShadowOriginal(upRequest, ioTDeviceDTO); + return; + } + String iotId = ioTDeviceDTO.getIotId(); + Shadow shadow = readShadowFromCache(iotId); + if (shadow == null) { + // fallback DB + IoTDeviceShadow fromDb = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (fromDb != null && StrUtil.isNotBlank(fromDb.getMetadata())) { + shadow = JSONUtil.toBean(fromDb.getMetadata(), Shadow.class); + } else { + shadow = Shadow.builder().timestamp(DateUtil.currentSeconds()).version(1L).build(); + } + } + // 合并期望/上报并自增版本 + doDesired(shadow, ioTDeviceDTO, upRequest); + // 写缓存 + writeShadowToCache(iotId, shadow); + // 标记刷盘 + markShadowDirtyForFlush(iotId); + // 清除相关业务缓存 + // iotCacheRemoveService.removeDevInstanceBOCache(); + } + + /** 检查版本号是否超过阈值,需要强制刷盘 */ + private boolean checkVersionThreshold(String iotId) { + try { + String cacheJson = stringRedisTemplate.opsForValue().get(buildShadowKey(iotId)); + if (StrUtil.isBlank(cacheJson)) { + return false; + } + + JSONObject shadow = JSONUtil.parseObj(cacheJson); + Long currentVersion = shadow.getLong("version", 1L); + + IoTDeviceShadow dbShadow = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + if (dbShadow != null && StrUtil.isNotBlank(dbShadow.getMetadata())) { + // 数据库记录存在:正常比较 + JSONObject dbShadowObj = JSONUtil.parseObj(dbShadow.getMetadata()); + Long dbVersion = dbShadowObj.getLong("version", 1L); + + return currentVersion != null + && dbVersion != null + && (currentVersion - dbVersion) >= VERSION_THRESHOLD; + } else { + // 数据库记录不存在:立即触发刷盘(创建记录) + log.info("[ShadowFlush] 数据库记录不存在,立即刷盘: iotId={}, currentVersion={}", iotId, currentVersion); + return true; + } + } catch (Exception e) { + log.warn("[ShadowFlush] 版本号检查失败: iotId={}, error={}", iotId, e.getMessage()); + } + return false; } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java index 156191fb5863d0d040310508ad6781ee8b4a66c3..355ad38fb2f3337fbe54e047f957f46c47e7cd2f 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/impl/IoTProductDeviceService.java @@ -343,12 +343,7 @@ public class IoTProductDeviceService { return productsMap; } - /** - * 查询网络类型 - * - * @param productKey 产品key - * @return int 总数 - */ + /** 查询网络类型 */ @Cacheable(cacheNames = "selectNetworkUnionId", key = "#productKey", unless = "#result == null") public String selectNetworkUnionId(String productKey) { if (StrUtil.isBlank(productKey)) { @@ -356,4 +351,20 @@ public class IoTProductDeviceService { } return ioTProductMapper.selectNetworkUnionId(productKey); } + + /** 查询产品配置信息 */ + @Cacheable( + cacheNames = "getProductConfiguration", + key = "#productKey", + unless = "#result == null") + public JSONObject getProductConfiguration(String productKey) { + // status-0 正常 + IoTProduct product = + IoTProduct.builder().productKey(productKey).state(IoTConstant.NORMAL.byteValue()).build(); + product = ioTProductMapper.selectOne(product); + if (product == null || StrUtil.isBlank(product.getConfiguration())) { + return null; + } + return JSONUtil.parseObj(product.getConfiguration()); + } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java index 88155f85841c7a8fcd912f4fd0258ba94e9a5bd1..06e28a6ae9f4610ce3948bf7f920b13613060995 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/AbstractIoTDeviceLogService.java @@ -12,7 +12,6 @@ package cn.universal.dm.device.service.log; -import cn.hutool.core.date.DateUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONArray; @@ -21,10 +20,10 @@ import cn.hutool.json.JSONUtil; import cn.universal.common.constant.IoTConstant.MessageType; import cn.universal.core.message.UPRequest; import cn.universal.core.metadata.DeviceMetadata; +import cn.universal.dm.device.constant.DeviceManagerConstant; import cn.universal.dm.device.service.impl.IoTProductDeviceService; import cn.universal.persistence.base.BaseUPRequest; import cn.universal.persistence.dto.IoTDeviceDTO; -import cn.universal.persistence.dto.LogStorePolicyDTO; import cn.universal.persistence.entity.IoTDeviceEvents; import cn.universal.persistence.entity.IoTDeviceLog; import cn.universal.persistence.entity.IoTDeviceLogMetadata; @@ -32,6 +31,7 @@ import cn.universal.persistence.entity.IoTDeviceLogMetadata.IoTDeviceLogMetadata import cn.universal.persistence.entity.IoTProduct; import cn.universal.persistence.mapper.IoTProductMapper; import jakarta.annotation.Resource; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -56,7 +56,7 @@ public abstract class AbstractIoTDeviceLogService implements IIoTDeviceLogServic IoTDeviceLogMetadataBuilder builder(UPRequest up) { final IoTDeviceLogMetadataBuilder ioTDeviceLogMetadataBuilder = IoTDeviceLogMetadata.builder() - .createTime(Integer.parseInt(DateUtil.currentSeconds() + "")) + .createTime(LocalDateTime.now()) .deviceId(up.getDeviceId()) .deviceName(StrUtil.sub(up.getDeviceName(), 0, 30)) .ext1(JSONUtil.toJsonStr(up.getData())) @@ -71,20 +71,20 @@ public abstract class AbstractIoTDeviceLogService implements IIoTDeviceLogServic IoTDeviceLog.builder() .content(peopertiesOrEventData(upRequest)) .deviceId(ioTDeviceDTO.getDeviceId()) - // .extDeviceId(ioTDeviceDTO.getExtDeviceId()) .iotId(ioTDeviceDTO.getIotId()) .productKey(ioTDeviceDTO.getProductKey()) .messageType(upRequest.getMessageType().name()) .event(functionOrEvent(upRequest)) .commandId(upRequest.getCommandId()) .commandStatus(upRequest.getCommandStatus()) - .createTime( - upRequest.getTime() == null - ? System.currentTimeMillis() / 1000 - : upRequest.getTime() / 1000) + .createTime(LocalDateTime.now()) .deviceName(StrUtil.sub(ioTDeviceDTO.getDeviceName(), 0, 30)) - .point(ioTDeviceDTO.getCoordinate()) .build(); + if (upRequest.getProperties() != null + && upRequest.getProperties().containsKey(DeviceManagerConstant.COORDINATE)) { + log.setPoint(ioTDeviceDTO.getCoordinate()); + } + return log; } @@ -123,7 +123,6 @@ public abstract class AbstractIoTDeviceLogService implements IIoTDeviceLogServic List ioTDeviceEventsList = new ArrayList<>(); JSONArray properties = metadata.getJSONArray("events"); if (properties != null) { - LogStorePolicyDTO storePolicy = iotProductDeviceService.getProductLogStorePolicy(productKey); for (Object object : properties) { JSONObject jsonObject = JSONUtil.parseObj(object); JSONObject expands = JSONUtil.parseObj(jsonObject.getStr("expands")); diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java index 12504e7d2ec2a88513f6365e6a43286a93270b63..b6db7a8cc1787064a3641f27bac823144d24c69a 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/ClickHouseIoTDeviceLogService.java @@ -239,7 +239,7 @@ public class ClickHouseIoTDeviceLogService extends AbstractIoTDeviceLogService { } if (MapUtil.isNotEmpty(logQuery.getParams()) && ObjectUtil.isNotNull(logQuery.getParams().get("endCreateTime"))) { - builder1.append(" AND create_time <= " + logQuery.getParams().get("beginCreateTime")); + builder1.append(" AND create_time <= " + logQuery.getParams().get("endCreateTime")); } builder1.append(" ORDER BY create_time DESC "); try { diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java index 2fe93d5c84131a9b78a560ef0081e63b5ab04300..180d436ee9ca3c273ac8b1a752ef099c8c3b081a 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/log/MysqlIoTDeviceLogService.java @@ -129,7 +129,7 @@ public class MysqlIoTDeviceLogService extends AbstractIoTDeviceLogService { /** 产品数据存储策略,不为空则保存日志 */ if (StrUtil.isNotBlank(ioTProduct.getStorePolicy())) { try { - ioTDeviceLog.setPoint(ioTDeviceDTO.getCoordinate()); +// ioTDeviceLog.setPoint(ioTDeviceDTO.getCoordinate()); // ioTDeviceLogMapper.insertSelective(ioTDeviceLog); // 日志分表 暂时双写单读 if (enable) { diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolClusterService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolClusterService.java new file mode 100644 index 0000000000000000000000000000000000000000..8158255a107ffc76f99f2f012a8a6df2f34d4d87 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolClusterService.java @@ -0,0 +1,83 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.protocol; + +/** + * 协议集群服务接口 + * + *

用于管理协议服务器的集群操作,包括启动、停止、重启等集群化操作 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface ProtocolClusterService { + + /** + * 集群启动(本地启动 + 广播) + * + * @param productKey 产品Key + * @return 是否启动成功 + */ + boolean start(String productKey); + + /** + * 集群停止(本地停止 + 广播) + * + * @param productKey 产品Key + * @return 是否停止成功 + */ + boolean stop(String productKey); + + /** + * 集群重启(本地重启 + 广播) + * + * @param productKey 产品Key + * @return 是否重启成功 + */ + boolean restart(String productKey); + + /** + * 本地启动(不广播) + * + * @param productKey 产品Key + * @return 是否启动成功 + */ + boolean startLocal(String productKey); + + /** + * 本地停止(不广播) + * + * @param productKey 产品Key + * @return 是否停止成功 + */ + boolean stopLocal(String productKey); + + /** + * 本地重启(不广播) + * + * @param productKey 产品Key + * @return 是否重启成功 + */ + boolean restartLocal(String productKey); + + /** + * 检查产品服务器是否存活 + * + * @param productKey 产品Key + * @return 是否存活 + */ + default boolean isProductServerAlive(String productKey) { + return false; + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolServerManager.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolServerManager.java new file mode 100644 index 0000000000000000000000000000000000000000..2bcbf2d38a836b8ee18e306c962907dcecb0a42d --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/ProtocolServerManager.java @@ -0,0 +1,41 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.protocol; + +/** + * 协议服务器管理器接口 + * + *

用于管理协议服务器的生命周期,包括启动、停止、重启等操作 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface ProtocolServerManager { + + /** + * 获取服务器实例 + * + * @param productKey 产品Key + * @return 服务器实例,如果不存在则返回null + */ + Object getServerInstance(String productKey); + + /** + * 检查服务器是否存活 + * + * @param serverInstance 服务器实例 + * @return 是否存活 + */ + boolean isAlive(Object serverInstance); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/UdpServerBootstrap.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/UdpServerBootstrap.java new file mode 100644 index 0000000000000000000000000000000000000000..89767c10fb2dd4ab419ca29dd14cbb9857222233 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/protocol/UdpServerBootstrap.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.protocol; + +/** + * UDP服务器启动器接口 + * + *

用于管理UDP服务器的启动、停止、重启等操作 + * + * @author Aleo + * @version 1.0 + * @since 2025/1/9 + */ +public interface UdpServerBootstrap { + + /** + * 启动产品UDP服务器 + * + * @param productKey 产品Key + * @return 是否启动成功 + */ + boolean startProductUdpServer(String productKey); + + /** + * 停止产品UDP服务器 + * + * @param productKey 产品Key + * @return 是否停止成功 + */ + boolean stopProductUdpServer(String productKey); + + /** + * 重启产品UDP服务器 + * + * @param productKey 产品Key + * @return 是否重启成功 + */ + boolean restartProductUdpServer(String productKey); + + /** + * 检查产品UDP服务器是否存活 + * + * @param productKey 产品Key + * @return 是否存活 + */ + boolean isProductUdpServerAlive(String productKey); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java index 9fbc012d5f618fadefce313c5fc1cc44c51b3de3..b91e9d744bcb15ac1376c985aa9d23b5bdcf7b46 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/HttpPushStrategy.java @@ -104,6 +104,10 @@ public class HttpPushStrategy implements PushStrategy { // 添加时间戳和签名 String timestamp = String.valueOf(System.currentTimeMillis()); httpRequest.header("X-Timestamp", timestamp); + if (StrUtil.isNotBlank(httpConfig.getHeader()) + && StrUtil.isNotBlank(httpConfig.getSecret())) { + httpRequest.header(httpConfig.getHeader(), httpConfig.getSecret()); + } httpRequest.header(Header.CONTENT_TYPE, "application/json"); // 添加自定义请求头(如果有配置) @@ -121,11 +125,7 @@ public class HttpPushStrategy implements PushStrategy { // 执行请求 HttpResponse response = httpRequest.execute(); String result = response.body(); - log.warn( - "[HTTP推送] 推送, url={}, status={}, messageJson={}", - httpConfig.getUrl(), - response != null ? response.getStatus() : "null", - result); + // 检查响应状态 if (response == null || response.getStatus() != 200) { log.warn( @@ -143,8 +143,11 @@ public class HttpPushStrategy implements PushStrategy { "HTTP状态码错误: " + (response != null ? response.getStatus() : "null"), "HTTP_ERROR"); } - - log.info("[HTTP推送] 推送成功, url={}, deviceId={}", httpConfig.getUrl(), request.getIotId()); + log.info( + "[HTTP推送] 推送成功, url={}, status={}, messageJson={}", + httpConfig.getUrl(), + response != null ? response.getStatus() : "null", + result); removeSuccess(httpConfig.getUrl()); return IoTPushResult.success( diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MQTTPushService.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MQTTPushService.java new file mode 100644 index 0000000000000000000000000000000000000000..ce4965b260c9ffb93270593c007faaa70af17b96 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MQTTPushService.java @@ -0,0 +1,32 @@ +/* + * + * Copyright (c) 2025, IoT-universal. All Rights Reserved. + * + * @Description: 本文件由 AleoXin 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: AleoXin + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ +package cn.universal.dm.device.service.push; + +/** + * mqtt消息推送接口 + * + * @version 1.0 @Author Aleo + * @since 2025/08/30 + */ +public interface MQTTPushService { + + /** + * 消息推送 + * + * @param topic 主题 + * @param payload 有效载荷 + * @param qos qos + * @param retained 是否保存 + * @return + */ + boolean publishMessage(String topic, byte[] payload, int qos, boolean retained); +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java index a8e2840fe66ee7b14b0c352ca8d9a301cd1eb9bb..46d104d51c751d845092a9e9cd28f0fae255554f 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/MqttPushStrategy.java @@ -12,11 +12,13 @@ package cn.universal.dm.device.service.push; -import cn.hutool.core.util.StrUtil; import cn.universal.dm.device.entity.IoTPushResult; import cn.universal.persistence.base.BaseUPRequest; import cn.universal.persistence.entity.bo.UPPushBO; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** @@ -35,6 +37,13 @@ public class MqttPushStrategy implements PushStrategy { this.mqttConfig = mqttConfig; } + @Autowired + @Qualifier("sysMQTTManager") + private MQTTPushService mqttPushService; + + @Value("${mqtt.cfg.defined.thing:$thing}") + private String thingPrefix; + @Override public IoTPushResult execute(BaseUPRequest request, String messageJson) { if (mqttConfig == null) { @@ -48,31 +57,21 @@ public class MqttPushStrategy implements PushStrategy { "配置为空", "CONFIG_NULL"); } - try { - if (StrUtil.isBlank(mqttConfig.getTopic())) { - log.warn("[MQTT推送] Topic为空,跳过推送"); - return IoTPushResult.failed( - request.getIoTDeviceDTO().getThirdPlatform(), - request.getProductKey(), - request.getIotId(), - "MQTT", - messageJson, - "Topic为空", - "TOPIC_EMPTY"); - } - - // 构建完整的topic - String fullTopic = mqttConfig.getTopic().replace("#", request.getIotId()); + // 构建完整的topic,不允许修改默认为 + // thingPrefix/${appid}/${productKey}/${deviceId} + String fullTopic = + thingPrefix + + "/" + + request.getIoTDeviceDTO().getAppId() + + "/" + + request.getProductKey() + + "/" + + request.getDeviceId(); log.info("[MQTT推送] 推送到Topic: {}, 消息: {}", fullTopic, request.getIotId()); - // TODO: 实现具体的MQTT推送逻辑 - // 这里可以集成具体的MQTT客户端,比如: - // - Eclipse Paho MQTT Client - // - HiveMQ MQTT Client - // - Spring Integration MQTT - // mqttClient.publish(fullTopic, messageJson.getBytes(), 1, false); + mqttPushService.publishMessage(fullTopic, messageJson.getBytes(), 1, false); // 暂时返回成功(实际实现时需要根据MQTT推送结果返回) return IoTPushResult.success( diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java index 873d74529927ced7cf29808ad46f613a4b8d0f97..90604cfc0ab434360595c4ac04111cb93f149238 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/push/processor/PushRetryProcessor.java @@ -77,7 +77,7 @@ public class PushRetryProcessor implements UPProcessor { /** 获取线程池状态信息 */ public String getThreadPoolStatus() { if (retryExecutor instanceof java.util.concurrent.ThreadPoolExecutor) { - java.util.concurrent.ThreadPoolExecutor executor = + java.util.concurrent.ThreadPoolExecutor executor = (java.util.concurrent.ThreadPoolExecutor) retryExecutor; return String.format( "PushRetry线程池状态: 核心线程数=%d, 最大线程数=%d, 当前线程数=%d, 活跃线程数=%d, 队列大小=%d, 已完成任务数=%d", @@ -162,10 +162,10 @@ public class PushRetryProcessor implements UPProcessor { // 添加到重试队列 redisTemplate.opsForList().rightPush(retryKey, retryData); redisTemplate.expire(retryKey, java.time.Duration.ofDays(1)); - log.debug("[推送重试] Redis添加队列成功: deviceId={}, channel={}", + log.debug("[推送重试] Redis添加队列成功: deviceId={}, channel={}", result.getDeviceId(), result.getChannel()); } catch (Exception e) { - log.error("[推送重试] Redis添加队列失败: deviceId={}, channel={}", + log.error("[推送重试] Redis添加队列失败: deviceId={}, channel={}", result.getDeviceId(), result.getChannel(), e); } }, @@ -178,10 +178,10 @@ public class PushRetryProcessor implements UPProcessor { // 增加重试次数 redisTemplate.opsForValue().increment(countKey); redisTemplate.expire(countKey, java.time.Duration.ofDays(1)); - log.debug("[推送重试] Redis增加计数成功: deviceId={}, channel={}", + log.debug("[推送重试] Redis增加计数成功: deviceId={}, channel={}", result.getDeviceId(), result.getChannel()); } catch (Exception e) { - log.error("[推送重试] Redis增加计数失败: deviceId={}, channel={}", + log.error("[推送重试] Redis增加计数失败: deviceId={}, channel={}", result.getDeviceId(), result.getChannel(), e); } }, @@ -570,10 +570,10 @@ public class PushRetryProcessor implements UPProcessor { try { redisTemplate.opsForList().rightPush(failedKey, failedData); redisTemplate.expire(failedKey, java.time.Duration.ofDays(7)); // 失败记录保留7天 - log.debug("[推送重试] 记录失败推送成功: deviceId={}, channel={}", + log.debug("[推送重试] 记录失败推送成功: deviceId={}, channel={}", result.getDeviceId(), result.getChannel()); } catch (Exception e) { - log.error("[推送重试] 记录失败推送Redis操作失败: deviceId={}, channel={}", + log.error("[推送重试] 记录失败推送Redis操作失败: deviceId={}, channel={}", result.getDeviceId(), result.getChannel(), e); } }, @@ -581,15 +581,15 @@ public class PushRetryProcessor implements UPProcessor { // 异步处理结果,不阻塞当前线程 future.thenRun(() -> { - log.debug("[推送重试] 记录失败推送完成: deviceId={}, channel={}", + log.debug("[推送重试] 记录失败推送完成: deviceId={}, channel={}", result.getDeviceId(), result.getChannel()); }).exceptionally(throwable -> { - log.error("[推送重试] 记录失败推送异常: deviceId={}, channel={}", + log.error("[推送重试] 记录失败推送异常: deviceId={}, channel={}", result.getDeviceId(), result.getChannel(), throwable); return null; }); } catch (Exception e) { - log.error("[推送重试] 记录失败推送失败: deviceId={}, channel={}", + log.error("[推送重试] 记录失败推送失败: deviceId={}, channel={}", result.getDeviceId(), result.getChannel(), e); } } diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/ShadowFlushScheduler.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/ShadowFlushScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..780a65efc0e0fd53349e5d36f4209a1df3c24dde --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/ShadowFlushScheduler.java @@ -0,0 +1,532 @@ +package cn.universal.dm.device.service.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.universal.common.config.InstanceIdProvider; +import cn.universal.dm.device.util.DistributedLockUtil; +import cn.universal.persistence.entity.IoTDevice; +import cn.universal.persistence.entity.IoTDeviceShadow; +import cn.universal.persistence.mapper.IoTDeviceMapper; +import cn.universal.persistence.mapper.IoTDeviceShadowMapper; +import cn.universal.persistence.query.IoTDeviceQuery; +import jakarta.annotation.Resource; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 设备影子定时缓存 + * + *

Periodically flush device shadows from Redis to DB. Uses Redis ZSET (score = nextFlushAtMs) + * and Redisson distributed lock. + */ +@Slf4j +@Component +public class ShadowFlushScheduler { + + @Resource private StringRedisTemplate stringRedisTemplate; + @Resource private IoTDeviceShadowMapper ioTDeviceShadowMapper; + @Resource private IoTDeviceMapper ioTDeviceMapper; + @Resource private DistributedLockUtil distributedLockUtil; + + @Value("${shadow.cache.enabled:true}") + private boolean shadowCacheEnabled; + + @Value("${shadow.cache.key-prefix:shadow}") + private String shadowKeyPrefix; + + @Value("${shadow.flush.enabled:true}") + private boolean flushEnabled; + + @Value("${shadow.flush.zset-key:shadow:flush}") + private String shadowFlushZsetKey; + + @Value("${shadow.flush.scan-interval-ms:300000}") // 5 minutes + private long scanIntervalMs; + + @Value("${shadow.flush.batch-size:1000}") + private int batchSize; + + @Value("${shadow.flush.lock-key:shadow:flush:lock}") + private String lockKey; + + @Value("${shadow.flush.lock-wait-time:10}") + private long lockWaitTime; + + @Value("${shadow.flush.lock-lease-time:60}") + private long lockLeaseTime; + + @Value("${shadow.flush.max-retries:3}") + private int maxRetries; + + @Resource private InstanceIdProvider instanceIdProvider; + + @Scheduled(fixedDelayString = "${shadow.flush.scan-interval-ms:300000}") + public void flushDueShadows() { + if (!shadowCacheEnabled || !flushEnabled) { + log.debug( + "[ShadowFlush] 影子刷盘已禁用: shadowCacheEnabled={}, flushEnabled={}", + shadowCacheEnabled, + flushEnabled); + return; + } + + long now = System.currentTimeMillis(); + String instanceId = instanceIdProvider.getInstanceId(); + log.info("[ShadowFlush] 开始设备影子扫描: scanTime={}, instanceId={}", now, instanceId); + + // 使用分布式锁工具类执行任务,确保集群环境下只有一个实例执行 + Integer result = + distributedLockUtil.tryLockAndExecute( + lockKey, lockWaitTime, lockLeaseTime, TimeUnit.SECONDS, this::executeFlushTask); + + if (result != null) { + log.info( + "[ShadowFlush] 影子扫描完成: processed={}, cost={}ms, instanceId={}", + result, + (System.currentTimeMillis() - now), + instanceId); + } else { + log.debug("[ShadowFlush] 获取分布式锁失败,跳过本次扫描: instanceId={}", instanceId); + } + } + + private String buildShadowKey(String iotId) { + return shadowKeyPrefix + ":" + iotId; + } + + /** 获取缓存数据 */ + private Map getCacheData(Set iotIds) { + Map result = new HashMap<>(); + + for (String iotId : iotIds) { + try { + String cacheJson = stringRedisTemplate.opsForValue().get(buildShadowKey(iotId)); + if (StrUtil.isNotBlank(cacheJson)) { + result.put(iotId, cacheJson); + } + } catch (Exception e) { + log.warn("[ShadowFlush] get cache error iotId={}, err={}", iotId, e.getMessage()); + } + } + + return result; + } + + /** 处理单个设备影子 - 带重试机制 */ + private boolean processShadowWithRetry(String iotId, String cacheJson) { + for (int retry = 0; retry < maxRetries; retry++) { + try { + return processShadow(iotId, cacheJson); + } catch (Exception e) { + if (retry == maxRetries - 1) { + log.error("[ShadowFlush] final retry failed iotId={}, err={}", iotId, e.getMessage()); + return false; + } else { + log.warn("[ShadowFlush] retry {} failed iotId={}, err={}", retry, iotId, e.getMessage()); + try { + Thread.sleep(1000); // 短暂延迟后重试 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return false; + } + } + } + } + return false; + } + + /** 处理单个设备影子 */ + private boolean processShadow(String iotId, String cacheJson) { + IoTDeviceShadow existed = + ioTDeviceShadowMapper.selectOne(IoTDeviceShadow.builder().iotId(iotId).build()); + + if (existed == null) { + IoTDeviceShadow entity = new IoTDeviceShadow(); + entity.setIotId(iotId); + entity.setMetadata(cacheJson); + entity.setUpdateDate(new Date()); + entity.setLastTime(new Date()); + entity.setActiveTime(new Date()); + ioTDeviceShadowMapper.insertSelective(entity); + } else { + String merged = mergeShadowJson(existed.getMetadata(), cacheJson); + existed.setMetadata(merged); + existed.setUpdateDate(new Date()); + existed.setLastTime(new Date()); + ioTDeviceShadowMapper.updateByPrimaryKeySelective(existed); + } + return true; + } + + /** 批量处理设备影子 - 优化数据库操作 */ + private int processBatchShadows(List iotIds, Map cacheData) { + if (CollUtil.isEmpty(iotIds)) { + return 0; + } + try { + // 批量查询已存在的记录 + List existingShadows = ioTDeviceShadowMapper.selectByIotIds(iotIds); + Map existingMap = + existingShadows.stream() + .collect(Collectors.toMap(IoTDeviceShadow::getIotId, Function.identity())); + + // 分离需要插入和更新的记录 + List toInsert = new ArrayList<>(); + List toUpdate = new ArrayList<>(); + Date now = new Date(); + + for (String iotId : iotIds) { + String cacheJson = cacheData.get(iotId); + if (StrUtil.isBlank(cacheJson)) { + continue; + } + if (existingMap.containsKey(iotId)) { + IoTDeviceShadow updated = buildUpdatedShadow(existingMap.get(iotId), cacheJson, now); + toUpdate.add(updated); + } else { + IoTDeviceShadow created = buildNewShadow(iotId, cacheJson, now); + if (created != null) { + toInsert.add(created); + } + } + } + // 批量插入 + if (!toInsert.isEmpty()) { + ioTDeviceShadowMapper.batchInsert(toInsert); + } + + // 批量更新 + if (!toUpdate.isEmpty()) { + ioTDeviceShadowMapper.batchUpdate(toUpdate); + } + + return toInsert.size() + toUpdate.size(); + + } catch (Exception e) { + log.error("[ShadowFlush] batch process error: {}", e.getMessage(), e); + // 回退到单个处理 + int processed = 0; + for (String iotId : iotIds) { + String cacheJson = cacheData.get(iotId); + if (StrUtil.isNotBlank(cacheJson) && processShadowWithRetry(iotId, cacheJson)) { + processed++; + } + } + return processed; + } + } + + private IoTDeviceShadow buildUpdatedShadow(IoTDeviceShadow existed, String cacheJson, Date now) { + if (existed == null) { + return null; + } + String merged = mergeShadowJson(existed.getMetadata(), cacheJson); + existed.setMetadata(merged); + existed.setUpdateDate(now); + existed.setLastTime(now); + return existed; + } + + private IoTDeviceShadow buildNewShadow(String iotId, String cacheJson, Date now) { + IoTDeviceShadow entity = new IoTDeviceShadow(); + entity.setIotId(iotId); + entity.setMetadata(cacheJson); + entity.setUpdateDate(now); + entity.setLastTime(now); + entity.setActiveTime(now); + try { + IoTDeviceQuery query = new IoTDeviceQuery(); + query.setIotId(iotId); + IoTDevice dev = ioTDeviceMapper.getOneByIotId(query); + if (dev != null) { + entity.setProductKey(dev.getProductKey()); + entity.setDeviceId(dev.getDeviceId()); + entity.setExtDeviceId(dev.getExtDeviceId()); + entity.setInstance(dev.getApplication()); + if (dev.getOnlineTime() != null) { + entity.setOnlineTime(DateUtil.date(dev.getOnlineTime() * 1000)); + } + } + } catch (Exception ignore) { + } + + return entity; + } + + /** + * 将数据库中的影子 JSON 与缓存中的影子 JSON 进行深度合并,避免覆盖丢失。 规则: - 对象与对象递归合并; - 非对象类型以 incoming 覆盖 existing; - + * 数组整体覆盖; - 针对顶层的 timestamp/version,取二者较大值。 + */ + private String mergeShadowJson(String existingJson, String incomingJson) { + if (StrUtil.isBlank(incomingJson)) { + return StrUtil.blankToDefault(existingJson, "{}"); + } + if (StrUtil.isBlank(existingJson)) { + return incomingJson; + } + JSONObject existing; + JSONObject incoming; + try { + existing = JSONUtil.parseObj(existingJson); + } catch (Exception e) { + existing = new JSONObject(); + } + try { + incoming = JSONUtil.parseObj(incomingJson); + } catch (Exception e) { + // 如果新数据都无法解析,直接返回旧数据 + log.warn("[ShadowFlush]新数据都无法解析,直接返回旧数据 error: {}", incomingJson, e); + return existingJson; + } + + JSONObject merged = deepMergeObject(existing, incoming); + + // 顶层特殊字段处理 + try { + Long tsExisting = merged.getLong("timestamp"); + Long tsIncoming = incoming.getLong("timestamp"); + if (tsExisting != null || tsIncoming != null) { + long maxTs = + Math.max(tsExisting == null ? 0L : tsExisting, tsIncoming == null ? 0L : tsIncoming); + merged.set("timestamp", maxTs); + } + } catch (Exception ignore) { + log.warn("[ShadowFlush顶层特殊字段处理] merge shadow json error: {}", incomingJson, ignore); + } + try { + Long verExisting = merged.getLong("version"); + Long verIncoming = incoming.getLong("version"); + if (verExisting != null || verIncoming != null) { + long maxVer = + Math.max( + verExisting == null ? 0L : verExisting, verIncoming == null ? 0L : verIncoming); + merged.set("version", maxVer); + } + } catch (Exception ignore) { + log.warn("[ShadowFlush] merge shadow json error: {}", incomingJson); + } + + return JSONUtil.toJsonStr(merged); + } + + /** 对 JSONObject 进行递归深度合并 */ + private JSONObject deepMergeObject(JSONObject base, JSONObject incoming) { + if (base == null) { + return incoming == null ? new JSONObject() : incoming; + } + if (incoming == null) { + return base; + } + JSONObject result = JSONUtil.parseObj(base.toString()); + for (String key : incoming.keySet()) { + Object inVal = incoming.get(key); + Object baseVal = result.get(key); + + if (inVal instanceof JSONObject && baseVal instanceof JSONObject) { + result.set(key, deepMergeObject((JSONObject) baseVal, (JSONObject) inVal)); + } else { + // 数组或标量:直接覆盖 + result.set(key, inVal); + } + } + return result; + } + + /** 执行影子刷新任务 */ + private Integer executeFlushTask() { + long start = System.currentTimeMillis(); + int processed = 0; + String instanceId = instanceIdProvider.getInstanceId(); + + try { + long now = System.currentTimeMillis(); + + // 检查ZSet中总共有多少待刷盘的设备 + Long totalCount = + stringRedisTemplate + .opsForZSet() + .count(shadowFlushZsetKey, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + log.debug("[ShadowFlush] ZSet中待刷盘设备总数: {}, instanceId={}", totalCount, instanceId); + + // fetch due iotIds by score <= now + Set dueIds = + stringRedisTemplate + .opsForZSet() + .rangeByScore( + shadowFlushZsetKey, Double.NEGATIVE_INFINITY, (double) now, 0, batchSize); + + if (dueIds == null || dueIds.isEmpty()) { + log.debug("[ShadowFlush] 没有到期的设备需要刷盘: now={}, instanceId={}", now, instanceId); + return 0; + } + + log.info("[ShadowFlush] 发现{}个到期设备需要刷盘: {}, instanceId={}", dueIds.size(), dueIds, instanceId); + + // 获取缓存数据 + Map cacheData = getCacheData(dueIds); + if (cacheData.isEmpty()) { + log.warn("[ShadowFlush] 缓存数据为空,跳过刷盘: dueIds={}, instanceId={}", dueIds, instanceId); + return 0; + } + + // 使用原子操作从ZSet中移除已获取的设备ID,防止集群环境下的重复处理 + List validIotIds = new ArrayList<>(cacheData.keySet()); + Long removedCount = + stringRedisTemplate.opsForZSet().remove(shadowFlushZsetKey, validIotIds.toArray()); + + if (removedCount == null || removedCount == 0L) { + log.warn( + "[ShadowFlush] 从ZSet中移除设备失败,可能被其他实例处理: validIotIds={}, instanceId={}", + validIotIds, + instanceId); + return 0; + } + + log.info( + "[ShadowFlush] 从ZSet中移除{}个设备: {}, instanceId={}", removedCount, validIotIds, instanceId); + + // 批量处理设备影子 + processed = processBatchShadows(validIotIds, cacheData); + + log.info( + "[ShadowFlush] 刷盘任务完成: processed={}, cost={}ms, validIotIds={}, instanceId={}", + processed, + (System.currentTimeMillis() - start), + validIotIds, + instanceId); + + return processed; + + } catch (Exception e) { + log.error("[ShadowFlush] 刷盘任务异常: instanceId={}, error={}", instanceId, e.getMessage(), e); + return 0; + } + } + + /** 测试Redisson分布式锁是否正常工作 可以通过API调用此方法进行测试 */ + public String testDistributedLock() { + String testLockKey = "test:shadow:flush:lock"; + + return distributedLockUtil.tryLockAndExecute( + testLockKey, + 5, // 等待5秒 + 10, // 持有10秒 + TimeUnit.SECONDS, + () -> { + log.info("[ShadowFlush] 测试锁获取成功,当前时间: {}", DateUtil.now()); + try { + Thread.sleep(2000); // 模拟处理时间 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return "锁测试成功,处理完成"; + }); + } + + /** + * 检查强制刷盘状态 - 用于调试 + * + * @param iotId 设备ID + * @return 强制刷盘状态信息 + */ + public String checkForceFlushStatus(String iotId) { + try { + // 检查ZSet中的刷盘时间 + Double score = stringRedisTemplate.opsForZSet().score(shadowFlushZsetKey, iotId); + if (score == null) { + return "设备" + iotId + "不在刷盘队列中"; + } + + long flushTime = score.longValue(); + long now = System.currentTimeMillis(); + long timeUntilFlush = flushTime - now; + + // 检查缓存数据 + String cacheKey = buildShadowKey(iotId); + String cacheData = stringRedisTemplate.opsForValue().get(cacheKey); + boolean hasCacheData = StrUtil.isNotBlank(cacheData); + + return String.format( + "设备%s刷盘状态: 刷盘时间=%d, 当前时间=%d, 距离刷盘=%dms, 有缓存数据=%s", + iotId, flushTime, now, timeUntilFlush, hasCacheData); + + } catch (Exception e) { + return "检查强制刷盘状态失败: " + e.getMessage(); + } + } + + /** + * 检查集群状态 - 用于调试和监控 + * + * @return 集群状态信息 + */ + public String checkClusterStatus() { + try { + String instanceId = instanceIdProvider.getInstanceId(); + + // 检查分布式锁状态 + boolean isLocked = distributedLockUtil.isLocked(lockKey); + boolean isHeldByCurrentThread = distributedLockUtil.isHeldByCurrentThread(lockKey); + + // 检查ZSet状态 + Long totalCount = + stringRedisTemplate + .opsForZSet() + .count(shadowFlushZsetKey, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + Long dueCount = + stringRedisTemplate + .opsForZSet() + .count( + shadowFlushZsetKey, + Double.NEGATIVE_INFINITY, + (double) System.currentTimeMillis()); + + return String.format( + "集群状态: 当前实例=%s, 锁状态=%s, 当前实例持有锁=%s, 待刷盘总数=%d, 到期数量=%d", + instanceId, isLocked, isHeldByCurrentThread, totalCount, dueCount); + + } catch (Exception e) { + return "检查集群状态失败: " + e.getMessage(); + } + } + + /** + * 手动触发刷盘 - 用于测试和紧急情况 + * + * @return 处理结果 + */ + public String manualFlush() { + try { + String instanceId = instanceIdProvider.getInstanceId(); + log.info("[ShadowFlush] 手动触发刷盘: instanceId={}", instanceId); + + Integer result = + distributedLockUtil.tryLockAndExecute( + lockKey + ":manual", 10, 120, TimeUnit.SECONDS, this::executeFlushTask); + + if (result != null) { + return String.format("手动刷盘完成: processed=%d, instanceId=%s", result, instanceId); + } else { + return String.format("手动刷盘失败: 获取锁失败, instanceId=%s", instanceId); + } + + } catch (Exception e) { + return "手动刷盘异常: " + e.getMessage(); + } + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/TcpErrorNoticeTask.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/TcpErrorNoticeTask.java new file mode 100644 index 0000000000000000000000000000000000000000..71a5be15ef106d971a27baf33b73fb808691c4f9 --- /dev/null +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/task/TcpErrorNoticeTask.java @@ -0,0 +1,60 @@ +/* + * + * Copyright (c) 2025, IoT-Universal. All Rights Reserved. + * + * @Description: 本文件由 Aleo 开发并拥有版权,未经授权严禁擅自商用、复制或传播。 + * @Author: Aleo + * @Email: wo8335224@gmail.com + * @Wechat: outlookFil + * + * + */ + +package cn.universal.dm.device.service.task; + +import cn.universal.common.constant.IoTConstant; +import cn.universal.common.utils.DingTalkUtil; +import jakarta.annotation.Resource; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class TcpErrorNoticeTask { + + @Resource private StringRedisTemplate stringRedisTemplate; + private final String KEY = "tcpErrorNoticeTask"; + + @Scheduled(cron = "0 55 8 * * ? ") + public void doTcpErrorNotice() { + // 分布式锁,只执行1次,必须设置失效时间 + boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(KEY, "0", 5, TimeUnit.MINUTES); + if (!flag) { + return; + } + log.info("开始执行tcp连接错误次数通知"); + Map entries = + stringRedisTemplate.opsForHash().entries(IoTConstant.TCP_ERROR_MONITOR); + String msg = "tcp异常连接数统计:\n"; + Iterator> iterable = entries.entrySet().iterator(); + if (!iterable.hasNext()) { + log.info("结束tcp连接错误次数通知"); + return; + } + while (iterable.hasNext()) { + Map.Entry entry = iterable.next(); + String host = entry.getKey().toString(); + Integer num = Integer.parseInt(entry.getValue().toString()); + msg = msg + String.format("ip=[ %s ]进入黑名单后重复连接[ %d ]次", host, num) + "\n"; + } + DingTalkUtil.send(msg); + stringRedisTemplate.delete(IoTConstant.TCP_ERROR_MONITOR); + log.info("结束tcp连接错误次数通知"); + } +} diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java index eb6e6635346d7f024eed3dbdd805e73ccb20fd79..eb28ba8b71e8f8afedbacd69ccfe262452df11e1 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceAutoInsertIntercept.java @@ -53,8 +53,9 @@ public class IoTDeviceAutoInsertIntercept implements IoTDownWrapper { public R beforeDownAction(IoTProduct product, Object data, DownRequest downRequest) { // 产品为空或者没有开启自动注册直接略过 if (ObjectUtil.isEmpty(product) - || !JSONUtil.parseObj(product.getConfiguration()) - .getBool(IoTConstant.ALLOW_INSERT, false)) { + || (JSONUtil.isTypeJSON(product.getConfiguration()) + && !JSONUtil.parseObj(product.getConfiguration()) + .getBool(IoTConstant.ALLOW_INSERT, false))) { return null; } R r = null; diff --git a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java index b1b5df344313e5d82c4861bb3fa467d7da781734..81a8f02544c0042f62bdc086d14cf062a299a101 100644 --- a/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java +++ b/cn-universal-framework/cn-universal-dm/src/main/java/cn/universal/dm/device/service/wrapper/IoTDeviceDownFunctionWrapper.java @@ -59,12 +59,12 @@ public class IoTDeviceDownFunctionWrapper implements IoTDownWrapper { if (function.startsWith("set") && ObjectUtil.isNotEmpty(data)) { // TODO: 判断物模型属性是否有可读写的 - doShadow(product, ioTDevice, data); + doShadow(ioTDevice, data); } return null; } - private void doShadow(IoTProduct product, IoTDevice ioTDevice, JSONObject data) { + private void doShadow(IoTDevice ioTDevice, JSONObject data) { IoTDeviceShadow ioTDeviceShadow = IoTDeviceShadow.builder() .deviceId(ioTDevice.getDeviceId()) @@ -77,10 +77,11 @@ public class IoTDeviceDownFunctionWrapper implements IoTDownWrapper { stringRedisTemplate .opsForValue() .setIfAbsent("doCreateShadow:" + ioTDevice.getIotId(), "1", 10, TimeUnit.MINUTES); - if (ObjectUtil.isNull(ioTDeviceShadow) && Boolean.TRUE.equals(flag)) { + if (ObjectUtil.isNull(ioTDeviceShadow) && flag) { ioTDeviceShadow = IoTDeviceShadow.builder() .iotId(ioTDevice.getIotId()) + .productKey(ioTDevice.getProductKey()) .extDeviceId(ioTDevice.getExtDeviceId()) .deviceId(ioTDevice.getDeviceId()) .activeTime(new Date())