mirror of
https://github.com/YunaiV/ruoyi-vue-pro.git
synced 2026-03-30 03:13:04 +00:00
feat(iot):【设备订单:50%】简化设备定位功能,支持 GeoLocation 自动更新,基于 calm-roaming-pillow.md
This commit is contained in:
@@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
|
||||
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -153,10 +152,9 @@ public class IotDeviceController {
|
||||
// 手动创建导出 demo
|
||||
List<IotDeviceImportExcelVO> list = Arrays.asList(
|
||||
IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110")
|
||||
.productKey("1de24640dfe").groupNames("灰度分组,生产分组")
|
||||
.locationType(IotLocationTypeEnum.IP.getType()).build(),
|
||||
.productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(),
|
||||
IotDeviceImportExcelVO.builder().deviceName("biubiu").productKey("YzvHxd4r67sT4s2B")
|
||||
.groupNames("").locationType(IotLocationTypeEnum.MANUAL.getType()).build());
|
||||
.groupNames("").build());
|
||||
// 输出
|
||||
ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -35,9 +32,4 @@ public class IotDeviceImportExcelVO {
|
||||
@ExcelProperty("设备分组")
|
||||
private String groupNames;
|
||||
|
||||
@ExcelProperty("上报方式(1:IP 定位, 2:设备上报,3:手动定位)")
|
||||
@NotNull(message = "上报方式不能为空")
|
||||
@InEnum(IotLocationTypeEnum.class)
|
||||
private Integer locationType;
|
||||
|
||||
}
|
||||
|
||||
@@ -84,11 +84,6 @@ public class IotDeviceRespVO {
|
||||
@Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
|
||||
private String config;
|
||||
|
||||
@Schema(description = "定位方式", example = "2")
|
||||
@ExcelProperty(value = "定位方式", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.LOCATION_TYPE)
|
||||
private Integer locationType;
|
||||
|
||||
@Schema(description = "设备位置的纬度", example = "45.000000")
|
||||
private BigDecimal latitude;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -39,10 +37,6 @@ public class IotDeviceSaveReqVO {
|
||||
@Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
|
||||
private String config;
|
||||
|
||||
@Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")
|
||||
private Integer locationType;
|
||||
|
||||
@Schema(description = "设备位置的纬度", example = "16380")
|
||||
private BigDecimal latitude;
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ public class IotProductController {
|
||||
List<IotProductDO> list = productService.getProductList();
|
||||
return success(convertList(list, product -> // 只返回 id、name 字段
|
||||
new IotProductRespVO().setId(product.getId()).setName(product.getName()).setStatus(product.getStatus())
|
||||
.setDeviceType(product.getDeviceType()).setLocationType(product.getLocationType())));
|
||||
.setDeviceType(product.getDeviceType())));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -61,11 +61,6 @@ public class IotProductRespVO {
|
||||
@DictFormat(DictTypeConstants.NET_TYPE)
|
||||
private Integer netType;
|
||||
|
||||
@Schema(description = "定位方式", example = "2")
|
||||
@ExcelProperty(value = "定位方式", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.LOCATION_TYPE)
|
||||
private Integer locationType;
|
||||
|
||||
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@ExcelProperty(value = "数据格式", converter = DictConvert.class)
|
||||
@DictFormat(DictTypeConstants.CODEC_TYPE)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -45,10 +44,6 @@ public class IotProductSaveReqVO {
|
||||
@InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
|
||||
private Integer netType;
|
||||
|
||||
@Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")
|
||||
private Integer locationType;
|
||||
|
||||
@Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||
@NotEmpty(message = "数据格式不能为空")
|
||||
private String codecType;
|
||||
|
||||
@@ -129,12 +129,6 @@ public class IotDeviceDO extends TenantBaseDO {
|
||||
// TODO @haohao:是不是要枚举哈
|
||||
private String authType;
|
||||
|
||||
/**
|
||||
* 定位方式
|
||||
* <p>
|
||||
* 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}
|
||||
*/
|
||||
private Integer locationType;
|
||||
/**
|
||||
* 设备位置的纬度
|
||||
*/
|
||||
|
||||
@@ -69,12 +69,6 @@ public class IotProductDO extends TenantBaseDO {
|
||||
* 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum}
|
||||
*/
|
||||
private Integer netType;
|
||||
/**
|
||||
* 定位方式
|
||||
* <p>
|
||||
* 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}
|
||||
*/
|
||||
private Integer locationType;
|
||||
/**
|
||||
* 数据格式(编解码器类型)
|
||||
* <p>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package cn.iocoder.yudao.module.iot.enums.product;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* IoT 定位方式枚举类
|
||||
*
|
||||
* @author alwayssuper
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum IotLocationTypeEnum implements ArrayValuable<Integer> {
|
||||
|
||||
IP(1, "IP 定位"),
|
||||
DEVICE(2, "设备上报"),
|
||||
MANUAL(3, "手动定位");
|
||||
|
||||
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private final Integer type;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private final String description;
|
||||
|
||||
@Override
|
||||
public Integer[] array() {
|
||||
return ARRAYS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -271,4 +272,13 @@ public interface IotDeviceService {
|
||||
*/
|
||||
void updateDeviceFirmware(Long deviceId, Long firmwareId);
|
||||
|
||||
/**
|
||||
* 更新设备定位信息(GeoLocation 上报时调用)
|
||||
*
|
||||
* @param device 设备信息(用于清除缓存)
|
||||
* @param longitude 经度
|
||||
* @param latitude 纬度
|
||||
*/
|
||||
void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude);
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@@ -376,8 +377,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
if (existDevice == null) {
|
||||
createDevice(new IotDeviceSaveReqVO()
|
||||
.setDeviceName(importDevice.getDeviceName())
|
||||
.setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)
|
||||
.setLocationType(importDevice.getLocationType()));
|
||||
.setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds));
|
||||
respVO.getCreateDeviceNames().add(importDevice.getDeviceName());
|
||||
return;
|
||||
}
|
||||
@@ -386,7 +386,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
throw exception(DEVICE_KEY_EXISTS);
|
||||
}
|
||||
updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId())
|
||||
.setGatewayId(gatewayId).setGroupIds(groupIds).setLocationType(importDevice.getLocationType()));
|
||||
.setGatewayId(gatewayId).setGroupIds(groupIds));
|
||||
respVO.getUpdateDeviceNames().add(importDevice.getDeviceName());
|
||||
} catch (ServiceException ex) {
|
||||
respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage());
|
||||
@@ -490,15 +490,25 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
public void updateDeviceFirmware(Long deviceId, Long firmwareId) {
|
||||
// 1. 校验设备是否存在
|
||||
IotDeviceDO device = validateDeviceExists(deviceId);
|
||||
|
||||
|
||||
// 2. 更新设备固件版本
|
||||
IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId);
|
||||
deviceMapper.updateById(updateObj);
|
||||
|
||||
|
||||
// 3. 清空对应缓存
|
||||
deleteDeviceCache(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude) {
|
||||
// 1. 更新定位信息
|
||||
deviceMapper.updateById(new IotDeviceDO().setId(device.getId())
|
||||
.setLongitude(longitude).setLatitude(latitude));
|
||||
|
||||
// 2. 清空对应缓存
|
||||
deleteDeviceCache(device);
|
||||
}
|
||||
|
||||
private IotDeviceServiceImpl getSelf() {
|
||||
return SpringUtil.getBean(getClass());
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
|
||||
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
|
||||
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
|
||||
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
|
||||
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
|
||||
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -30,6 +31,7 @@ import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
@@ -66,6 +68,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotProductService productService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private DevicePropertyRedisDAO deviceDataRedisDAO;
|
||||
@@ -168,6 +173,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
|
||||
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 并更新设备定位
|
||||
extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,4 +221,73 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
|
||||
return deviceServerIdRedisDAO.get(id);
|
||||
}
|
||||
|
||||
// ========== 设备定位相关操作 ==========
|
||||
|
||||
/**
|
||||
* 从属性中提取 GeoLocation 并更新设备定位
|
||||
*
|
||||
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/device-geolocation">阿里云规范</a>
|
||||
* GeoLocation 结构体包含:Longitude, Latitude, Altitude, CoordinateSystem
|
||||
*/
|
||||
private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) {
|
||||
// 1. 解析 GeoLocation 经纬度坐标
|
||||
Double[] location = parseGeoLocation(params);
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 更新设备定位
|
||||
deviceService.updateDeviceLocation(device,
|
||||
BigDecimal.valueOf(location[0]), BigDecimal.valueOf(location[1]));
|
||||
log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]",
|
||||
device.getId(), location[0], location[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从属性参数中解析 GeoLocation,返回经纬度坐标数组 [longitude, latitude]
|
||||
*
|
||||
* @param params 属性参数
|
||||
* @return [经度, 纬度],解析失败返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
// TODO @AI:返回 BigDecimal 数组;
|
||||
private Double[] parseGeoLocation(Map<?, ?> params) {
|
||||
if (params == null) {
|
||||
return null;
|
||||
}
|
||||
// 1. 查找 GeoLocation 属性(标识符为 GeoLocation 或 geoLocation)
|
||||
Object geoValue = params.get("GeoLocation");
|
||||
if (geoValue == null) {
|
||||
geoValue = params.get("geoLocation");
|
||||
}
|
||||
if (geoValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. 转换为 Map
|
||||
Map<String, Object> geoLocation = null;
|
||||
if (geoValue instanceof Map) {
|
||||
geoLocation = (Map<String, Object>) geoValue;
|
||||
} else if (geoValue instanceof String) {
|
||||
geoLocation = JsonUtils.parseObject((String) geoValue, Map.class);
|
||||
}
|
||||
if (geoLocation == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 提取经纬度(支持阿里云命名规范:首字母大写)
|
||||
Double longitude = MapUtil.getDouble(geoLocation, "Longitude");
|
||||
if (longitude == null) {
|
||||
longitude = MapUtil.getDouble(geoLocation, "longitude");
|
||||
}
|
||||
Double latitude = MapUtil.getDouble(geoLocation, "Latitude");
|
||||
if (latitude == null) {
|
||||
latitude = MapUtil.getDouble(geoLocation, "latitude");
|
||||
}
|
||||
if (longitude == null || latitude == null) {
|
||||
return null;
|
||||
}
|
||||
return new Double[]{longitude, latitude};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -271,6 +271,10 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
|
||||
if (ObjUtil.notEqual(action.getType(), dataSink.getType())) {
|
||||
return;
|
||||
}
|
||||
if (CommonStatusEnum.isDisable(dataSink.getStatus())) {
|
||||
log.warn("[executeDataRuleAction][消息({}) 数据目的({}) 状态为禁用]", message.getId(), dataSink.getId());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
action.execute(message, dataSink);
|
||||
log.info("[executeDataRuleAction][消息({}) 数据目的({}) 执行成功]", message.getId(), dataSink.getId());
|
||||
|
||||
Reference in New Issue
Block a user