From 6892571a330f882dfc875be4f664d11a255e1448 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 21 Jan 2026 09:12:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(iot):=E3=80=90=E8=AE=BE=E5=A4=87=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=EF=BC=9A70%=E3=80=91=E4=BC=98=E5=8C=96=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=9D=90=E6=A0=87=E7=9A=84=E8=A7=A3=E6=9E=90=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=9F=BA=E4=BA=8E=20hashed?= =?UTF-8?q?-juggling-tome.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/collection/MapUtils.java | 44 +++++++++++++ .../device/vo/device/IotDeviceSaveReqVO.java | 10 ++- .../dal/dataobject/device/IotDeviceDO.java | 10 --- .../IotDevicePropertyServiceImpl.java | 66 +++++++++++-------- 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java index a59b53fd4b..7ba36710ec 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/MapUtils.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -65,4 +66,47 @@ public class MapUtils { return map; } + /** + * 从 Map 中获取 BigDecimal 值 + * + * @param map Map 数据源 + * @param key 键名 + * @return BigDecimal 值,解析失败或值为 null 时返回 null + */ + public static BigDecimal getBigDecimal(Map map, String key) { + return getBigDecimal(map, key, null); + } + + /** + * 从 Map 中获取 BigDecimal 值 + * + * @param map Map 数据源 + * @param key 键名 + * @param defaultValue 默认值 + * @return BigDecimal 值,解析失败或值为 null 时返回默认值 + */ + public static BigDecimal getBigDecimal(Map map, String key, BigDecimal defaultValue) { + if (map == null) { + return defaultValue; + } + Object value = map.get(key); + if (value == null) { + return defaultValue; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + if (value instanceof Number) { + return BigDecimal.valueOf(((Number) value).doubleValue()); + } + if (value instanceof String) { + try { + return new BigDecimal((String) value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java index 28f99f11d5..637ebfefbd 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import lombok.Data; import java.math.BigDecimal; @@ -37,10 +39,14 @@ public class IotDeviceSaveReqVO { @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") private String config; - @Schema(description = "设备位置的纬度", example = "16380") + @Schema(description = "设备位置的纬度", example = "39.915") + @DecimalMin(value = "-90", message = "纬度范围为 -90 到 90") + @DecimalMax(value = "90", message = "纬度范围为 -90 到 90") private BigDecimal latitude; - @Schema(description = "设备位置的经度", example = "16380") + @Schema(description = "设备位置的经度", example = "116.404") + @DecimalMin(value = "-180", message = "经度范围为 -180 到 180") + @DecimalMax(value = "180", message = "经度范围为 -180 到 180") private BigDecimal longitude; } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java index e6ae0cfd25..efb232b963 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java @@ -137,16 +137,6 @@ public class IotDeviceDO extends TenantBaseDO { * 设备位置的经度 */ private BigDecimal longitude; - /** - * 地区编码 - *

- * 关联 Area 的 id - */ - private Integer areaId; - /** - * 设备详细地址 - */ - private String address; /** * 设备配置 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java index ecdbfa230d..afc90429b0 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java @@ -36,6 +36,7 @@ import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal; /** * IoT 设备【属性】数据 Service 实现类 @@ -131,50 +132,59 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { } @Override + @SuppressWarnings("PatternVariableCanBeUsed") public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { if (!(message.getParams() instanceof Map)) { log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); return; } + Map params = (Map) message.getParams(); + if (CollUtil.isEmpty(params)) { + log.error("[saveDeviceProperty][消息内容({}) 的 data 为空]", message); + return; + } // 1. 根据物模型,拼接合法的属性 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? List thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); Map properties = new HashMap<>(); - ((Map) message.getParams()).forEach((key, value) -> { + params.forEach((key, value) -> { IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key)); if (thingModel == null || thingModel.getProperty() == null) { log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); return; } - if (ObjectUtils.equalsAny(thingModel.getProperty().getDataType(), + String dataType = thingModel.getProperty().getDataType(); + if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 properties.put((String) key, JsonUtils.toJsonString(value)); - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(thingModel.getProperty().getDataType())) { - properties.put((String) key, Convert.toDouble(value)); - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(thingModel.getProperty().getDataType())) { + } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { + properties.put((String) key, Convert.toInt(value)); + } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { properties.put((String) key, Convert.toFloat(value)); - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(thingModel.getProperty().getDataType())) { + } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { + properties.put((String) key, Convert.toDouble(value)); + } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { properties.put((String) key, Convert.toByte(value)); - } else { + } else { properties.put((String) key, value); } }); if (CollUtil.isEmpty(properties)) { log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); - return; + } else { + // 2.1 保存设备属性【数据】 + devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); + + // 2.2 保存设备属性【日志】 + Map properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> + IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); + deviceDataRedisDAO.putAll(device.getId(), properties2); } - // 2.1 保存设备属性【数据】 - devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); - - // 2.2 保存设备属性【日志】 - Map properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> - IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); - deviceDataRedisDAO.putAll(device.getId(), properties2); - // 2.3 提取 GeoLocation 并更新设备定位 + // 为什么 properties 为空,也要执行定位更新?因为可能上报的属性里,没有合法属性,但是包含 GeoLocation 定位属性 extractAndUpdateDeviceLocation(device, (Map) message.getParams()); } @@ -231,14 +241,13 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { */ private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map params) { // 1. 解析 GeoLocation 经纬度坐标 - Double[] location = parseGeoLocation(params); + BigDecimal[] location = parseGeoLocation(params); if (location == null) { return; } // 2. 更新设备定位 - deviceService.updateDeviceLocation(device, - BigDecimal.valueOf(location[0]), BigDecimal.valueOf(location[1])); + deviceService.updateDeviceLocation(device, location[0], location[1]); log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]", device.getId(), location[0], location[1]); } @@ -250,8 +259,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { * @return [经度, 纬度],解析失败返回 null */ @SuppressWarnings("unchecked") - // TODO @AI:返回 BigDecimal 数组; - private Double[] parseGeoLocation(Map params) { + private BigDecimal[] parseGeoLocation(Map params) { if (params == null) { return null; } @@ -276,18 +284,24 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { } // 3. 提取经纬度(支持阿里云命名规范:首字母大写) - Double longitude = MapUtil.getDouble(geoLocation, "Longitude"); + BigDecimal longitude = getBigDecimal(geoLocation, "Longitude"); if (longitude == null) { - longitude = MapUtil.getDouble(geoLocation, "longitude"); + longitude = getBigDecimal(geoLocation, "longitude"); } - Double latitude = MapUtil.getDouble(geoLocation, "Latitude"); + BigDecimal latitude = getBigDecimal(geoLocation, "Latitude"); if (latitude == null) { - latitude = MapUtil.getDouble(geoLocation, "latitude"); + latitude = getBigDecimal(geoLocation, "latitude"); } if (longitude == null || latitude == null) { return null; } - return new Double[]{longitude, latitude}; + // 校验经纬度范围:经度 -180 到 180,纬度 -90 到 90 + if (longitude.compareTo(BigDecimal.valueOf(-180)) < 0 || longitude.compareTo(BigDecimal.valueOf(180)) > 0 + || latitude.compareTo(BigDecimal.valueOf(-90)) < 0 || latitude.compareTo(BigDecimal.valueOf(90)) > 0) { + log.warn("[parseGeoLocation][经纬度超出有效范围: lng={}, lat={}]", longitude, latitude); + return null; + } + return new BigDecimal[]{longitude, latitude}; } } \ No newline at end of file