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 7 import com.google.common.collect.Maps;
8 8 import com.google.common.collect.Multimap;
9 9  
  10 +import java.math.BigDecimal;
10 11 import java.util.ArrayList;
11 12 import java.util.Collection;
12 13 import java.util.List;
... ... @@ -65,4 +66,47 @@ public class MapUtils {
65 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 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.DecimalMax;
  5 +import jakarta.validation.constraints.DecimalMin;
4 6 import lombok.Data;
5 7  
6 8 import java.math.BigDecimal;
... ... @@ -37,10 +39,14 @@ public class IotDeviceSaveReqVO {
37 39 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
38 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 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 50 private BigDecimal longitude;
45 51  
46 52 }
47 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 137 * 设备位置的经度
138 138 */
139 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 36 import java.util.*;
37 37  
38 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 42 * IoT 设备【属性】数据 Service 实现类
... ... @@ -131,50 +132,59 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
131 132 }
132 133  
133 134 @Override
  135 + @SuppressWarnings("PatternVariableCanBeUsed")
134 136 public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) {
135 137 if (!(message.getParams() instanceof Map)) {
136 138 log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message);
137 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 147 // 1. 根据物模型,拼接合法的属性
141 148 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)?
142 149 List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId());
143 150 Map<String, Object> properties = new HashMap<>();
144   - ((Map<?, ?>) message.getParams()).forEach((key, value) -> {
  151 + params.forEach((key, value) -> {
145 152 IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> o.getIdentifier().equals(key));
146 153 if (thingModel == null || thingModel.getProperty() == null) {
147 154 log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key);
148 155 return;
149 156 }
150   - if (ObjectUtils.equalsAny(thingModel.getProperty().getDataType(),
  157 + String dataType = thingModel.getProperty().getDataType();
  158 + if (ObjectUtils.equalsAny(dataType,
151 159 IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
152 160 // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储
153 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 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 169 properties.put((String) key, Convert.toByte(value));
160   - } else {
  170 + } else {
161 171 properties.put((String) key, value);
162 172 }
163 173 });
164 174 if (CollUtil.isEmpty(properties)) {
165 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 186 // 2.3 提取 GeoLocation 并更新设备定位
  187 + // 为什么 properties 为空,也要执行定位更新?因为可能上报的属性里,没有合法属性,但是包含 GeoLocation 定位属性
178 188 extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams());
179 189 }
180 190  
... ... @@ -231,14 +241,13 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
231 241 */
232 242 private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) {
233 243 // 1. 解析 GeoLocation 经纬度坐标
234   - Double[] location = parseGeoLocation(params);
  244 + BigDecimal[] location = parseGeoLocation(params);
235 245 if (location == null) {
236 246 return;
237 247 }
238 248  
239 249 // 2. 更新设备定位
240   - deviceService.updateDeviceLocation(device,
241   - BigDecimal.valueOf(location[0]), BigDecimal.valueOf(location[1]));
  250 + deviceService.updateDeviceLocation(device, location[0], location[1]);
242 251 log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]",
243 252 device.getId(), location[0], location[1]);
244 253 }
... ... @@ -250,8 +259,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
250 259 * @return [经度, 纬度],解析失败返回 null
251 260 */
252 261 @SuppressWarnings("unchecked")
253   - // TODO @AI:返回 BigDecimal 数组;
254   - private Double[] parseGeoLocation(Map<?, ?> params) {
  262 + private BigDecimal[] parseGeoLocation(Map<?, ?> params) {
255 263 if (params == null) {
256 264 return null;
257 265 }
... ... @@ -276,18 +284,24 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
276 284 }
277 285  
278 286 // 3. 提取经纬度(支持阿里云命名规范:首字母大写)
279   - Double longitude = MapUtil.getDouble(geoLocation, "Longitude");
  287 + BigDecimal longitude = getBigDecimal(geoLocation, "Longitude");
280 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 292 if (latitude == null) {
285   - latitude = MapUtil.getDouble(geoLocation, "latitude");
  293 + latitude = getBigDecimal(geoLocation, "latitude");
286 294 }
287 295 if (longitude == null || latitude == null) {
288 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 308 \ No newline at end of file
... ...