!2 fix:合并2.0商业版代码:dm模块

Merge pull request !2 from 奈科斯(NexIoT)/dev
This commit is contained in:
奈科斯(NexIoT)
2025-09-23 03:03:32 +00:00
committed by Gitee
46 changed files with 2491 additions and 946 deletions
+6 -1
View File
@@ -114,7 +114,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-notice</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -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文档
*
* <p>基础路径: /admin/network
*
* <p>1. 查询网络组件列表 GET /admin/network/list 参数: - page: 页码 (可选,默认1) - size: 每页大小 (可选,默认10) - type:
* 网络类型 (可选,单个类型,如 TCP_CLIENT) - types: 网络类型列表 (可选,多个类型,如 ["MQTT_CLIENT", "MQTT_SERVER"]) - name:
* 网络组件名称 (可选,模糊查询) - productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选)
*
* <p>2. 查询网络组件列表(支持多个类型) GET /admin/network/list/multi-type 参数: - page: 页码 (可选,默认1) - size: 每页大小
* (可选,默认10) - type: 网络类型 (可选,支持逗号分隔多个类型,如 "MQTT_CLIENT,MQTT_SERVER") - name: 网络组件名称 (可选,模糊查询) -
* productKey: 产品Key (可选) - state: 状态 (可选,true/false) - unionId: 唯一标识 (可选)
*
* <p>3. 根据ID查询网络组件 GET /admin/network/{id} 参数: - id: 网络组件ID (路径参数)
*
* <p>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" }
*
* <p>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}" }
*
* <p>6. 删除网络组件 DELETE /admin/network/{id} 参数: - id: 网络组件ID (路径参数)
*
* <p>7. 批量删除网络组件 DELETE /admin/network/batch/{ids} 参数: - ids: 网络组件ID数组 (路径参数,逗号分隔)
*
* <p>8. 启动网络组件 POST /admin/network/start/{id} 参数: - id: 网络组件ID (路径参数)
*
* <p>9. 停止网络组件 POST /admin/network/stop/{id} 参数: - id: 网络组件ID (路径参数)
*
* <p>10. 重启网络组件 POST /admin/network/restart/{id} 参数: - id: 网络组件ID (路径参数)
*
* <p>11. 获取网络类型列表 GET /admin/network/types
*
* <p>12. 验证网络组件配置 POST /admin/network/validate 请求体: { "type": "TCP_SERVER", "unionId":
* "unique_id", "name": "网络组件名称", "configuration":
* "{\"host\":\"0.0.0.0\",\"port\":6372,\"ssl\":false}" }
*
* <p>响应格式: { "code": 200, "msg": "操作成功", "data": { // 具体数据 } }
*
* <p>网络类型说明: - TCP_CLIENT: TCP客户端 - TCP_SERVER: TCP服务端 - MQTT_CLIENT: MQTT客户端 - MQTT_SERVER:
* MQTT服务端
*
* <p>配置示例:
*
* <p>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 }
*
* <p>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 }
*/
}
@@ -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<NetworkVO> selectNetworkList(NetworkBO bo);
NetworkVO getDetail(Long id);
List<IoTDevice> queryMileSightList(NetworkBO bo);
R insertDevInstance(IoTDeviceBO devInstancebo);
R deleteDevInstanceByIds(String[] ids);
void reloadTcpClient(String applicationId);
/**
* 查询网络组件列表
*
@@ -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 ProtocolClusterService tcpClusterService;
@Autowired(required = false)
private ProtocolServerManager tcpServerManager;
@Qualifier("udpClusterService")
@Autowired(required = false)
private ProtocolClusterService udpClusterService;
@Autowired(required = false)
private ThirdMQTTServerManager mqttServerManager;
@Override
public boolean start(String productKey) {
return true;
}
@Override
public boolean stop(String productKey) {
return true;
}
@Override
public Object get(String productKey) {
return null;
}
@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<NetworkVO> 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<NetworkBO> 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<IoTProductBO> 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<String> 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<Network> 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;
}
}
}
@@ -29,7 +29,7 @@ public class NetworkTypeUtil {
/** 支持的网络类型列表 */
public static final List<String> SUPPORTED_TYPES =
Arrays.asList("TCP_CLIENT", "TCP_SERVER", "MQTT_CLIENT", "MQTT_SERVER");
Arrays.asList("TCP_CLIENT", "TCP_SERVER", "MQTT_CLIENT", "MQTT_SERVER", "UDP");
/**
* 解析网络类型字符串,支持逗号分隔的多个类型
@@ -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();
}
@@ -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<GatewayDeviceVo> selectGwIotDevices(IoTGwDeviceBO bo);
IoTDevice selectDeviceByDeviceId(String deviceId);
IoTDevice selectIoTDevice(String productKey, String deviceId);
List<IoTDevice> selectDevInstanceListWithTags(IoTDeviceBO ioTDeviceBO, IoTUser iotUser);
List<IoTProduct> selectProductListInBatchFunction(Long applicationId, IoTUser iotUser);
/** 新:按第三方平台过滤设备列表(不影响旧接口) */
List<IoTDevice> selectDevInstanceListByPlatform(
IoTDevice ioTDevice, IoTUser iotUser, String thirdPlatform);
}
@@ -178,7 +178,9 @@ public interface IIoTProductService {
Map<String, Integer> countDevNumberByProductKey(String unionId);
AjaxResult<IoTProduct> selectDevProductByKey(String key);
AjaxResult<IoTProduct> selectIoTProductByKey(String key);
AjaxResult<List<IoTProduct>> selectGatewaySubProductsByKey(String gwProductKey);
int updateDevProductOtherConfig(String otherConfig);
@@ -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<IoTDeviceGroupVO> 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<Void> 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<Void> 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> ioTDeviceGroupVO, Map<Long, List<IoTDeviceGroupVO>> groups) {
if (ioTDeviceGroupVO == null) {
return;
@@ -315,7 +332,7 @@ public class IoTDeviceGroupServiceImpl extends BaseServiceImpl implements IIoTDe
if (ioTDeviceGroupVO.get(i).getHasChild() == 1) {
List<IoTDeviceGroupVO> devGroups = groups.get(ioTDeviceGroupVO.get(i).getId());
ioTDeviceGroupVO.get(i).setChildren(devGroups);
buildGroupTree(devGroups, groups);
groupTree(devGroups, groups);
}
}
return;
@@ -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<IoTDevice> 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("设备不存在");
}
@@ -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<IoTProductVO> selectDevProductV4List(IoTProductQuery ioTProductQuery) {
Page<IoTProductVO> page =
PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize());
// 设置分页参数
PageHelper.startPage(ioTProductQuery.getPageNum(), ioTProductQuery.getPageSize());
// 优化:一次SQL查询获取产品列表和设备数量,避免N+1查询
List<IoTProductVO> devProductVOS = ioTProductMapper.selectDevProductV3List(ioTProductQuery);
List<IoTProductVO> results =
ioTProductMapper.countDevNumberByProductKey(ioTProductQuery.getCreatorId());
if (CollUtil.isNotEmpty(results)) {
final Map<String, Integer> 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<IoTProductVO> selectDevProductV4ListNoPage(IoTProductQuery ioTProductQuery) {
// 创建不分页的查询条件
IoTProductQuery noPageQuery = new IoTProductQuery();
BeanUtil.copyProperties(ioTProductQuery, noPageQuery);
noPageQuery.setPageNum(1);
noPageQuery.setPageSize(Integer.MAX_VALUE); // 获取所有数据
// 优化:一次SQL查询获取产品列表和设备数量,避免N+1查询
List<IoTProductVO> 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<IoTProduct> 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<IoTProductImportBO> 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<String, Integer> countDevNumberByProductKey(String unionId) {
List<IoTProductVO> 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<IoTProduct> selectDevProductByKey(String key) {
public AjaxResult<IoTProduct> selectIoTProductByKey(String key) {
IoTProduct ioTProduct = new IoTProduct();
ioTProduct.setProductKey(key);
return AjaxResult.success(ioTProductMapper.selectOne(ioTProduct));
}
@Override
public AjaxResult<List<IoTProduct>> 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
@@ -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<IoTDevice> 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() + ")");
}
}
@@ -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<IoTProduct> getProByKey(@PathVariable("key") String key) {
return devProductService.selectDevProductByKey(key);
return devProductService.selectIoTProductByKey(key);
}
/**
* 查询设备产品列表
*/
/** 获取网关绑定的子产品列表 */
@GetMapping("/gateway/subProducts/{key}")
public AjaxResult<List<IoTProduct>> 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<IoTProductVO> list = devProductService.selectDevProductAllList();
return getDataTable(list);
}
/**
* 物模型新增内容
*/
/** 物模型新增内容 */
@PostMapping("/metadata/add")
@Log(title = "物模型新增内容", businessType = BusinessType.INSERT)
public AjaxResult<Void> metadataAdd(@RequestBody IoTProductBO ioTProductBO) {
return toAjax(devProductService.insertMetadata(ioTProductBO));
}
/**
* 物模型修改内容
*/
/** 物模型修改内容 */
@PostMapping("/metadata/update")
@Log(title = "物模型修改内容", businessType = BusinessType.UPDATE)
public AjaxResult<Void> metadataUpdate(@RequestBody IoTProductBO ioTProductBO) {
return toAjax(devProductService.updateMetadata(ioTProductBO));
}
/**
* 物模型删除内容
*/
/** 物模型删除内容 */
@PostMapping("/metadata/delete")
@Log(title = "物模型删除内容", businessType = BusinessType.DELETE)
public AjaxResult<Void> metadataDelete(@RequestBody IoTProductBO ioTProductBO) {
return toAjax(devProductService.deleteMetadata(ioTProductBO));
}
/**
* 物模型查询内容
*/
/** 物模型查询内容 */
@PostMapping("/metadata/get")
public AjaxResult<IoTProductBO> metadataGet(@RequestBody IoTProductBO ioTProductBO) {
return AjaxResult.success(devProductService.getMetadata(ioTProductBO));
}
/** 导出设备产品列表 */
// @PostMapping("/export")
// public void export(HttpServletResponse response, IoTProduct ioTProduct)
// {
// List<IoTProduct> list = devProductService.selectDevProductList(ioTProduct);
// ExcelUtil<IoTProduct> util = new ExcelUtil<IoTProduct>(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<List<IoTProductVO>> 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<List<IoTProductVO>> getGatewaySubDeviceList(
@PathVariable("productKey") String productKey) {
List<IoTProductVO> 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<String, Object> 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<String, Object> 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<IoTDevice> 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<String, Object> 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<IoTDeviceProperties> list = devProductService.selectDevProperties(id);
return AjaxResult.success(list);
}
/**
* 查询设备产品物模型属性列表(功能下发属性)
*/
/** 查询设备产品物模型属性列表(功能下发属性) */
@GetMapping("/functionProperties/{id}")
public AjaxResult getFunctionPropertiesList(@PathVariable("id") String id) {
List<IoTDeviceProperties> list = devProductService.selectDevProperties(id);
@@ -535,7 +509,8 @@ public class IoTProductController extends BaseController {
List<IoTDeviceProperties> dataRw =
list.stream().filter((item) -> "rw".equals(item.getMode())).collect(Collectors.toList());
// 获取设备公共字段
List<SysDictData> data = dictTypeService.selectDictDataByType("device_common_property");
List<SysDictData> 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<IoTDeviceEvents> list = devProductService.selectDevEvents(id);
return AjaxResult.success(list);
}
/**
* 查询设备产品物模型方法列表
*/
/** 查询设备产品物模型方法列表 */
@GetMapping("/functions/{id}")
public AjaxResult getFunctionsList(@PathVariable("id") String id) {
List<IoTDeviceFunction> 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<String, Integer> 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<Void> 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<Map<String, String>> 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<String, String> 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<IoTProductImportBO> 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());
}
}
}
@@ -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<Void> 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<Void> 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<String, String> 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);
}
@@ -67,4 +67,15 @@ public interface IIoTUserApplicationService {
int deleteIotUserApplicationById(String appUniqueId);
List<IoTUserApplicationVO> 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);
}
@@ -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<IoTUserApplication> 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;
}
}
@@ -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<String> 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<String> 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<String> 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);
}
}
}
@@ -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());
}
}
}
@@ -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;
}
@@ -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<T> extends AbstratIoTService implements IDown, ICodec {
public abstract class AbstractDownService<T> extends AbstratIoTService implements IDown {
@Autowired protected ICodecService codecService;
@Autowired protected ICodecService iCodecService;
/**
* 消息转换和编解码
@@ -37,9 +37,17 @@ public abstract class AbstractDownService<T> 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);
}
}
@@ -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<T extends UPRequest> extends AbstratIoTService
implements IUP, ICodec {
implements IUP {
@Autowired protected ICodecService codecService;
@@ -112,10 +111,4 @@ public abstract class AbstractUPService<T extends UPRequest> extends AbstratIoTS
/** 当前iot组件名称 */
protected abstract String currentComponent();
@Override
public UPRequest preDecode(String productKey, String message) {
// 使用新的统一编解码服务
return codecService.preDecode(productKey, message);
}
}
@@ -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
*
* <p>处理以下情况:
* <ul>
* <li>配置为null</li>
* <li>配置为空字符串</li>
* <li>配置不是有效的JSON格式</li>
* </ul>
*
* @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;
}
/**
* 查询产品是否配置了离线周期阈值
*
* <p>用于判断产品是否需要监控设备离线状态
*
* @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;
}
/**
* 查询产品的协议解码定义
*
* <p>获取产品配置的协议解码规则,用于消息的编解码处理
*
* @param productKey 产品唯一标识
* @return 协议支持定义,包含解码规则和配置
*/
protected ProtocolSupportDefinition selectProtocolDef(String productKey) {
return iotProductDeviceService.selectProtocolDef(productKey);
}
/**
* 查询产品的协议解码定义(不带超长脚本)
*
* <p>获取产品配置的协议解码规则,用于消息的编解码处理
*
* @param productKey 产品唯一标识
* @return 协议支持定义,包含解码规则和配置
*/
protected ProtocolSupportDefinition selectProtocolDefNoScript(String productKey) {
return iotProductDeviceService.selectProtocolDefNoScript(productKey);
}
/**
* 保存设备上行日志和影子数据
*
@@ -231,73 +230,8 @@ public abstract class AbstratIoTService {
}
}
/**
* 获取编解码插件提供者
*
* <p>根据支持类型获取对应的协议编解码实现,支持: - 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加密
*
* <p>使用设备唯一标识作为密钥对原始报文进行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解密
*
* <p>使用设备唯一标识作为密钥对加密报文进行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));
}
/**
* 根据设备序列号查询设备信息
*
* <p>通过设备序列号获取设备的完整信息,包括设备状态、配置等
*
* @param deviceId 设备序列号
* @return 设备实例信息
*/
public IoTDeviceDTO getIotDeviceByDeviceIdLimitOne(String deviceId) {
IoTDeviceDTO devInstance =
iotDeviceService.lifeCycleDevInstance(IoTDeviceQuery.builder().deviceId(deviceId).build());
return devInstance;
}
protected <R> List<R> decode(
String productKey, String payload, Object context, Class<R> 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())) {
@@ -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;
/**
* 编解码服务使用示例
*
* <p>展示如何使用统一的编解码服务进行各种编解码操作
*
* @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<UPRequest> decodeDeviceData(String productKey, String payload) {
log.info("开始解码设备数据: productKey={}, payload={}", productKey, payload);
// 使用统一的解码服务
List<UPRequest> 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 <T> 泛型类型
* @return 解码后的对象列表
*/
public <T> List<T> decodeWithType(String productKey, String payload, Class<T> elementType) {
log.info("开始泛型解码: productKey={}, payload={}, elementType={}", productKey, payload, elementType);
// 使用统一的泛型解码服务
List<T> 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;
}
}
@@ -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");
}
}
@@ -162,7 +162,7 @@ public abstract class IoTUPPushAdapter<T extends BaseUPRequest> {
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;
@@ -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<BaseUPRequest>
.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;
}
@@ -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)
@@ -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);
}
}
@@ -43,6 +43,7 @@ public class IoTCacheRemoveService {
"iot_dev_shadow_bo",
"iot_dev_action",
"iot_dev_product_list",
"selectDevProductV4List",
"selectDevCount"
},
allEntries = true)
@@ -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<IoTDeviceVO> apiDeviceList(IoTAPIQuery iotAPIQuery) {
Page<IoTDeviceVO> 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<String, Object> 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 {
*
* <p>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<String, Object> 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 {
*
* <p>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);
}
}
}
@@ -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<IoTDevicePropertiesBO> getDevState(String iotId) {
List<IoTDevicePropertiesBO> 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<String, Object> 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<IoTDevicePropertiesBO> getDevStateWithNullResult(String iotId) {
List<IoTDevicePropertiesBO> 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<String, Object> 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;
}
}
@@ -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());
}
}
@@ -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<IoTDeviceEvents> 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"));
@@ -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 {
@@ -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) {
@@ -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;
/**
* 协议集群服务接口
*
* <p>用于管理协议服务器的集群操作,包括启动、停止、重启等集群化操作
*
* @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;
}
}
@@ -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;
/**
* 协议服务器管理器接口
*
* <p>用于管理协议服务器的生命周期,包括启动、停止、重启等操作
*
* @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);
}
@@ -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服务器启动器接口
*
* <p>用于管理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);
}
@@ -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(
@@ -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);
}
@@ -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(
@@ -77,7 +77,7 @@ public class PushRetryProcessor implements UPProcessor<BaseUPRequest> {
/** 获取线程池状态信息 */
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<BaseUPRequest> {
// 添加到重试队列
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<BaseUPRequest> {
// 增加重试次数
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<BaseUPRequest> {
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<BaseUPRequest> {
// 异步处理结果,不阻塞当前线程
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);
}
}
@@ -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;
/**
* 设备影子定时缓存
*
* <p>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<String, String> getCacheData(Set<String> iotIds) {
Map<String, String> 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<String> iotIds, Map<String, String> cacheData) {
if (CollUtil.isEmpty(iotIds)) {
return 0;
}
try {
// 批量查询已存在的记录
List<IoTDeviceShadow> existingShadows = ioTDeviceShadowMapper.selectByIotIds(iotIds);
Map<String, IoTDeviceShadow> existingMap =
existingShadows.stream()
.collect(Collectors.toMap(IoTDeviceShadow::getIotId, Function.identity()));
// 分离需要插入和更新的记录
List<IoTDeviceShadow> toInsert = new ArrayList<>();
List<IoTDeviceShadow> 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<String> 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<String, String> cacheData = getCacheData(dueIds);
if (cacheData.isEmpty()) {
log.warn("[ShadowFlush] 缓存数据为空,跳过刷盘: dueIds={}, instanceId={}", dueIds, instanceId);
return 0;
}
// 使用原子操作从ZSet中移除已获取的设备ID,防止集群环境下的重复处理
List<String> 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();
}
}
}
@@ -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<Object, Object> entries =
stringRedisTemplate.opsForHash().entries(IoTConstant.TCP_ERROR_MONITOR);
String msg = "tcp异常连接数统计:\n";
Iterator<Entry<Object, Object>> iterable = entries.entrySet().iterator();
if (!iterable.hasNext()) {
log.info("结束tcp连接错误次数通知");
return;
}
while (iterable.hasNext()) {
Map.Entry<Object, Object> 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连接错误次数通知");
}
}
@@ -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;
@@ -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())