feat(iot): 实现设备 API 接口及更新网关处理逻辑
This commit is contained in:
@@ -1,75 +1,75 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.enums.RpcConstants;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备 API 实现类
|
||||
*
|
||||
* @author haohao
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary // 保证优先匹配,因为 viewsh-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入
|
||||
public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> authDevice(@RequestBody IotDeviceAuthReqDTO authReqDTO) {
|
||||
return success(deviceService.authDevice(authReqDTO));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/get") // 特殊:方便调用,暂时使用 POST,实际更推荐 GET
|
||||
@PermitAll
|
||||
public CommonResult<IotDeviceRespDTO> getDevice(@RequestBody IotDeviceGetReqDTO getReqDTO) {
|
||||
IotDeviceDO device;
|
||||
|
||||
// 查询优先级:id > (productKey + deviceName) > deviceName
|
||||
if (getReqDTO.getId() != null) {
|
||||
// 通过设备 ID 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getId());
|
||||
} else if (getReqDTO.getProductKey() != null && getReqDTO.getDeviceName() != null) {
|
||||
// 通过 productKey + deviceName 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName());
|
||||
} else if (getReqDTO.getDeviceName() != null) {
|
||||
// 仅通过 deviceName 查询(用于 JT808 等协议,终端手机号应该是全局唯一的)
|
||||
device = deviceService.getDeviceFromCacheByDeviceName(getReqDTO.getDeviceName());
|
||||
} else {
|
||||
device = null;
|
||||
}
|
||||
|
||||
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
|
||||
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
|
||||
if (product != null) {
|
||||
deviceDTO.setCodecType(product.getCodecType());
|
||||
deviceDTO.setProductAuthType(product.getAuthType());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.enums.RpcConstants;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.iot.core.biz.IotDeviceCommonApi;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceGetReqDTO;
|
||||
import com.viewsh.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备 API 实现类(供IOT GATEWAY使用)
|
||||
*
|
||||
* @author haohao
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary // 保证优先匹配,因为 viewsh-iot-gateway 也有 IotDeviceCommonApi 的实现,并且也可能会被 biz 引入
|
||||
public class IoTDeviceApiImpl implements IotDeviceCommonApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/auth")
|
||||
@PermitAll
|
||||
public CommonResult<Boolean> authDevice(@RequestBody IotDeviceAuthReqDTO authReqDTO) {
|
||||
return success(deviceService.authDevice(authReqDTO));
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(RpcConstants.RPC_API_PREFIX + "/iot/device/get") // 特殊:方便调用,暂时使用 POST,实际更推荐 GET
|
||||
@PermitAll
|
||||
public CommonResult<IotDeviceRespDTO> getDevice(@RequestBody IotDeviceGetReqDTO getReqDTO) {
|
||||
IotDeviceDO device;
|
||||
|
||||
// 查询优先级:id > (productKey + deviceName) > deviceName
|
||||
if (getReqDTO.getId() != null) {
|
||||
// 通过设备 ID 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getId());
|
||||
} else if (getReqDTO.getProductKey() != null && getReqDTO.getDeviceName() != null) {
|
||||
// 通过 productKey + deviceName 查询
|
||||
device = deviceService.getDeviceFromCache(getReqDTO.getProductKey(), getReqDTO.getDeviceName());
|
||||
} else if (getReqDTO.getDeviceName() != null) {
|
||||
// 仅通过 deviceName 查询(用于 JT808 等协议,终端手机号应该是全局唯一的)
|
||||
device = deviceService.getDeviceFromCacheByDeviceName(getReqDTO.getDeviceName());
|
||||
} else {
|
||||
device = null;
|
||||
}
|
||||
|
||||
return success(BeanUtils.toBean(device, IotDeviceRespDTO.class, deviceDTO -> {
|
||||
IotProductDO product = productService.getProductFromCache(deviceDTO.getProductId());
|
||||
if (product != null) {
|
||||
deviceDTO.setCodecType(product.getCodecType());
|
||||
deviceDTO.setProductAuthType(product.getAuthType());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeRespDTO;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.device.message.IotDeviceMessageService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备控制 API 实现类
|
||||
* <p>
|
||||
* 提供 RPC 接口供其他模块调用 IoT 设备服务
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary
|
||||
@Slf4j
|
||||
public class IotDeviceControlApiImpl implements IotDeviceControlApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotDeviceMessageService deviceMessageService;
|
||||
|
||||
@Override
|
||||
@PostMapping(PREFIX + "/invoke-service")
|
||||
@Operation(summary = "调用设备服务")
|
||||
public CommonResult<IotDeviceServiceInvokeRespDTO> invokeService(@RequestBody IotDeviceServiceInvokeReqDTO reqDTO) {
|
||||
try {
|
||||
// 1. 获取设备信息
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(reqDTO.getDeviceId());
|
||||
if (device == null) {
|
||||
return success(IotDeviceServiceInvokeRespDTO.builder()
|
||||
.success(false)
|
||||
.code(404)
|
||||
.errorMsg("设备不存在")
|
||||
.responseTime(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
// 2. 检查设备是否在线
|
||||
if (!IotDeviceStateEnum.ONLINE.getState().equals(device.getState())) {
|
||||
return success(IotDeviceServiceInvokeRespDTO.builder()
|
||||
.success(false)
|
||||
.code(400)
|
||||
.errorMsg("设备不在线,当前状态: " + device.getState())
|
||||
.responseTime(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
// 3. 构建服务调用消息
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = (Map<String, Object>) (Map<?, ?>) MapUtil.builder()
|
||||
.put("identifier", reqDTO.getIdentifier())
|
||||
.put("params", reqDTO.getParams() != null ? reqDTO.getParams() : new HashMap<>())
|
||||
.build();
|
||||
|
||||
IotDeviceMessage message = IotDeviceMessage.requestOf(
|
||||
IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
|
||||
|
||||
// 4. 发送消息
|
||||
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(message, device);
|
||||
|
||||
// 5. 返回结果(异步模式,立即返回消息ID)
|
||||
return success(IotDeviceServiceInvokeRespDTO.builder()
|
||||
.messageId(result.getId())
|
||||
.success(true)
|
||||
.data(null)
|
||||
.responseTime(LocalDateTime.now())
|
||||
.build());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[invokeService] 设备服务调用失败: deviceId={}, identifier={}",
|
||||
reqDTO.getDeviceId(), reqDTO.getIdentifier(), e);
|
||||
return success(IotDeviceServiceInvokeRespDTO.builder()
|
||||
.success(false)
|
||||
.code(500)
|
||||
.errorMsg(e.getMessage())
|
||||
.responseTime(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(PREFIX + "/invoke-service-batch")
|
||||
@Operation(summary = "批量调用设备服务")
|
||||
public CommonResult<List<IotDeviceServiceInvokeRespDTO>> invokeServiceBatch(
|
||||
@RequestBody List<IotDeviceServiceInvokeReqDTO> reqDTOList) {
|
||||
|
||||
List<IotDeviceServiceInvokeRespDTO> results = new java.util.ArrayList<>();
|
||||
for (IotDeviceServiceInvokeReqDTO reqDTO : reqDTOList) {
|
||||
CommonResult<IotDeviceServiceInvokeRespDTO> result = invokeService(reqDTO);
|
||||
results.add(result.getData());
|
||||
}
|
||||
|
||||
return success(results);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.property.DevicePropertyBatchQueryReqDTO;
|
||||
import com.viewsh.module.iot.api.device.dto.property.DevicePropertyHistoryQueryReqDTO;
|
||||
import com.viewsh.module.iot.api.device.dto.property.DevicePropertyHistoryRespDTO;
|
||||
import com.viewsh.module.iot.api.device.dto.property.DevicePropertyRespDTO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.device.property.IotDevicePropertyService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备属性查询 API 实现类
|
||||
* <p>
|
||||
* 提供 RPC 接口供其他模块(如 Ops 模块)查询设备属性
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary
|
||||
@Slf4j
|
||||
public class IotDevicePropertyQueryApiImpl implements IotDevicePropertyQueryApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotDevicePropertyService devicePropertyService;
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/get")
|
||||
@Operation(summary = "获取设备单个属性")
|
||||
public CommonResult<DevicePropertyRespDTO> getProperty(@RequestParam("deviceId") Long deviceId,
|
||||
@RequestParam("identifier") String identifier) {
|
||||
try {
|
||||
Map<String, IotDevicePropertyDO> properties = devicePropertyService.getLatestDeviceProperties(deviceId);
|
||||
IotDevicePropertyDO property = properties.get(identifier);
|
||||
|
||||
if (property == null) {
|
||||
return success(DevicePropertyRespDTO.builder()
|
||||
.identifier(identifier)
|
||||
.value(null)
|
||||
.updateTime(null)
|
||||
.build());
|
||||
}
|
||||
|
||||
return success(DevicePropertyRespDTO.builder()
|
||||
.identifier(identifier)
|
||||
.value(property.getValue())
|
||||
.updateTime(property.getUpdateTime() != null ?
|
||||
property.getUpdateTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() : null)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.error("[getProperty] 获取设备属性失败: deviceId={}, identifier={}", deviceId, identifier, e);
|
||||
return success(DevicePropertyRespDTO.builder()
|
||||
.identifier(identifier)
|
||||
.value(null)
|
||||
.updateTime(null)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/get-latest")
|
||||
@Operation(summary = "获取设备最新属性")
|
||||
public CommonResult<Map<String, Object>> getLatestProperties(@RequestParam("deviceId") Long deviceId) {
|
||||
try {
|
||||
// 验证设备是否存在
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
if (device == null) {
|
||||
log.warn("[getLatestProperties] 设备不存在: deviceId={}", deviceId);
|
||||
return success(Map.of());
|
||||
}
|
||||
|
||||
Map<String, IotDevicePropertyDO> properties = devicePropertyService.getLatestDeviceProperties(deviceId);
|
||||
Map<String, Object> result = properties.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue()));
|
||||
|
||||
return success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("[getLatestProperties] 获取设备最新属性失败: deviceId={}", deviceId, e);
|
||||
return success(Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(PREFIX + "/batch-get")
|
||||
@Operation(summary = "批量获取多个设备的指定属性")
|
||||
public CommonResult<Map<Long, Map<String, Object>>> batchGetProperties(
|
||||
@RequestBody DevicePropertyBatchQueryReqDTO reqDTO) {
|
||||
try {
|
||||
Map<Long, Map<String, Object>> result = reqDTO.getDeviceIds().stream()
|
||||
.collect(Collectors.toMap(
|
||||
deviceId -> deviceId,
|
||||
deviceId -> {
|
||||
Map<String, IotDevicePropertyDO> properties = devicePropertyService.getLatestDeviceProperties(deviceId);
|
||||
|
||||
if (reqDTO.getIdentifiers() == null || reqDTO.getIdentifiers().isEmpty()) {
|
||||
// 返回所有属性
|
||||
return properties.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue()));
|
||||
} else {
|
||||
// 只返回指定的属性
|
||||
return reqDTO.getIdentifiers().stream()
|
||||
.filter(properties::containsKey)
|
||||
.collect(Collectors.toMap(
|
||||
identifier -> identifier,
|
||||
identifier -> properties.get(identifier).getValue()));
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
return success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("[batchGetProperties] 批量获取设备属性失败: deviceIds={}", reqDTO.getDeviceIds(), e);
|
||||
return success(Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@PostMapping(PREFIX + "/history")
|
||||
@Operation(summary = "查询设备属性历史数据")
|
||||
public CommonResult<List<DevicePropertyHistoryRespDTO>> getPropertyHistory(
|
||||
@RequestBody DevicePropertyHistoryQueryReqDTO reqDTO) {
|
||||
try {
|
||||
// 构建查询请求
|
||||
IotDevicePropertyHistoryListReqVO listReqVO = new IotDevicePropertyHistoryListReqVO();
|
||||
listReqVO.setDeviceId(reqDTO.getDeviceId());
|
||||
listReqVO.setIdentifier(reqDTO.getIdentifier());
|
||||
if (reqDTO.getStartTime() != null && reqDTO.getEndTime() != null) {
|
||||
listReqVO.setTimes(new LocalDateTime[]{reqDTO.getStartTime(), reqDTO.getEndTime()});
|
||||
}
|
||||
// listReqVO.setPageSize(reqDTO.getLimit()); // 暂不支持分页
|
||||
|
||||
List<IotDevicePropertyRespVO> historyList = devicePropertyService.getHistoryDevicePropertyList(listReqVO);
|
||||
|
||||
List<DevicePropertyHistoryRespDTO> result = historyList.stream()
|
||||
.map(vo -> DevicePropertyHistoryRespDTO.builder()
|
||||
.identifier(vo.getIdentifier())
|
||||
.value(vo.getValue())
|
||||
.timestamp(vo.getUpdateTime())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("[getPropertyHistory] 查询设备属性历史失败: deviceId={}, identifier={}",
|
||||
reqDTO.getDeviceId(), reqDTO.getIdentifier(), e);
|
||||
return success(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.viewsh.module.iot.api.device;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.dto.status.DeviceStatusRespDTO;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* IoT 设备状态查询 API 实现类
|
||||
* <p>
|
||||
* 提供 RPC 接口供其他模块查询设备状态
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@RestController
|
||||
@Validated
|
||||
@Primary
|
||||
@Slf4j
|
||||
public class IotDeviceStatusQueryApiImpl implements IotDeviceStatusQueryApi {
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/is-online")
|
||||
@Operation(summary = "检查设备是否在线")
|
||||
public CommonResult<Boolean> isDeviceOnline(@RequestParam("deviceId") Long deviceId) {
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
if (device == null) {
|
||||
return success(false);
|
||||
}
|
||||
return success(IotDeviceStateEnum.ONLINE.getState().equals(device.getState()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(PREFIX + "/get-status")
|
||||
@Operation(summary = "获取设备状态")
|
||||
public CommonResult<DeviceStatusRespDTO> getDeviceStatus(@RequestParam("deviceId") Long deviceId) {
|
||||
IotDeviceDO device = deviceService.getDeviceFromCache(deviceId);
|
||||
if (device == null) {
|
||||
return success(DeviceStatusRespDTO.builder()
|
||||
.deviceId(deviceId)
|
||||
.status(IotDeviceStateEnum.OFFLINE.getState())
|
||||
.build());
|
||||
}
|
||||
return success(DeviceStatusRespDTO.builder()
|
||||
.deviceId(device.getId())
|
||||
.deviceCode(device.getSerialNumber())
|
||||
.status(device.getState())
|
||||
.statusChangeTime(IotDeviceStateEnum.ONLINE.getState().equals(device.getState()) ?
|
||||
device.getOnlineTime() : device.getOfflineTime())
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,209 +1,274 @@
|
||||
package com.viewsh.module.iot.service.device.property;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||
import com.viewsh.framework.common.util.object.ObjectUtils;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
|
||||
import com.viewsh.module.iot.dal.redis.device.DevicePropertyRedisDAO;
|
||||
import com.viewsh.module.iot.dal.redis.device.DeviceReportTimeRedisDAO;
|
||||
import com.viewsh.module.iot.dal.redis.device.DeviceServerIdRedisDAO;
|
||||
import com.viewsh.module.iot.dal.tdengine.IotDevicePropertyMapper;
|
||||
import com.viewsh.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
|
||||
import com.viewsh.module.iot.enums.thingmodel.IotThingModelTypeEnum;
|
||||
import com.viewsh.module.iot.framework.tdengine.core.TDengineTableField;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import com.viewsh.module.iot.service.thingmodel.IotThingModelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static com.viewsh.framework.common.util.collection.CollectionUtils.*;
|
||||
|
||||
/**
|
||||
* IoT 设备【属性】数据 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
|
||||
|
||||
/**
|
||||
* 物模型的数据类型,与 TDengine 数据类型的映射关系
|
||||
*
|
||||
* @see <a href="https://docs.taosdata.com/reference/taos-sql/data-type/">TDEngine 数据类型</a>
|
||||
*/
|
||||
private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
|
||||
.put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT)
|
||||
.put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
|
||||
.put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
|
||||
.put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT)
|
||||
.put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT)
|
||||
.put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
|
||||
.put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.build();
|
||||
|
||||
@Resource
|
||||
private IotThingModelService thingModelService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotProductService productService;
|
||||
|
||||
@Resource
|
||||
private DevicePropertyRedisDAO deviceDataRedisDAO;
|
||||
@Resource
|
||||
private DeviceReportTimeRedisDAO deviceReportTimeRedisDAO;
|
||||
@Resource
|
||||
private DeviceServerIdRedisDAO deviceServerIdRedisDAO;
|
||||
|
||||
@Resource
|
||||
private IotDevicePropertyMapper devicePropertyMapper;
|
||||
|
||||
// ========== 设备属性相关操作 ==========
|
||||
|
||||
@Override
|
||||
public void defineDevicePropertyData(Long productId) {
|
||||
// 1.1 查询产品和物模型
|
||||
IotProductDO product = productService.validateProductExists(productId);
|
||||
List<IotThingModelDO> thingModels = filterList(thingModelService.getThingModelListByProductId(productId),
|
||||
thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
|
||||
// 1.2 解析 DB 里的字段
|
||||
List<TDengineTableField> oldFields = new ArrayList<>();
|
||||
try {
|
||||
oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getId()));
|
||||
} catch (Exception e) {
|
||||
if (!e.getMessage().contains("Table does not exist")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 情况一:如果是新增的时候,需要创建表
|
||||
List<TDengineTableField> newFields = buildTableFieldList(thingModels);
|
||||
if (CollUtil.isEmpty(oldFields)) {
|
||||
if (CollUtil.isEmpty(newFields)) {
|
||||
log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId);
|
||||
return;
|
||||
}
|
||||
devicePropertyMapper.createProductPropertySTable(product.getId(), newFields);
|
||||
return;
|
||||
}
|
||||
// 2.2 情况二:如果是修改的时候,需要更新表
|
||||
devicePropertyMapper.alterProductPropertySTable(product.getId(), oldFields, newFields);
|
||||
}
|
||||
|
||||
private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
|
||||
return convertList(thingModels, thingModel -> {
|
||||
TDengineTableField field = new TDengineTableField(
|
||||
StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写
|
||||
TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
|
||||
String dataType = thingModel.getProperty().getDataType();
|
||||
if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
|
||||
field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength());
|
||||
} else if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(),
|
||||
IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
|
||||
field.setLength(TDengineTableField.LENGTH_VARCHAR);
|
||||
}
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) {
|
||||
if (!(message.getParams() instanceof Map)) {
|
||||
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) -> {
|
||||
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(),
|
||||
IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
|
||||
// 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储
|
||||
properties.put((String) key, JsonUtils.toJsonString(value));
|
||||
} else {
|
||||
properties.put((String) key, value);
|
||||
}
|
||||
});
|
||||
if (CollUtil.isEmpty(properties)) {
|
||||
log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IotDevicePropertyDO> getLatestDeviceProperties(Long deviceId) {
|
||||
return deviceDataRedisDAO.get(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotDevicePropertyRespVO> getHistoryDevicePropertyList(IotDevicePropertyHistoryListReqVO listReqVO) {
|
||||
try {
|
||||
return devicePropertyMapper.selectListByHistory(listReqVO);
|
||||
} catch (Exception exception) {
|
||||
if (exception.getMessage().contains("Table does not exist")) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 设备时间相关操作 ==========
|
||||
|
||||
@Override
|
||||
public Set<Long> getDeviceIdListByReportTime(LocalDateTime maxReportTime) {
|
||||
return deviceReportTimeRedisDAO.range(maxReportTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void updateDeviceReportTimeAsync(Long id, LocalDateTime reportTime) {
|
||||
deviceReportTimeRedisDAO.update(id, reportTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceServerIdAsync(Long id, String serverId) {
|
||||
if (StrUtil.isEmpty(serverId)) {
|
||||
return;
|
||||
}
|
||||
deviceServerIdRedisDAO.update(id, serverId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDeviceServerId(Long id) {
|
||||
return deviceServerIdRedisDAO.get(id);
|
||||
}
|
||||
|
||||
package com.viewsh.module.iot.service.device.property;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||
import com.viewsh.framework.common.util.object.ObjectUtils;
|
||||
import com.viewsh.framework.mq.redis.core.RedisMQTemplate;
|
||||
import com.viewsh.module.iot.core.integration.event.DevicePropertyChangedEvent;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyHistoryListReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO;
|
||||
import com.viewsh.module.iot.core.integration.publisher.IntegrationEventPublisher;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDevicePropertyDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
|
||||
import com.viewsh.module.iot.dal.redis.device.DevicePropertyRedisDAO;
|
||||
import com.viewsh.module.iot.dal.redis.device.DeviceReportTimeRedisDAO;
|
||||
import com.viewsh.module.iot.dal.redis.device.DeviceServerIdRedisDAO;
|
||||
import com.viewsh.module.iot.dal.tdengine.IotDevicePropertyMapper;
|
||||
import com.viewsh.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
|
||||
import com.viewsh.module.iot.enums.thingmodel.IotThingModelTypeEnum;
|
||||
import com.viewsh.module.iot.framework.tdengine.core.TDengineTableField;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import com.viewsh.module.iot.service.thingmodel.IotThingModelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static com.viewsh.framework.common.util.collection.CollectionUtils.*;
|
||||
|
||||
/**
|
||||
* IoT 设备【属性】数据 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
|
||||
|
||||
/**
|
||||
* 物模型的数据类型,与 TDengine 数据类型的映射关系
|
||||
*
|
||||
* @see <a href="https://docs.taosdata.com/reference/taos-sql/data-type/">TDEngine 数据类型</a>
|
||||
*/
|
||||
private static final Map<String, String> TYPE_MAPPING = MapUtil.<String, String>builder()
|
||||
.put(IotDataSpecsDataTypeEnum.INT.getDataType(), TDengineTableField.TYPE_INT)
|
||||
.put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
|
||||
.put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
|
||||
.put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT)
|
||||
.put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT)
|
||||
.put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
|
||||
.put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_VARCHAR)
|
||||
.build();
|
||||
|
||||
@Resource
|
||||
private IotThingModelService thingModelService;
|
||||
@Resource
|
||||
@Lazy // 延迟加载,解决循环依赖
|
||||
private IotProductService productService;
|
||||
|
||||
@Resource
|
||||
private DevicePropertyRedisDAO deviceDataRedisDAO;
|
||||
@Resource
|
||||
private DeviceReportTimeRedisDAO deviceReportTimeRedisDAO;
|
||||
@Resource
|
||||
private DeviceServerIdRedisDAO deviceServerIdRedisDAO;
|
||||
|
||||
@Resource
|
||||
private IotDevicePropertyMapper devicePropertyMapper;
|
||||
|
||||
@Resource
|
||||
private RedisMQTemplate redisMQTemplate;
|
||||
|
||||
@Resource
|
||||
private IntegrationEventPublisher integrationEventPublisher;
|
||||
|
||||
// ========== 设备属性相关操作 ==========
|
||||
|
||||
@Override
|
||||
public void defineDevicePropertyData(Long productId) {
|
||||
// 1.1 查询产品和物模型
|
||||
IotProductDO product = productService.validateProductExists(productId);
|
||||
List<IotThingModelDO> thingModels = filterList(thingModelService.getThingModelListByProductId(productId),
|
||||
thingModel -> IotThingModelTypeEnum.PROPERTY.getType().equals(thingModel.getType()));
|
||||
// 1.2 解析 DB 里的字段
|
||||
List<TDengineTableField> oldFields = new ArrayList<>();
|
||||
try {
|
||||
oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getId()));
|
||||
} catch (Exception e) {
|
||||
if (!e.getMessage().contains("Table does not exist")) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 情况一:如果是新增的时候,需要创建表
|
||||
List<TDengineTableField> newFields = buildTableFieldList(thingModels);
|
||||
if (CollUtil.isEmpty(oldFields)) {
|
||||
if (CollUtil.isEmpty(newFields)) {
|
||||
log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId);
|
||||
return;
|
||||
}
|
||||
devicePropertyMapper.createProductPropertySTable(product.getId(), newFields);
|
||||
return;
|
||||
}
|
||||
// 2.2 情况二:如果是修改的时候,需要更新表
|
||||
devicePropertyMapper.alterProductPropertySTable(product.getId(), oldFields, newFields);
|
||||
}
|
||||
|
||||
private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
|
||||
return convertList(thingModels, thingModel -> {
|
||||
TDengineTableField field = new TDengineTableField(
|
||||
StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写
|
||||
TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
|
||||
String dataType = thingModel.getProperty().getDataType();
|
||||
if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
|
||||
field.setLength(((ThingModelDateOrTextDataSpecs) thingModel.getProperty().getDataSpecs()).getLength());
|
||||
} else if (ObjectUtils.equalsAny(dataType, IotDataSpecsDataTypeEnum.STRUCT.getDataType(),
|
||||
IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
|
||||
field.setLength(TDengineTableField.LENGTH_VARCHAR);
|
||||
}
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) {
|
||||
if (!(message.getParams() instanceof Map)) {
|
||||
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) -> {
|
||||
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(),
|
||||
IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
|
||||
// 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储
|
||||
properties.put((String) key, JsonUtils.toJsonString(value));
|
||||
} else {
|
||||
properties.put((String) key, value);
|
||||
}
|
||||
});
|
||||
if (CollUtil.isEmpty(properties)) {
|
||||
log.error("[saveDeviceProperty][消息({}) 没有合法的属性]", message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 发布属性消息到 Redis Stream(供其他模块如 Ops 订阅)
|
||||
publishPropertyMessage(device, properties, message.getReportTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布设备属性消息
|
||||
* <p>
|
||||
* 同时发布到 Redis Stream(旧方式,保持兼容)和 RocketMQ IntegrationEventBus(新方式)
|
||||
*
|
||||
* @param device 设备信息
|
||||
* @param properties 属性数据
|
||||
* @param reportTime 上报时间
|
||||
*/
|
||||
private void publishPropertyMessage(IotDeviceDO device, Map<String, Object> properties, LocalDateTime reportTime) {
|
||||
try {
|
||||
// 获取产品信息
|
||||
IotProductDO product = productService.getProductFromCache(device.getProductId());
|
||||
String productKey = product != null ? product.getProductKey() : "unknown";
|
||||
|
||||
// 发布到 IntegrationEventBus(RocketMQ)
|
||||
publishToIntegrationEventBus(device, productKey, properties, reportTime);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[publishPropertyMessage] 属性消息发布失败: deviceId={}", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布跨模块属性变更事件到 RocketMQ
|
||||
*
|
||||
* @param device 设备信息
|
||||
* @param productKey 产品标识符(用作 RocketMQ Tag)
|
||||
* @param properties 属性数据
|
||||
* @param reportTime 上报时间
|
||||
*/
|
||||
private void publishToIntegrationEventBus(IotDeviceDO device, String productKey,
|
||||
Map<String, Object> properties, LocalDateTime reportTime) {
|
||||
try {
|
||||
DevicePropertyChangedEvent event = DevicePropertyChangedEvent.builder()
|
||||
.deviceId(device.getId())
|
||||
.deviceName(device.getDeviceName())
|
||||
.productId(device.getProductId())
|
||||
.productKey(productKey)
|
||||
.tenantId(device.getTenantId())
|
||||
.properties(properties)
|
||||
.changedIdentifiers(properties.keySet())
|
||||
.eventTime(reportTime)
|
||||
.build();
|
||||
|
||||
integrationEventPublisher.publishPropertyChanged(event);
|
||||
log.debug("[publishToIntegrationEventBus] 跨模块属性变更事件已发布: eventId={}, deviceId={}, productKey={}, properties={}",
|
||||
event.getEventId(), device.getId(), productKey, properties.keySet());
|
||||
} catch (Exception e) {
|
||||
log.error("[publishToIntegrationEventBus] 跨模块属性变更事件发布失败: deviceId={}", device.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, IotDevicePropertyDO> getLatestDeviceProperties(Long deviceId) {
|
||||
return deviceDataRedisDAO.get(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotDevicePropertyRespVO> getHistoryDevicePropertyList(IotDevicePropertyHistoryListReqVO listReqVO) {
|
||||
try {
|
||||
return devicePropertyMapper.selectListByHistory(listReqVO);
|
||||
} catch (Exception exception) {
|
||||
if (exception.getMessage().contains("Table does not exist")) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 设备时间相关操作 ==========
|
||||
|
||||
@Override
|
||||
public Set<Long> getDeviceIdListByReportTime(LocalDateTime maxReportTime) {
|
||||
return deviceReportTimeRedisDAO.range(maxReportTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void updateDeviceReportTimeAsync(Long id, LocalDateTime reportTime) {
|
||||
deviceReportTimeRedisDAO.update(id, reportTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDeviceServerIdAsync(Long id, String serverId) {
|
||||
if (StrUtil.isEmpty(serverId)) {
|
||||
return;
|
||||
}
|
||||
deviceServerIdRedisDAO.update(id, serverId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDeviceServerId(Long id) {
|
||||
return deviceServerIdRedisDAO.get(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,157 +1,163 @@
|
||||
spring:
|
||||
application:
|
||||
name: iot-server
|
||||
|
||||
profiles:
|
||||
active: local
|
||||
|
||||
main:
|
||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
|
||||
|
||||
config:
|
||||
import:
|
||||
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
|
||||
|
||||
# Servlet 配置
|
||||
servlet:
|
||||
# 文件上传相关配置项
|
||||
multipart:
|
||||
max-file-size: 16MB # 单个文件大小
|
||||
max-request-size: 32MB # 设置总上传的文件大小
|
||||
|
||||
# Jackson 配置项
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
|
||||
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
|
||||
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
|
||||
fail-on-empty-beans: false # 允许序列化无属性的 Bean
|
||||
|
||||
# Cache 配置项
|
||||
cache:
|
||||
type: REDIS
|
||||
redis:
|
||||
time-to-live: 1h # 设置过期时间为 1 小时
|
||||
|
||||
server:
|
||||
port: 48091
|
||||
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
|
||||
--- #################### 接口文档配置 ####################
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true # 1. 是否开启 Swagger 接文档的元数据
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
|
||||
path: /swagger-ui
|
||||
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
|
||||
|
||||
knife4j:
|
||||
enable: true
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
# MyBatis Plus 的配置项
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
|
||||
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
|
||||
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
|
||||
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
type-aliases-package: ${viewsh.info.base-package}.dal.dataobject
|
||||
encryptor:
|
||||
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
|
||||
|
||||
mybatis-plus-join:
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
|
||||
# Spring Data Redis 配置
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
repositories:
|
||||
enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度
|
||||
|
||||
# VO 转换(数据翻译)相关
|
||||
easy-trans:
|
||||
is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
|
||||
|
||||
--- #################### RPC 远程调用相关配置 ####################
|
||||
|
||||
--- #################### 消息队列相关 ####################
|
||||
|
||||
# rocketmq 配置项,对应 RocketMQProperties 配置类
|
||||
rocketmq:
|
||||
# Producer 配置项
|
||||
producer:
|
||||
group: ${spring.application.name}_PRODUCER # 生产者分组
|
||||
|
||||
spring:
|
||||
# Kafka 配置项,对应 KafkaProperties 配置类
|
||||
kafka:
|
||||
# Kafka Producer 配置项
|
||||
producer:
|
||||
acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
|
||||
retries: 3 # 发送失败时,重试发送的次数
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
|
||||
# Kafka Consumer 配置项
|
||||
consumer:
|
||||
auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring.json.trusted.packages: '*'
|
||||
# Kafka Consumer Listener 监听器配置
|
||||
listener:
|
||||
missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
xxl:
|
||||
job:
|
||||
executor:
|
||||
appname: ${spring.application.name} # 执行器 AppName
|
||||
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
|
||||
accessToken: default_token # 执行器通讯TOKEN
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
viewsh:
|
||||
info:
|
||||
version: 1.0.0
|
||||
base-package: com.viewsh.module.iot
|
||||
web:
|
||||
admin-ui:
|
||||
url: http://dashboard.viewsh.iocoder.cn # Admin 管理后台 UI 的地址
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
swagger:
|
||||
title: 管理后台
|
||||
description: 提供管理员管理的所有功能
|
||||
version: ${viewsh.info.version}
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
ignore-urls:
|
||||
ignore-tables:
|
||||
ignore-caches:
|
||||
- iot:device
|
||||
- iot:thing_model_list
|
||||
iot:
|
||||
message-bus:
|
||||
type: redis # 消息总线的类型
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: iot-server
|
||||
|
||||
profiles:
|
||||
active: local
|
||||
|
||||
main:
|
||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
|
||||
|
||||
config:
|
||||
import:
|
||||
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
|
||||
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
|
||||
|
||||
# Servlet 配置
|
||||
servlet:
|
||||
# 文件上传相关配置项
|
||||
multipart:
|
||||
max-file-size: 16MB # 单个文件大小
|
||||
max-request-size: 32MB # 设置总上传的文件大小
|
||||
|
||||
# Jackson 配置项
|
||||
jackson:
|
||||
serialization:
|
||||
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
|
||||
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
|
||||
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
|
||||
fail-on-empty-beans: false # 允许序列化无属性的 Bean
|
||||
|
||||
# Cache 配置项
|
||||
cache:
|
||||
type: REDIS
|
||||
redis:
|
||||
time-to-live: 1h # 设置过期时间为 1 小时
|
||||
|
||||
server:
|
||||
port: 48091
|
||||
|
||||
logging:
|
||||
file:
|
||||
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
|
||||
|
||||
--- #################### 接口文档配置 ####################
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true # 1. 是否开启 Swagger 接文档的元数据
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
|
||||
path: /swagger-ui
|
||||
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
|
||||
|
||||
knife4j:
|
||||
enable: true
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
# MyBatis Plus 的配置项
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
|
||||
# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
|
||||
# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
|
||||
# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
|
||||
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
|
||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
type-aliases-package: ${viewsh.info.base-package}.dal.dataobject
|
||||
encryptor:
|
||||
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
|
||||
|
||||
mybatis-plus-join:
|
||||
banner: false # 关闭控制台的 Banner 打印
|
||||
|
||||
# Spring Data Redis 配置
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
repositories:
|
||||
enabled: false # 项目未使用到 Spring Data Redis 的 Repository,所以直接禁用,保证启动速度
|
||||
|
||||
# VO 转换(数据翻译)相关
|
||||
easy-trans:
|
||||
is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
|
||||
|
||||
--- #################### RPC 远程调用相关配置 ####################
|
||||
|
||||
--- #################### 消息队列相关 ####################
|
||||
|
||||
# rocketmq 配置项,对应 RocketMQProperties 配置类
|
||||
rocketmq:
|
||||
# Producer 配置项
|
||||
producer:
|
||||
group: ${spring.application.name}_PRODUCER # 生产者分组
|
||||
|
||||
spring:
|
||||
# Kafka 配置项,对应 KafkaProperties 配置类
|
||||
kafka:
|
||||
# Kafka Producer 配置项
|
||||
producer:
|
||||
acks: 1 # 0-不应答。1-leader 应答。all-所有 leader 和 follower 应答。
|
||||
retries: 3 # 发送失败时,重试发送的次数
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer # 消息的 value 的序列化
|
||||
# Kafka Consumer 配置项
|
||||
consumer:
|
||||
auto-offset-reset: earliest # 设置消费者分组最初的消费进度为 earliest 。可参考博客 https://blog.csdn.net/lishuangzhe7047/article/details/74530417 理解
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
spring.json.trusted.packages: '*'
|
||||
# Kafka Consumer Listener 监听器配置
|
||||
listener:
|
||||
missing-topics-fatal: false # 消费监听接口监听的主题不存在时,默认会报错。所以通过设置为 false ,解决报错
|
||||
|
||||
--- #################### 定时任务相关配置 ####################
|
||||
|
||||
xxl:
|
||||
job:
|
||||
executor:
|
||||
appname: ${spring.application.name} # 执行器 AppName
|
||||
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
|
||||
accessToken: default_token # 执行器通讯TOKEN
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
viewsh:
|
||||
info:
|
||||
version: 1.0.0
|
||||
base-package: com.viewsh.module.iot
|
||||
web:
|
||||
admin-ui:
|
||||
url: http://dashboard.viewsh.iocoder.cn # Admin 管理后台 UI 的地址
|
||||
xss:
|
||||
enable: false
|
||||
exclude-urls: # 如下 url,仅仅是为了演示,去掉配置也没关系
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
swagger:
|
||||
title: 管理后台
|
||||
description: 提供管理员管理的所有功能
|
||||
version: ${viewsh.info.version}
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
ignore-urls:
|
||||
ignore-tables:
|
||||
ignore-caches:
|
||||
- iot:device
|
||||
- iot:thing_model_list
|
||||
iot:
|
||||
message-bus:
|
||||
type: redis # 消息总线的类型
|
||||
# 跨模块事件总线配置(IntegrationEventBus)
|
||||
integration:
|
||||
mq:
|
||||
enabled: true # 是否启用跨模块事件发布
|
||||
producer-group: integration-event-producer # 生产者组名
|
||||
send-timeout-ms: 3000 # 发送超时时间(毫秒)
|
||||
|
||||
debug: false
|
||||
Reference in New Issue
Block a user