From ad1855cfb5f9d5750b749d8acb994d79a93d0b8f Mon Sep 17 00:00:00 2001 From: linjiaqin Date: Fri, 16 Jan 2026 15:45:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E6=8E=A5=E5=8F=A3=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xiaozhi/common/redis/RedisKeys.java | 10 ++ .../java/xiaozhi/common/utils/ToolUtil.java | 89 +++++++++++++++ .../device/controller/DeviceController.java | 91 +-------------- .../modules/device/service/DeviceService.java | 4 + .../service/impl/DeviceServiceImpl.java | 105 ++++++++++++++---- 5 files changed, 188 insertions(+), 111 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/common/utils/ToolUtil.java diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 281472e0..527b9330 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -166,4 +166,14 @@ public class RedisKeys { public static String getTmpRegisterMacKey(String deviceId) { return "tmp_register_mac:" + deviceId; } + + /** + * OTA绑定设备 + */ + public static String getOtaActivationCode(String activationCode) {return "ota:activation:code:" + activationCode;} + + /** + * OTA获取设备mac相关信息 + */ + public static String getOtaDeviceActivationInfo(String deviceId) {return "ota:activation:data:" + deviceId;} } diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/ToolUtil.java b/main/manager-api/src/main/java/xiaozhi/common/utils/ToolUtil.java new file mode 100644 index 00000000..40888965 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/ToolUtil.java @@ -0,0 +1,89 @@ +package xiaozhi.common.utils; + +import cn.hutool.core.util.ReUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 通用工具类 + */ +public class ToolUtil { + private static final Logger logger = LoggerFactory.getLogger(ToolUtil.class); + + /** + * 对象是否不为空(新增) + */ + public static boolean isNotEmpty(Object o) { + return !isEmpty(o); + } + + /** + * 对象是否为空 + */ + public static boolean isEmpty(Object o) { + if (o == null) { + return true; + } + if (o instanceof String) { + if (o.toString().trim().equals("")) { + return true; + } + } else if (o instanceof List) { + if (((List) o).size() == 0) { + return true; + } + } else if (o instanceof Map) { + if (((Map) o).size() == 0) { + return true; + } + } else if (o instanceof Set) { + if (((Set) o).size() == 0) { + return true; + } + } else if (o instanceof Object[]) { + if (((Object[]) o).length == 0) { + return true; + } + } else if (o instanceof int[]) { + if (((int[]) o).length == 0) { + return true; + } + } else if (o instanceof long[]) { + if (((long[]) o).length == 0) { + return true; + } + } + return false; + } + + /** + * 对象组中是否存在空对象 + */ + public static boolean isOneEmpty(Object... os) { + for (Object o : os) { + if (isEmpty(o)) { + return true; + } + } + return false; + } + + /** + * 对象组中是否全是空对象 + */ + public static boolean isAllEmpty(Object... os) { + for (Object o : os) { + if (!isEmpty(o)) { + return false; + } + } + return true; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java index 78a17b02..b562542b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/controller/DeviceController.java @@ -1,14 +1,9 @@ package xiaozhi.modules.device.controller; -import java.util.List; - +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.BeanUtils; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -16,10 +11,6 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; - -import com.fasterxml.jackson.databind.ObjectMapper; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -44,16 +35,11 @@ public class DeviceController { private final DeviceService deviceService; private final RedisUtils redisUtils; private final SysParamsService sysParamsService; - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService, - RestTemplate restTemplate, ObjectMapper objectMapper) { + public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) { this.deviceService = deviceService; this.redisUtils = redisUtils; this.sysParamsService = sysParamsService; - this.restTemplate = restTemplate; - this.objectMapper = objectMapper; } @PostMapping("/bind/{agentId}/{deviceCode}") @@ -99,83 +85,12 @@ public class DeviceController { @RequiresPermissions("sys:role:normal") public Result forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) { try { - // 从系统参数中获取MQTT网关地址 - String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true); - if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) { - return new Result<>(); - } - - // 获取当前用户的设备列表 - UserDetail user = SecurityUser.getUser(); - List devices = deviceService.getUserDevices(user.getId(), agentId); - - // 构建deviceIds数组 - java.util.List deviceIds = new java.util.ArrayList<>(); - for (DeviceEntity device : devices) { - String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown"; - String groupId = device.getBoard() != null ? device.getBoard() : "GID_default"; - - // 替换冒号为下划线 - groupId = groupId.replace(":", "_"); - macAddress = macAddress.replace(":", "_"); - - // 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress - String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress; - deviceIds.add(mqttClientId); - } - - // 构建完整的URL - String url = "http://" + mqttGatewayUrl + "/api/devices/status"; - - // 设置请求头 - HttpHeaders headers = new HttpHeaders(); - headers.set("Content-Type", "application/json"); - - // 生成Bearer令牌 - String token = generateBearerToken(); - if (token == null) { - return new Result().error("令牌生成失败"); - } - headers.set("Authorization", "Bearer " + token); - - // 构建请求体JSON - String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}"; - HttpEntity requestEntity = new HttpEntity<>(jsonBody, headers); - - // 发送POST请求 - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); - - // 返回响应 - return new Result().ok(response.getBody()); + return new Result().ok(deviceService.getDeviceOnlineData(agentId)); } catch (Exception e) { return new Result().error("转发请求失败: " + e.getMessage()); } } - private String generateBearerToken() { - try { - // 获取当前日期,格式为yyyy-MM-dd - String dateStr = java.time.LocalDate.now() - .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")); - - // 获取MQTT签名密钥 - String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false); - if (StringUtils.isBlank(signatureKey)) { - return null; - } - - // 将日期字符串与MQTT_SIGNATURE_KEY连接 - String tokenContent = dateStr + signatureKey; - - // 对连接后的字符串进行SHA256哈希计算 - String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent); - - return token; - } catch (Exception e) { - return null; - } - } - @PostMapping("/unbind") @Operation(summary = "解绑设备") @RequiresPermissions("sys:role:normal") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java index f4debba9..c37f69f5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java @@ -13,6 +13,10 @@ import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.vo.UserShowDeviceListVO; public interface DeviceService extends BaseService { + /** + * 获取设备在线数据 + */ + String getDeviceOnlineData(String agentId); /** * 检查设备是否激活 diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index ea0d20eb..8c9d6232 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -4,17 +4,21 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.Base64; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.ContentType; +import cn.hutool.http.Header; +import cn.hutool.http.HttpRequest; +import cn.hutool.json.JSONUtil; import org.apache.commons.lang3.StringUtils; import org.springframework.aop.framework.AopContext; import org.springframework.scheduling.annotation.Async; @@ -40,6 +44,8 @@ import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.DateUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.common.utils.ToolUtil; import xiaozhi.modules.device.dao.DeviceDao; import xiaozhi.modules.device.dto.DeviceManualAddDTO; import xiaozhi.modules.device.dto.DevicePageUserDTO; @@ -88,16 +94,16 @@ public class DeviceServiceImpl extends BaseServiceImpl if (StringUtils.isBlank(activationCode)) { throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY); } - String deviceKey = "ota:activation:code:" + activationCode; + String deviceKey = RedisKeys.getOtaActivationCode(activationCode); Object cacheDeviceId = redisUtils.get(deviceKey); - if (cacheDeviceId == null) { + if (ToolUtil.isEmpty(cacheDeviceId)) { throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } String deviceId = (String) cacheDeviceId; String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); - String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId); + String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId); Map cacheMap = (Map) redisUtils.get(cacheDeviceKey); - if (cacheMap == null) { + if (ToolUtil.isEmpty(cacheMap)) { throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR); } String cachedCode = (String) cacheMap.get("activation_code"); @@ -133,19 +139,56 @@ public class DeviceServiceImpl extends BaseServiceImpl deviceEntity.setLastConnectedAt(currentTime); deviceDao.insert(deviceEntity); - // 清理redis缓存 - redisUtils.delete(cacheDeviceKey); - redisUtils.delete(deviceKey); - - // 添加:清除智能体设备数量缓存 - redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId)); - + // 清理redis缓存、清除智能体设备数量缓存 + redisUtils.delete(List.of(cacheDeviceKey, deviceKey, RedisKeys.getAgentDeviceCountById(agentId))); return true; } + /** + * 获取设备在线数据 + */ @Override - public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId, - DeviceReportReqDTO deviceReport) { + public String getDeviceOnlineData(String agentId) { + // 从系统参数中获取MQTT网关地址 + String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true); + if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) { + return ""; + } + // 构建完整的URL + String url = StrUtil.format("http://{}/api/devices/status", mqttGatewayUrl); + + // 获取当前用户的设备列表 + UserDetail user = SecurityUser.getUser(); + List devices = getUserDevices(user.getId(), agentId); + + // 构建deviceIds数组 + Set deviceIds = devices.stream().map(o -> { + String macAddress = Optional.ofNullable(o.getMacAddress()).orElse("unknown").replace(":", "_"); + String groupId = Optional.ofNullable(o.getBoard()).orElse("GID_default").replace(":", "_"); + return StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress); + }).collect(Collectors.toSet()); + + // 构建请求入参 + Map> params = MapUtil + .builder(new HashMap>()) + .put("clientIds", deviceIds).build(); + + if (ToolUtil.isNotEmpty(deviceIds)) { + // 发送请求 + String resultMessage = HttpRequest.post(url) + .header(Header.CONTENT_TYPE, ContentType.JSON.getValue()) + .header(Header.AUTHORIZATION, "Bearer " + generateBearerToken()) + .body(JSONUtil.toJsonStr(params)) + .timeout(10000) //超时,毫秒 + .execute().body(); + return resultMessage; + } + // 返回响应 + return ""; + } + + @Override + public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId, DeviceReportReqDTO deviceReport) { DeviceReportRespDTO response = new DeviceReportRespDTO(); response.setServer_time(buildServerTime()); @@ -356,8 +399,7 @@ public class DeviceServiceImpl extends BaseServiceImpl private String getDeviceCacheKey(String deviceId) { String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); - String dataKey = String.format("ota:activation:data:%s", safeDeviceId); - return dataKey; + return RedisKeys.getOtaDeviceActivationInfo(safeDeviceId); } public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) { @@ -396,7 +438,7 @@ public class DeviceServiceImpl extends BaseServiceImpl redisUtils.set(dataKey, dataMap); // 写入反查激活码 key - String codeKey = "ota:activation:code:" + newCode; + String codeKey = RedisKeys.getOtaActivationCode(newCode); redisUtils.set(codeKey, deviceId); } return code; @@ -606,4 +648,21 @@ public class DeviceServiceImpl extends BaseServiceImpl return mqtt; } + + + /** + * 生成BearerToken + */ + private String generateBearerToken() { + try { + String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN); + String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET,false); + if (ToolUtil.isEmpty(signatureKey)) { + return null; + } + return DigestUtil.sha256Hex(dateStr + signatureKey); + } catch (Exception e) { + return null; + } + } }