mirror of
https://github.com/YunaiV/ruoyi-vue-pro.git
synced 2026-03-30 02:33:44 +00:00
feat(iot):【设备定位:70%】优化设备坐标的解析逻辑代码,基于 hashed-juggling-tome.md
This commit is contained in:
@@ -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<String, ?> 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<String, ?> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -137,16 +137,6 @@ public class IotDeviceDO extends TenantBaseDO {
|
||||
* 设备位置的经度
|
||||
*/
|
||||
private BigDecimal longitude;
|
||||
/**
|
||||
* 地区编码
|
||||
* <p>
|
||||
* 关联 Area 的 id
|
||||
*/
|
||||
private Integer areaId;
|
||||
/**
|
||||
* 设备详细地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 设备配置
|
||||
|
||||
@@ -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<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId());
|
||||
Map<String, Object> 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<String, IotDevicePropertyDO> 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<String, IotDevicePropertyDO> 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};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user