feat(iot):【设备订单:50%】简化设备定位功能,支持 GeoLocation 自动更新,基于 calm-roaming-pillow.md

This commit is contained in:
YunaiV
2026-01-20 21:41:43 +08:00
parent 28a30d4b79
commit 3a832d9fb4
14 changed files with 109 additions and 89 deletions

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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())));
}
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;
/**
* 设备位置的纬度
*/

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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};
}
}

View File

@@ -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());