feat:【IoT 物联网】完整实现“数据流转”功能(后台管理)

This commit is contained in:
YunaiV
2025-06-25 21:45:02 +08:00
parent 956418d31f
commit 2a04bdc3fe
18 changed files with 216 additions and 25 deletions

View File

@@ -58,7 +58,8 @@ public interface ErrorCodeConstants {
ErrorCode DATA_RULE_NOT_EXISTS = new ErrorCode(1_050_010_000, "数据流转规则不存在");
// ========== IoT 数据流转目的 1-050-011-000 ==========
ErrorCode DATA_BRIDGE_NOT_EXISTS = new ErrorCode(1_050_011_000, "数据桥梁不存在");
ErrorCode DATA_SINK_NOT_EXISTS = new ErrorCode(1_050_011_000, "数据桥梁不存在");
ErrorCode DATA_SINK_DELETE_FAIL_USED_BY_RULE = new ErrorCode(1_050_011_001, "数据流转目的正在被数据流转规则使用,无法删除");
// ========== IoT 场景联动 1-050-012-000 ==========
ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_012_000, "场景联动不存在");

View File

@@ -9,7 +9,6 @@ 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.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -38,8 +37,6 @@ public class IotDeviceController {
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageService deviceMessageService;
@PostMapping("/create")
@Operation(summary = "创建设备")
@@ -125,11 +122,12 @@ public class IotDeviceController {
@GetMapping("/simple-list")
@Operation(summary = "获取设备的精简信息列表", description = "主要用于前端的下拉选项")
@Parameter(name = "deviceType", description = "设备类型", example = "1")
public CommonResult<List<IotDeviceRespVO>> getSimpleDeviceList(
public CommonResult<List<IotDeviceRespVO>> getDeviceSimpleList(
@RequestParam(value = "deviceType", required = false) Integer deviceType) {
List<IotDeviceDO> list = deviceService.getDeviceListByDeviceType(deviceType);
return success(convertList(list, device -> // 只返回 id、name 字段
new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName())));
return success(convertList(list, device -> // 只返回 id、name、productId 字段
new IotDeviceRespVO().setId(device.getId()).setDeviceName(device.getDeviceName())
.setProductId(device.getProductId())));
}
@PostMapping("/import")

View File

@@ -143,7 +143,7 @@ public class IotProductController {
@GetMapping("/simple-list")
@Operation(summary = "获取产品的精简信息列表", description = "主要用于前端的下拉选项")
public CommonResult<List<IotProductRespVO>> getSimpleProductList() {
public CommonResult<List<IotProductRespVO>> getProductSimpleList() {
List<IotProductDO> list = productService.getProductList();
return success(convertList(list, product -> // 只返回 id、name 字段
new IotProductRespVO().setId(product.getId()).setName(product.getName())

View File

@@ -13,10 +13,10 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
@Data
public class IotDataRulePageReqVO extends PageParam {
@Schema(description = "场景名称", example = "芋艿")
@Schema(description = "数据流转规则名称", example = "芋艿")
private String name;
@Schema(description = "场景状态", example = "1")
@Schema(description = "数据流转规则状态", example = "1")
private Integer status;
@Schema(description = "创建时间")

View File

@@ -11,16 +11,16 @@ import java.util.List;
@Data
public class IotDataRuleRespVO {
@Schema(description = "场景编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8540")
@Schema(description = "数据流转规则编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8540")
private Long id;
@Schema(description = "场景名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
@Schema(description = "数据流转规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String name;
@Schema(description = "场景描述", example = "你猜")
@Schema(description = "数据流转规则描述", example = "你猜")
private String description;
@Schema(description = "场景状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@Schema(description = "数据流转规则状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "数据源配置数组", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@@ -14,18 +14,18 @@ import java.util.List;
@Data
public class IotDataRuleSaveReqVO {
@Schema(description = "场景编号", example = "8540")
@Schema(description = "数据流转规则编号", example = "8540")
private Long id;
@Schema(description = "场景名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
@NotEmpty(message = "场景名称不能为空")
@Schema(description = "数据流转规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
@NotEmpty(message = "数据流转规则名称不能为空")
private String name;
@Schema(description = "场景描述", example = "你猜")
@Schema(description = "数据流转规则描述", example = "你猜")
private String description;
@Schema(description = "场景状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "场景状态不能为空")
@Schema(description = "数据流转规则状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "数据流转规则状态不能为空")
@InEnum(CommonStatusEnum.class)
private Integer status;

View File

@@ -6,9 +6,9 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDevicePageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import jakarta.annotation.Nullable;
import org.apache.ibatis.annotations.Mapper;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@@ -50,8 +50,9 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
return selectCount(IotDeviceDO::getProductId, productId);
}
default List<IotDeviceDO> selectListByDeviceType(Integer deviceType) {
return selectList(IotDeviceDO::getDeviceType, deviceType);
default List<IotDeviceDO> selectListByDeviceType(@Nullable Integer deviceType) {
return selectList(new LambdaQueryWrapperX<IotDeviceDO>()
.geIfPresent(IotDeviceDO::getDeviceType, deviceType));
}
default List<IotDeviceDO> selectListByState(Integer state) {

View File

@@ -3,10 +3,13 @@ package cn.iocoder.yudao.module.iot.dal.mysql.rule;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* IoT 数据流转规则 Mapper
*
@@ -23,4 +26,9 @@ public interface IotDataRuleMapper extends BaseMapperX<IotDataRuleDO> {
.orderByDesc(IotDataRuleDO::getId));
}
default List<IotDataRuleDO> selectListBySinkId(Long sinkId) {
return selectList(new LambdaQueryWrapperX<IotDataRuleDO>()
.apply(MyBatisUtils.findInSet("sink_ids", sinkId)));
}
}

View File

@@ -13,6 +13,7 @@ import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* IoT 设备 Service 接口
@@ -255,4 +256,11 @@ public interface IotDeviceService {
*/
boolean authDevice(@Valid IotDeviceAuthReqDTO authReqDTO);
/**
* 校验设备是否存在
*
* @param ids 设备编号数组
*/
void validateDevicesExist(Set<Long> ids);
}

View File

@@ -483,6 +483,17 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return true;
}
@Override
public void validateDevicesExist(Set<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
List<IotDeviceDO> deviceIds = deviceMapper.selectByIds(ids);
if (deviceIds.size() != ids.size()) {
throw exception(DEVICE_NOT_EXISTS);
}
}
private IotDeviceServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}

View File

@@ -8,6 +8,7 @@ import jakarta.validation.Valid;
import javax.annotation.Nullable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
/**
@@ -112,5 +113,11 @@ public interface IotProductService {
*/
Long getProductCount(@Nullable LocalDateTime createTime);
/**
* 批量校验产品存在
*
* @param ids 产品编号集合
*/
void validateProductsExist(Collection<Long> ids);
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.service.product;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
@@ -20,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -157,4 +159,15 @@ public class IotProductServiceImpl implements IotProductService {
return productMapper.selectCountByCreateTime(createTime);
}
@Override
public void validateProductsExist(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
List<IotProductDO> products = productMapper.selectByIds(ids);
if (products.size() != ids.size()) {
throw exception(PRODUCT_NOT_EXISTS);
}
}
}

View File

@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.iot.service.rule.data;
import java.util.List;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleSaveReqVO;
@@ -51,4 +53,12 @@ public interface IotDataRuleService {
*/
PageResult<IotDataRuleDO> getDataRulePage(IotDataRulePageReqVO pageReqVO);
/**
* 根据数据目的编号,获得数据流转规则列表
*
* @param sinkId 数据目的编号
* @return 是否被使用
*/
List<IotDataRuleDO> getDataRuleBySinkId(Long sinkId);
}

View File

@@ -1,16 +1,26 @@
package cn.iocoder.yudao.module.iot.service.rule.data;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRulePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.rule.IotDataRuleSaveReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataRuleDO;
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataRuleMapper;
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;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_RULE_NOT_EXISTS;
/**
@@ -25,8 +35,20 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
@Resource
private IotDataRuleMapper dataRuleMapper;
@Resource
private IotProductService productService;
@Resource
private IotDeviceService deviceService;
@Resource
private IotThingModelService thingModelService;
@Resource
private IotDataSinkService dataSinkService;
@Override
public Long createDataRule(IotDataRuleSaveReqVO createReqVO) {
// 校验数据源配置和数据目的
validateDataRuleConfig(createReqVO);
// 新增
IotDataRuleDO dataRule = BeanUtils.toBean(createReqVO, IotDataRuleDO.class);
dataRuleMapper.insert(dataRule);
return dataRule.getId();
@@ -36,6 +58,9 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
public void updateDataRule(IotDataRuleSaveReqVO updateReqVO) {
// 校验存在
validateDataRuleExists(updateReqVO.getId());
// 校验数据源配置和数据目的
validateDataRuleConfig(updateReqVO);
// 更新
IotDataRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotDataRuleDO.class);
dataRuleMapper.updateById(updateObj);
@@ -55,6 +80,55 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
}
}
/**
* 校验数据流转规则配置
*
* @param reqVO 数据流转规则保存请求VO
*/
private void validateDataRuleConfig(IotDataRuleSaveReqVO reqVO) {
// 1. 校验数据源配置
validateSourceConfigs(reqVO.getSourceConfigs());
// 2. 校验数据目的
dataSinkService.validateDataSinksExist(reqVO.getSinkIds());
}
/**
* 校验数据源配置
*
* @param sourceConfigs 数据源配置列表
*/
private void validateSourceConfigs(List<IotDataRuleDO.SourceConfig> sourceConfigs) {
// 1. 校验产品
productService.validateProductsExist(
convertSet(sourceConfigs, IotDataRuleDO.SourceConfig::getProductId));
// 2. 校验设备
deviceService.validateDevicesExist(convertSet(sourceConfigs, IotDataRuleDO.SourceConfig::getDeviceId,
config -> ObjUtil.notEqual(config.getDeviceId(), IotDeviceDO.DEVICE_ID_ALL)));
// 3. 校验物模型存在
validateThingModelsExist(sourceConfigs);
}
/**
* 校验物模型存在
*
* @param sourceConfigs 数据源配置列表
*/
private void validateThingModelsExist(List<IotDataRuleDO.SourceConfig> sourceConfigs) {
Map<Long, Set<String>> productIdToIdentifiers = new HashMap<>();
for (IotDataRuleDO.SourceConfig config : sourceConfigs) {
if (StrUtil.isEmpty(config.getIdentifier())) {
continue;
}
productIdToIdentifiers.computeIfAbsent(config.getProductId(),
productId -> new HashSet<>()).add(config.getIdentifier());
}
for (Map.Entry<Long, Set<String>> entry : productIdToIdentifiers.entrySet()) {
thingModelService.validateThingModelsExist(entry.getKey(), entry.getValue());
}
}
@Override
public IotDataRuleDO getDataRule(Long id) {
return dataRuleMapper.selectById(id);
@@ -65,4 +139,9 @@ public class IotDataRuleServiceImpl implements IotDataRuleService {
return dataRuleMapper.selectPage(pageReqVO);
}
@Override
public List<IotDataRuleDO> getDataRuleBySinkId(Long sinkId) {
return dataRuleMapper.selectListBySinkId(sinkId);
}
}

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSin
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
@@ -61,4 +62,11 @@ public interface IotDataSinkService {
*/
List<IotDataSinkDO> getDataSinkListByStatus(Integer status);
/**
* 批量校验数据目的存在
*
* @param ids 数据目的编号集合
*/
void validateDataSinksExist(Collection<Long> ids);
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.service.rule.data;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSinkPageReqVO;
@@ -7,13 +8,16 @@ import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.data.sink.IotDataSin
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotDataSinkDO;
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotDataSinkMapper;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_BRIDGE_NOT_EXISTS;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_NOT_EXISTS;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DATA_SINK_DELETE_FAIL_USED_BY_RULE;
/**
* IoT 数据流转目的 Service 实现类
@@ -27,6 +31,10 @@ public class IotDataSinkServiceImpl implements IotDataSinkService {
@Resource
private IotDataSinkMapper dataSinkMapper;
@Resource
@Lazy // 延迟,避免循环依赖报错
private IotDataRuleService dataRuleService;
@Override
public Long createDataSink(IotDataSinkSaveReqVO createReqVO) {
IotDataSinkDO dataBridge = BeanUtils.toBean(createReqVO, IotDataSinkDO.class);
@@ -47,13 +55,17 @@ public class IotDataSinkServiceImpl implements IotDataSinkService {
public void deleteDataSink(Long id) {
// 校验存在
validateDataBridgeExists(id);
// 校验是否被数据流转规则使用
if (CollUtil.isNotEmpty(dataRuleService.getDataRuleBySinkId(id))) {
throw exception(DATA_SINK_DELETE_FAIL_USED_BY_RULE);
}
// 删除
dataSinkMapper.deleteById(id);
}
private void validateDataBridgeExists(Long id) {
if (dataSinkMapper.selectById(id) == null) {
throw exception(DATA_BRIDGE_NOT_EXISTS);
throw exception(DATA_SINK_NOT_EXISTS);
}
}
@@ -72,4 +84,15 @@ public class IotDataSinkServiceImpl implements IotDataSinkService {
return dataSinkMapper.selectListByStatus(status);
}
@Override
public void validateDataSinksExist(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
List<IotDataSinkDO> sinks = dataSinkMapper.selectByIds(ids);
if (sinks.size() != ids.size()) {
throw exception(DATA_SINK_NOT_EXISTS);
}
}
}

View File

@@ -9,6 +9,7 @@ import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* IoT 产品物模型 Service 接口
@@ -99,4 +100,12 @@ public interface IotThingModelService {
*/
List<IotThingModelDO> getThingModelList(IotThingModelListReqVO reqVO);
/**
* 批量校验物模型存在
*
* @param productId 产品编号
* @param identifiers 标识符集合
*/
void validateThingModelsExist(Long productId, Set<String> identifiers);
}

View File

@@ -158,6 +158,21 @@ public class IotThingModelServiceImpl implements IotThingModelService {
return thingModelMapper.selectList(reqVO);
}
@Override
public void validateThingModelsExist(Long productId, Set<String> identifiers) {
if (CollUtil.isEmpty(identifiers)) {
return;
}
List<IotThingModelDO> thingModels = thingModelMapper.selectListByProductIdAndIdentifiers(
productId, identifiers);
Set<String> foundIdentifiers = convertSet(thingModels, IotThingModelDO::getIdentifier);
for (String identifier : identifiers) {
if (!foundIdentifiers.contains(identifier)) {
throw exception(THING_MODEL_NOT_EXISTS);
}
}
}
/**
* 校验功能是否存在
*