Commit 3a832d9fb46e2078745f8040aee72134a07dc942

Authored by YunaiV
1 parent 28a30d4b

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

Showing 14 changed files with 109 additions and 89 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.java
... ... @@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 8 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
9 9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.*;
10 10 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
11   -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
12 11 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
13 12 import io.swagger.v3.oas.annotations.Operation;
14 13 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -153,10 +152,9 @@ public class IotDeviceController {
153 152 // 手动创建导出 demo
154 153 List<IotDeviceImportExcelVO> list = Arrays.asList(
155 154 IotDeviceImportExcelVO.builder().deviceName("温度传感器001").parentDeviceName("gateway110")
156   - .productKey("1de24640dfe").groupNames("灰度分组,生产分组")
157   - .locationType(IotLocationTypeEnum.IP.getType()).build(),
  155 + .productKey("1de24640dfe").groupNames("灰度分组,生产分组").build(),
158 156 IotDeviceImportExcelVO.builder().deviceName("biubiu").productKey("YzvHxd4r67sT4s2B")
159   - .groupNames("").locationType(IotLocationTypeEnum.MANUAL.getType()).build());
  157 + .groupNames("").build());
160 158 // 输出
161 159 ExcelUtils.write(response, "设备导入模板.xls", "数据", IotDeviceImportExcelVO.class, list);
162 160 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
2 2  
3 3 import cn.idev.excel.annotation.ExcelProperty;
4   -import cn.iocoder.yudao.framework.common.validation.InEnum;
5   -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
6 4 import io.swagger.v3.oas.annotations.media.Schema;
7 5 import jakarta.validation.constraints.NotEmpty;
8   -import jakarta.validation.constraints.NotNull;
9 6 import lombok.AllArgsConstructor;
10 7 import lombok.Builder;
11 8 import lombok.Data;
... ... @@ -35,9 +32,4 @@ public class IotDeviceImportExcelVO {
35 32 @ExcelProperty("设备分组")
36 33 private String groupNames;
37 34  
38   - @ExcelProperty("上报方式(1:IP 定位, 2:设备上报,3:手动定位)")
39   - @NotNull(message = "上报方式不能为空")
40   - @InEnum(IotLocationTypeEnum.class)
41   - private Integer locationType;
42   -
43 35 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
... ... @@ -84,11 +84,6 @@ public class IotDeviceRespVO {
84 84 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
85 85 private String config;
86 86  
87   - @Schema(description = "定位方式", example = "2")
88   - @ExcelProperty(value = "定位方式", converter = DictConvert.class)
89   - @DictFormat(DictTypeConstants.LOCATION_TYPE)
90   - private Integer locationType;
91   -
92 87 @Schema(description = "设备位置的纬度", example = "45.000000")
93 88 private BigDecimal latitude;
94 89  
... ...
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   -import cn.iocoder.yudao.framework.common.validation.InEnum;
4   -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
7 5  
... ... @@ -39,10 +37,6 @@ public class IotDeviceSaveReqVO {
39 37 @Schema(description = "设备配置", example = "{\"abc\": \"efg\"}")
40 38 private String config;
41 39  
42   - @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
43   - @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")
44   - private Integer locationType;
45   -
46 40 @Schema(description = "设备位置的纬度", example = "16380")
47 41 private BigDecimal latitude;
48 42  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/IotProductController.java
... ... @@ -147,7 +147,7 @@ public class IotProductController {
147 147 List<IotProductDO> list = productService.getProductList();
148 148 return success(convertList(list, product -> // 只返回 id、name 字段
149 149 new IotProductRespVO().setId(product.getId()).setName(product.getName()).setStatus(product.getStatus())
150   - .setDeviceType(product.getDeviceType()).setLocationType(product.getLocationType())));
  150 + .setDeviceType(product.getDeviceType())));
151 151 }
152 152  
153 153 }
154 154 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java
... ... @@ -61,11 +61,6 @@ public class IotProductRespVO {
61 61 @DictFormat(DictTypeConstants.NET_TYPE)
62 62 private Integer netType;
63 63  
64   - @Schema(description = "定位方式", example = "2")
65   - @ExcelProperty(value = "定位方式", converter = DictConvert.class)
66   - @DictFormat(DictTypeConstants.LOCATION_TYPE)
67   - private Integer locationType;
68   -
69 64 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
70 65 @ExcelProperty(value = "数据格式", converter = DictConvert.class)
71 66 @DictFormat(DictTypeConstants.CODEC_TYPE)
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductSaveReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4   -import cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum;
5 4 import cn.iocoder.yudao.module.iot.enums.product.IotNetTypeEnum;
6 5 import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
7 6 import io.swagger.v3.oas.annotations.media.Schema;
... ... @@ -45,10 +44,6 @@ public class IotProductSaveReqVO {
45 44 @InEnum(value = IotNetTypeEnum.class, message = "联网方式必须是 {value}")
46 45 private Integer netType;
47 46  
48   - @Schema(description = "定位类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
49   - @InEnum(value = IotLocationTypeEnum.class, message = "定位方式必须是 {value}")
50   - private Integer locationType;
51   -
52 47 @Schema(description = "数据格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
53 48 @NotEmpty(message = "数据格式不能为空")
54 49 private String codecType;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceDO.java
... ... @@ -130,12 +130,6 @@ public class IotDeviceDO extends TenantBaseDO {
130 130 private String authType;
131 131  
132 132 /**
133   - * 定位方式
134   - * <p>
135   - * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}
136   - */
137   - private Integer locationType;
138   - /**
139 133 * 设备位置的纬度
140 134 */
141 135 private BigDecimal latitude;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/product/IotProductDO.java
... ... @@ -70,12 +70,6 @@ public class IotProductDO extends TenantBaseDO {
70 70 */
71 71 private Integer netType;
72 72 /**
73   - * 定位方式
74   - * <p>
75   - * 枚举 {@link cn.iocoder.yudao.module.iot.enums.product.IotLocationTypeEnum}
76   - */
77   - private Integer locationType;
78   - /**
79 73 * 数据格式(编解码器类型)
80 74 * <p>
81 75 * 字典 {@link cn.iocoder.yudao.module.iot.enums.DictTypeConstants#CODEC_TYPE}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java deleted
1   -package cn.iocoder.yudao.module.iot.enums.product;
2   -
3   -import cn.iocoder.yudao.framework.common.core.ArrayValuable;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Getter;
6   -
7   -import java.util.Arrays;
8   -
9   -/**
10   - * IoT 定位方式枚举类
11   - *
12   - * @author alwayssuper
13   - */
14   -@AllArgsConstructor
15   -@Getter
16   -public enum IotLocationTypeEnum implements ArrayValuable<Integer> {
17   -
18   - IP(1, "IP 定位"),
19   - DEVICE(2, "设备上报"),
20   - MANUAL(3, "手动定位");
21   -
22   - public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);
23   -
24   - /**
25   - * 类型
26   - */
27   - private final Integer type;
28   - /**
29   - * 描述
30   - */
31   - private final String description;
32   -
33   - @Override
34   - public Integer[] array() {
35   - return ARRAYS;
36   - }
37   -
38   -}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceService.java
... ... @@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
8 8 import jakarta.validation.Valid;
9 9  
10 10 import javax.annotation.Nullable;
  11 +import java.math.BigDecimal;
11 12 import java.time.LocalDateTime;
12 13 import java.util.Collection;
13 14 import java.util.List;
... ... @@ -271,4 +272,13 @@ public interface IotDeviceService {
271 272 */
272 273 void updateDeviceFirmware(Long deviceId, Long firmwareId);
273 274  
  275 + /**
  276 + * 更新设备定位信息(GeoLocation 上报时调用)
  277 + *
  278 + * @param device 设备信息(用于清除缓存)
  279 + * @param longitude 经度
  280 + * @param latitude 纬度
  281 + */
  282 + void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude);
  283 +
274 284 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
... ... @@ -34,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
34 34 import org.springframework.validation.annotation.Validated;
35 35  
36 36 import javax.annotation.Nullable;
  37 +import java.math.BigDecimal;
37 38 import java.time.LocalDateTime;
38 39 import java.util.*;
39 40  
... ... @@ -376,8 +377,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
376 377 if (existDevice == null) {
377 378 createDevice(new IotDeviceSaveReqVO()
378 379 .setDeviceName(importDevice.getDeviceName())
379   - .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds)
380   - .setLocationType(importDevice.getLocationType()));
  380 + .setProductId(product.getId()).setGatewayId(gatewayId).setGroupIds(groupIds));
381 381 respVO.getCreateDeviceNames().add(importDevice.getDeviceName());
382 382 return;
383 383 }
... ... @@ -386,7 +386,7 @@ public class IotDeviceServiceImpl implements IotDeviceService {
386 386 throw exception(DEVICE_KEY_EXISTS);
387 387 }
388 388 updateDevice(new IotDeviceSaveReqVO().setId(existDevice.getId())
389   - .setGatewayId(gatewayId).setGroupIds(groupIds).setLocationType(importDevice.getLocationType()));
  389 + .setGatewayId(gatewayId).setGroupIds(groupIds));
390 390 respVO.getUpdateDeviceNames().add(importDevice.getDeviceName());
391 391 } catch (ServiceException ex) {
392 392 respVO.getFailureDeviceNames().put(importDevice.getDeviceName(), ex.getMessage());
... ... @@ -490,15 +490,25 @@ public class IotDeviceServiceImpl implements IotDeviceService {
490 490 public void updateDeviceFirmware(Long deviceId, Long firmwareId) {
491 491 // 1. 校验设备是否存在
492 492 IotDeviceDO device = validateDeviceExists(deviceId);
493   -
  493 +
494 494 // 2. 更新设备固件版本
495 495 IotDeviceDO updateObj = new IotDeviceDO().setId(deviceId).setFirmwareId(firmwareId);
496 496 deviceMapper.updateById(updateObj);
497   -
  497 +
498 498 // 3. 清空对应缓存
499 499 deleteDeviceCache(device);
500 500 }
501 501  
  502 + @Override
  503 + public void updateDeviceLocation(IotDeviceDO device, BigDecimal longitude, BigDecimal latitude) {
  504 + // 1. 更新定位信息
  505 + deviceMapper.updateById(new IotDeviceDO().setId(device.getId())
  506 + .setLongitude(longitude).setLatitude(latitude));
  507 +
  508 + // 2. 清空对应缓存
  509 + deleteDeviceCache(device);
  510 + }
  511 +
502 512 private IotDeviceServiceImpl getSelf() {
503 513 return SpringUtil.getBean(getClass());
504 514 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
... ... @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
22 22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
23 23 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
24 24 import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  25 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
25 26 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
26 27 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
27 28 import jakarta.annotation.Resource;
... ... @@ -30,6 +31,7 @@ import org.springframework.context.annotation.Lazy;
30 31 import org.springframework.scheduling.annotation.Async;
31 32 import org.springframework.stereotype.Service;
32 33  
  34 +import java.math.BigDecimal;
33 35 import java.time.LocalDateTime;
34 36 import java.util.*;
35 37  
... ... @@ -66,6 +68,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
66 68 @Resource
67 69 @Lazy // 延迟加载,解决循环依赖
68 70 private IotProductService productService;
  71 + @Resource
  72 + @Lazy // 延迟加载,解决循环依赖
  73 + private IotDeviceService deviceService;
69 74  
70 75 @Resource
71 76 private DevicePropertyRedisDAO deviceDataRedisDAO;
... ... @@ -168,6 +173,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
168 173 Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry ->
169 174 IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build());
170 175 deviceDataRedisDAO.putAll(device.getId(), properties2);
  176 +
  177 + // 2.3 提取 GeoLocation 并更新设备定位
  178 + extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams());
171 179 }
172 180  
173 181 @Override
... ... @@ -213,4 +221,73 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
213 221 return deviceServerIdRedisDAO.get(id);
214 222 }
215 223  
  224 + // ========== 设备定位相关操作 ==========
  225 +
  226 + /**
  227 + * 从属性中提取 GeoLocation 并更新设备定位
  228 + *
  229 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/device-geolocation">阿里云规范</a>
  230 + * GeoLocation 结构体包含:Longitude, Latitude, Altitude, CoordinateSystem
  231 + */
  232 + private void extractAndUpdateDeviceLocation(IotDeviceDO device, Map<?, ?> params) {
  233 + // 1. 解析 GeoLocation 经纬度坐标
  234 + Double[] location = parseGeoLocation(params);
  235 + if (location == null) {
  236 + return;
  237 + }
  238 +
  239 + // 2. 更新设备定位
  240 + deviceService.updateDeviceLocation(device,
  241 + BigDecimal.valueOf(location[0]), BigDecimal.valueOf(location[1]));
  242 + log.info("[extractAndUpdateGeoLocation][设备({}) 定位更新: lng={}, lat={}]",
  243 + device.getId(), location[0], location[1]);
  244 + }
  245 +
  246 + /**
  247 + * 从属性参数中解析 GeoLocation,返回经纬度坐标数组 [longitude, latitude]
  248 + *
  249 + * @param params 属性参数
  250 + * @return [经度, 纬度],解析失败返回 null
  251 + */
  252 + @SuppressWarnings("unchecked")
  253 + // TODO @AI:返回 BigDecimal 数组;
  254 + private Double[] parseGeoLocation(Map<?, ?> params) {
  255 + if (params == null) {
  256 + return null;
  257 + }
  258 + // 1. 查找 GeoLocation 属性(标识符为 GeoLocation 或 geoLocation)
  259 + Object geoValue = params.get("GeoLocation");
  260 + if (geoValue == null) {
  261 + geoValue = params.get("geoLocation");
  262 + }
  263 + if (geoValue == null) {
  264 + return null;
  265 + }
  266 +
  267 + // 2. 转换为 Map
  268 + Map<String, Object> geoLocation = null;
  269 + if (geoValue instanceof Map) {
  270 + geoLocation = (Map<String, Object>) geoValue;
  271 + } else if (geoValue instanceof String) {
  272 + geoLocation = JsonUtils.parseObject((String) geoValue, Map.class);
  273 + }
  274 + if (geoLocation == null) {
  275 + return null;
  276 + }
  277 +
  278 + // 3. 提取经纬度(支持阿里云命名规范:首字母大写)
  279 + Double longitude = MapUtil.getDouble(geoLocation, "Longitude");
  280 + if (longitude == null) {
  281 + longitude = MapUtil.getDouble(geoLocation, "longitude");
  282 + }
  283 + Double latitude = MapUtil.getDouble(geoLocation, "Latitude");
  284 + if (latitude == null) {
  285 + latitude = MapUtil.getDouble(geoLocation, "latitude");
  286 + }
  287 + if (longitude == null || latitude == null) {
  288 + return null;
  289 + }
  290 + return new Double[]{longitude, latitude};
  291 + }
  292 +
216 293 }
217 294 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/IotDataRuleServiceImpl.java
... ... @@ -271,6 +271,10 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
271 271 if (ObjUtil.notEqual(action.getType(), dataSink.getType())) {
272 272 return;
273 273 }
  274 + if (CommonStatusEnum.isDisable(dataSink.getStatus())) {
  275 + log.warn("[executeDataRuleAction][消息({}) 数据目的({}) 状态为禁用]", message.getId(), dataSink.getId());
  276 + return;
  277 + }
274 278 try {
275 279 action.execute(message, dataSink);
276 280 log.info("[executeDataRuleAction][消息({}) 数据目的({}) 执行成功]", message.getId(), dataSink.getId());
... ...