Commit 6892571a330f882dfc875be4f664d11a255e1448

Authored by YunaiV
1 parent 3a832d9f

feat(iot):【设备定位:70%】优化设备坐标的解析逻辑代码,基于 hashed-juggling-tome.md

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