Commit 6892571a330f882dfc875be4f664d11a255e1448
1 parent
3a832d9f
feat(iot):【设备定位:70%】优化设备坐标的解析逻辑代码,基于 hashed-juggling-tome.md
Showing
4 changed files
with
92 additions
and
38 deletions
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; | @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; | ||
| 7 | import com.google.common.collect.Maps; | 7 | import com.google.common.collect.Maps; |
| 8 | import com.google.common.collect.Multimap; | 8 | import com.google.common.collect.Multimap; |
| 9 | 9 | ||
| 10 | +import java.math.BigDecimal; | ||
| 10 | import java.util.ArrayList; | 11 | import java.util.ArrayList; |
| 11 | import java.util.Collection; | 12 | import java.util.Collection; |
| 12 | import java.util.List; | 13 | import java.util.List; |
| @@ -65,4 +66,47 @@ public class MapUtils { | @@ -65,4 +66,47 @@ public class MapUtils { | ||
| 65 | return map; | 66 | return map; |
| 66 | } | 67 | } |
| 67 | 68 | ||
| 69 | + /** | ||
| 70 | + * 从 Map 中获取 BigDecimal 值 | ||
| 71 | + * | ||
| 72 | + * @param map Map 数据源 | ||
| 73 | + * @param key 键名 | ||
| 74 | + * @return BigDecimal 值,解析失败或值为 null 时返回 null | ||
| 75 | + */ | ||
| 76 | + public static BigDecimal getBigDecimal(Map<String, ?> map, String key) { | ||
| 77 | + return getBigDecimal(map, key, null); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 从 Map 中获取 BigDecimal 值 | ||
| 82 | + * | ||
| 83 | + * @param map Map 数据源 | ||
| 84 | + * @param key 键名 | ||
| 85 | + * @param defaultValue 默认值 | ||
| 86 | + * @return BigDecimal 值,解析失败或值为 null 时返回默认值 | ||
| 87 | + */ | ||
| 88 | + public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) { | ||
| 89 | + if (map == null) { | ||
| 90 | + return defaultValue; | ||
| 91 | + } | ||
| 92 | + Object value = map.get(key); | ||
| 93 | + if (value == null) { | ||
| 94 | + return defaultValue; | ||
| 95 | + } | ||
| 96 | + if (value instanceof BigDecimal) { | ||
| 97 | + return (BigDecimal) value; | ||
| 98 | + } | ||
| 99 | + if (value instanceof Number) { | ||
| 100 | + return BigDecimal.valueOf(((Number) value).doubleValue()); | ||
| 101 | + } | ||
| 102 | + if (value instanceof String) { | ||
| 103 | + try { | ||
| 104 | + return new BigDecimal((String) value); | ||
| 105 | + } catch (NumberFormatException e) { | ||
| 106 | + return defaultValue; | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + return defaultValue; | ||
| 110 | + } | ||
| 111 | + | ||
| 68 | } | 112 | } |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java
| 1 | package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; | 1 | package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; |
| 2 | 2 | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | +import jakarta.validation.constraints.DecimalMax; | ||
| 5 | +import jakarta.validation.constraints.DecimalMin; | ||
| 4 | import lombok.Data; | 6 | import lombok.Data; |
| 5 | 7 | ||
| 6 | import java.math.BigDecimal; | 8 | import java.math.BigDecimal; |
| @@ -37,10 +39,14 @@ public class IotDeviceSaveReqVO { | @@ -37,10 +39,14 @@ public class IotDeviceSaveReqVO { | ||
| 37 | @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") | 39 | @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}") |
| 38 | private String config; | 40 | private String config; |
| 39 | 41 | ||
| 40 | - @Schema(description = "设备位置的纬度", example = "16380") | 42 | + @Schema(description = "设备位置的纬度", example = "39.915") |
| 43 | + @DecimalMin(value = "-90", message = "纬度范围为 -90 到 90") | ||
| 44 | + @DecimalMax(value = "90", message = "纬度范围为 -90 到 90") | ||
| 41 | private BigDecimal latitude; | 45 | private BigDecimal latitude; |
| 42 | 46 | ||
| 43 | - @Schema(description = "设备位置的经度", example = "16380") | 47 | + @Schema(description = "设备位置的经度", example = "116.404") |
| 48 | + @DecimalMin(value = "-180", message = "经度范围为 -180 到 180") | ||
| 49 | + @DecimalMax(value = "180", message = "经度范围为 -180 到 180") | ||
| 44 | private BigDecimal longitude; | 50 | private BigDecimal longitude; |
| 45 | 51 | ||
| 46 | } | 52 | } |
| 47 | \ No newline at end of file | 53 | \ No newline at end of file |
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 { | @@ -137,16 +137,6 @@ public class IotDeviceDO extends TenantBaseDO { | ||
| 137 | * 设备位置的经度 | 137 | * 设备位置的经度 |
| 138 | */ | 138 | */ |
| 139 | private BigDecimal longitude; | 139 | private BigDecimal longitude; |
| 140 | - /** | ||
| 141 | - * 地区编码 | ||
| 142 | - * <p> | ||
| 143 | - * 关联 Area 的 id | ||
| 144 | - */ | ||
| 145 | - private Integer areaId; | ||
| 146 | - /** | ||
| 147 | - * 设备详细地址 | ||
| 148 | - */ | ||
| 149 | - private String address; | ||
| 150 | 140 | ||
| 151 | /** | 141 | /** |
| 152 | * 设备配置 | 142 | * 设备配置 |
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; | @@ -36,6 +36,7 @@ import java.time.LocalDateTime; | ||
| 36 | import java.util.*; | 36 | import java.util.*; |
| 37 | 37 | ||
| 38 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; | 38 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; |
| 39 | +import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.getBigDecimal; | ||
| 39 | 40 | ||
| 40 | /** | 41 | /** |
| 41 | * IoT 设备【属性】数据 Service 实现类 | 42 | * IoT 设备【属性】数据 Service 实现类 |
| @@ -131,50 +132,59 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | @@ -131,50 +132,59 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | ||
| 131 | } | 132 | } |
| 132 | 133 | ||
| 133 | @Override | 134 | @Override |
| 135 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 134 | public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { | 136 | public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { |
| 135 | if (!(message.getParams() instanceof Map)) { | 137 | if (!(message.getParams() instanceof Map)) { |
| 136 | log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); | 138 | log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); |
| 137 | return; | 139 | return; |
| 138 | } | 140 | } |
| 141 | + Map<?, ?> params = (Map<?, ?>) message.getParams(); | ||
| 142 | + if (CollUtil.isEmpty(params)) { | ||
| 143 | + log.error("[saveDeviceProperty][消息内容({}) 的 data 为空]", message); | ||
| 144 | + return; | ||
| 145 | + } | ||
| 139 | 146 | ||
| 140 | // 1. 根据物模型,拼接合法的属性 | 147 | // 1. 根据物模型,拼接合法的属性 |
| 141 | // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? | 148 | // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? |
| 142 | List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); | 149 | List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); |
| 143 | Map<String, Object> properties = new HashMap<>(); | 150 | Map<String, Object> properties = new HashMap<>(); |
| 144 | - ((Map<?, ?>) message.getParams()).forEach((key, value) -> { | 151 | + params.forEach((key, value) -> { |
| 145 | IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key)); | 152 | IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key)); |
| 146 | if (thingModel == null || thingModel.getProperty() == null) { | 153 | if (thingModel == null || thingModel.getProperty() == null) { |
| 147 | log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); | 154 | log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); |
| 148 | return; | 155 | return; |
| 149 | } | 156 | } |
| 150 | - if (ObjectUtils.equalsAny(thingModel.getProperty().getDataType(), | 157 | + String dataType = thingModel.getProperty().getDataType(); |
| 158 | + if (ObjectUtils.equalsAny(dataType, | ||
| 151 | IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { | 159 | IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { |
| 152 | // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 | 160 | // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 |
| 153 | properties.put((String) key, JsonUtils.toJsonString(value)); | 161 | properties.put((String) key, JsonUtils.toJsonString(value)); |
| 154 | - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(thingModel.getProperty().getDataType())) { | ||
| 155 | - properties.put((String) key, Convert.toDouble(value)); | ||
| 156 | - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(thingModel.getProperty().getDataType())) { | 162 | + } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { |
| 163 | + properties.put((String) key, Convert.toInt(value)); | ||
| 164 | + } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { | ||
| 157 | properties.put((String) key, Convert.toFloat(value)); | 165 | properties.put((String) key, Convert.toFloat(value)); |
| 158 | - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(thingModel.getProperty().getDataType())) { | 166 | + } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { |
| 167 | + properties.put((String) key, Convert.toDouble(value)); | ||
| 168 | + } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { | ||
| 159 | properties.put((String) key, Convert.toByte(value)); | 169 | properties.put((String) key, Convert.toByte(value)); |
| 160 | - } else { | 170 | + } else { |
| 161 | properties.put((String) key, value); | 171 | properties.put((String) key, value); |
| 162 | } | 172 | } |
| 163 | }); | 173 | }); |
| 164 | if (CollUtil.isEmpty(properties)) { | 174 | if (CollUtil.isEmpty(properties)) { |
| 165 | log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); | 175 | log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message); |
| 166 | - return; | ||
| 167 | - } | ||
| 168 | - | ||
| 169 | - // 2.1 保存设备属性【数据】 | ||
| 170 | - devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); | 176 | + } else { |
| 177 | + // 2.1 保存设备属性【数据】 | ||
| 178 | + devicePropertyMapper.insert(device, properties, LocalDateTimeUtil.toEpochMilli(message.getReportTime())); | ||
| 171 | 179 | ||
| 172 | - // 2.2 保存设备属性【日志】 | ||
| 173 | - Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> | ||
| 174 | - IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); | ||
| 175 | - deviceDataRedisDAO.putAll(device.getId(), properties2); | 180 | + // 2.2 保存设备属性【日志】 |
| 181 | + Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> | ||
| 182 | + IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); | ||
| 183 | + deviceDataRedisDAO.putAll(device.getId(), properties2); | ||
| 184 | + } | ||
| 176 | 185 | ||
| 177 | // 2.3 提取 GeoLocation 并更新设备定位 | 186 | // 2.3 提取 GeoLocation 并更新设备定位 |
| 187 | + // 为什么 properties 为空,也要执行定位更新?因为可能上报的属性里,没有合法属性,但是包含 GeoLocation 定位属性 | ||
| 178 | extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams()); | 188 | extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams()); |
| 179 | } | 189 | } |
| 180 | 190 | ||
| @@ -231,14 +241,13 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | @@ -231,14 +241,13 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | ||
| 231 | */ | 241 | */ |
| 232 | private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) { | 242 | private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) { |
| 233 | // 1. 解析 GeoLocation 经纬度坐标 | 243 | // 1. 解析 GeoLocation 经纬度坐标 |
| 234 | - Double[] location = parseGeoLocation(params); | 244 | + BigDecimal[] location = parseGeoLocation(params); |
| 235 | if (location == null) { | 245 | if (location == null) { |
| 236 | return; | 246 | return; |
| 237 | } | 247 | } |
| 238 | 248 | ||
| 239 | // 2. 更新设备定位 | 249 | // 2. 更新设备定位 |
| 240 | - deviceService.updateDeviceLocation(device, | ||
| 241 | - BigDecimal.valueOf(location[0]), BigDecimal.valueOf(location[1])); | 250 | + deviceService.updateDeviceLocation(device, location[0], location[1]); |
| 242 | log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]", | 251 | log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]", |
| 243 | device.getId(), location[0], location[1]); | 252 | device.getId(), location[0], location[1]); |
| 244 | } | 253 | } |
| @@ -250,8 +259,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | @@ -250,8 +259,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | ||
| 250 | * @return [经度, 纬度],解析失败返回 null | 259 | * @return [经度, 纬度],解析失败返回 null |
| 251 | */ | 260 | */ |
| 252 | @SuppressWarnings("unchecked") | 261 | @SuppressWarnings("unchecked") |
| 253 | - // TODO @AI:返回 BigDecimal 数组; | ||
| 254 | - private Double[] parseGeoLocation(Map<?, ?> params) { | 262 | + private BigDecimal[] parseGeoLocation(Map<?, ?> params) { |
| 255 | if (params == null) { | 263 | if (params == null) { |
| 256 | return null; | 264 | return null; |
| 257 | } | 265 | } |
| @@ -276,18 +284,24 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | @@ -276,18 +284,24 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { | ||
| 276 | } | 284 | } |
| 277 | 285 | ||
| 278 | // 3. 提取经纬度(支持阿里云命名规范:首字母大写) | 286 | // 3. 提取经纬度(支持阿里云命名规范:首字母大写) |
| 279 | - Double longitude = MapUtil.getDouble(geoLocation, "Longitude"); | 287 | + BigDecimal longitude = getBigDecimal(geoLocation, "Longitude"); |
| 280 | if (longitude == null) { | 288 | if (longitude == null) { |
| 281 | - longitude = MapUtil.getDouble(geoLocation, "longitude"); | 289 | + longitude = getBigDecimal(geoLocation, "longitude"); |
| 282 | } | 290 | } |
| 283 | - Double latitude = MapUtil.getDouble(geoLocation, "Latitude"); | 291 | + BigDecimal latitude = getBigDecimal(geoLocation, "Latitude"); |
| 284 | if (latitude == null) { | 292 | if (latitude == null) { |
| 285 | - latitude = MapUtil.getDouble(geoLocation, "latitude"); | 293 | + latitude = getBigDecimal(geoLocation, "latitude"); |
| 286 | } | 294 | } |
| 287 | if (longitude == null || latitude == null) { | 295 | if (longitude == null || latitude == null) { |
| 288 | return null; | 296 | return null; |
| 289 | } | 297 | } |
| 290 | - return new Double[]{longitude, latitude}; | 298 | + // 校验经纬度范围:经度 -180 到 180,纬度 -90 到 90 |
| 299 | + if (longitude.compareTo(BigDecimal.valueOf(-180)) < 0 || longitude.compareTo(BigDecimal.valueOf(180)) > 0 | ||
| 300 | + || latitude.compareTo(BigDecimal.valueOf(-90)) < 0 || latitude.compareTo(BigDecimal.valueOf(90)) > 0) { | ||
| 301 | + log.warn("[parseGeoLocation][经纬度超出有效范围: lng={}, lat={}]", longitude, latitude); | ||
| 302 | + return null; | ||
| 303 | + } | ||
| 304 | + return new BigDecimal[]{longitude, latitude}; | ||
| 291 | } | 305 | } |
| 292 | 306 | ||
| 293 | } | 307 | } |
| 294 | \ No newline at end of file | 308 | \ No newline at end of file |