feat(iot):【网关设备:20%】增加网关设备绑定能力(未完成),基于 breezy-doodling-starlight.md 规划

This commit is contained in:
YunaiV
2026-01-22 00:52:09 +08:00
parent 45638b35f4
commit 4b1dfad063
7 changed files with 265 additions and 1 deletions

View File

@@ -51,6 +51,7 @@ public class IotDeviceController {
return success(deviceService.createDevice(createReqVO)); return success(deviceService.createDevice(createReqVO));
} }
@PutMapping("/update") @PutMapping("/update")
@Operation(summary = "更新设备") @Operation(summary = "更新设备")
@PreAuthorize("@ss.hasPermission('iot:device:update')") @PreAuthorize("@ss.hasPermission('iot:device:update')")
@@ -59,7 +60,72 @@ public class IotDeviceController {
return success(true); return success(true);
} }
// TODO @芋艿参考阿里云1绑定网关2解绑网关 @PutMapping("/bind-gateway")
@Operation(summary = "绑定子设备到网关")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult<Boolean> bindDeviceGateway(@Valid @RequestBody IotDeviceBindGatewayReqVO reqVO) {
deviceService.bindDeviceGateway(reqVO.getIds(), reqVO.getGatewayId());
return success(true);
}
@PutMapping("/unbind-gateway")
@Operation(summary = "解绑子设备与网关")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult<Boolean> unbindDeviceGateway(@Valid @RequestBody IotDeviceUnbindGatewayReqVO reqVO) {
deviceService.unbindDeviceGateway(reqVO.getIds());
return success(true);
}
@GetMapping("/sub-device-list")
@Operation(summary = "获取网关的子设备列表")
@Parameter(name = "gatewayId", description = "网关设备编号", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<List<IotDeviceRespVO>> getSubDeviceList(@RequestParam("gatewayId") Long gatewayId) {
List<IotDeviceDO> list = deviceService.getDeviceListByGatewayId(gatewayId);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 补充产品名称
Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId);
return success(convertList(list, device -> {
IotDeviceRespVO respVO = BeanUtils.toBean(device, IotDeviceRespVO.class);
MapUtils.findAndThen(productMap, device.getProductId(),
product -> respVO.setProductName(product.getName()));
return respVO;
}));
}
// TODO @AI希望改成“未绑定的”。需要剔除已经绑定包括自己的
// TODO @AI不需要传递 gatewayId
// TODO @AI需要分页
@GetMapping("/bindable-sub-device-list")
@Operation(summary = "获取可绑定到网关的子设备列表")
@Parameter(name = "gatewayId", description = "网关设备编号(可选)", example = "1")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<List<IotDeviceRespVO>> getBindableSubDeviceList(
@RequestParam(value = "gatewayId", required = false) Long gatewayId) {
List<IotDeviceDO> list = deviceService.getBindableSubDeviceList(gatewayId);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 补充产品名称
Map<Long, IotProductDO> productMap = convertMap(productService.getProductList(), IotProductDO::getId);
return success(convertList(list, device -> {
// TODO @AI可以 beanutils 转换么?
IotDeviceRespVO respVO = new IotDeviceRespVO()
.setId(device.getId())
.setDeviceName(device.getDeviceName())
.setNickname(device.getNickname())
.setProductId(device.getProductId())
.setState(device.getState())
.setGatewayId(device.getGatewayId());
MapUtils.findAndThen(productMap, device.getProductId(),
product -> respVO.setProductName(product.getName()));
return respVO;
}));
}
@PutMapping("/update-group") @PutMapping("/update-group")
@Operation(summary = "更新设备分组") @Operation(summary = "更新设备分组")

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Set;
@Schema(description = "管理后台 - IoT 设备绑定网关 Request VO")
@Data
public class IotDeviceBindGatewayReqVO {
@Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
@NotEmpty(message = "子设备编号列表不能为空")
private Set<Long> ids;
@Schema(description = "网关设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "网关设备编号不能为空")
private Long gatewayId;
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Set;
@Schema(description = "管理后台 - IoT 设备解绑网关 Request VO")
@Data
public class IotDeviceUnbindGatewayReqVO {
@Schema(description = "子设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
@NotEmpty(message = "子设备编号列表不能为空")
private Set<Long> ids;
}

View File

@@ -129,4 +129,33 @@ public interface IotDeviceMapper extends BaseMapperX<IotDeviceDO> {
.isNotNull(IotDeviceDO::getLongitude)); .isNotNull(IotDeviceDO::getLongitude));
} }
// ========== 网关-子设备绑定相关 ==========
/**
* 根据网关编号查询子设备列表
*
* @param gatewayId 网关设备编号
* @return 子设备列表
*/
default List<IotDeviceDO> selectListByGatewayId(Long gatewayId) {
return selectList(IotDeviceDO::getGatewayId, gatewayId);
}
/**
* 查询可绑定到网关的子设备列表
* <p>
* 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关,或已绑定到指定网关
*
* @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备)
* @return 子设备列表
*/
default List<IotDeviceDO> selectBindableSubDeviceList(@Nullable Long gatewayId) {
return selectList(new LambdaQueryWrapperX<IotDeviceDO>()
.eq(IotDeviceDO::getDeviceType, cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum.GATEWAY_SUB.getType())
.and(wrapper -> wrapper
.isNull(IotDeviceDO::getGatewayId)));
// .or()
// .eqIfPresent(IotDeviceDO::getGatewayId, gatewayId)))
}
} }

View File

@@ -33,6 +33,12 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!"); ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关"); ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
// TODO @AI1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下
ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备不是网关子设备类型,无法绑定到网关");
// TODO @AI1_050_003_009 需要提示具体的哪个设备。产品/设备,标识下
ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备已绑定到其他网关,请先解绑");
// TODO @AI是不是可以删除DEVICE_GATEWAY_BINDTO_NOT_EXISTS
ErrorCode DEVICE_GATEWAY_BINDTO_NOT_EXISTS = new ErrorCode(1_050_003_011, "设备未绑定到任何网关");
// ========== 产品分类 1-050-004-000 ========== // ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");

View File

@@ -73,6 +73,7 @@ public interface IotDeviceService {
*/ */
void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO); void updateDeviceGroup(@Valid IotDeviceUpdateGroupReqVO updateReqVO);
// TODO @AI网关设备被删除时需要查看是否有子设备绑定。如果有则不允许删除。
/** /**
* 删除单个设备 * 删除单个设备
* *
@@ -288,4 +289,48 @@ public interface IotDeviceService {
*/ */
List<IotDeviceDO> getDeviceListByHasLocation(); List<IotDeviceDO> getDeviceListByHasLocation();
// ========== 网关-子设备绑定相关 ==========
/**
* 绑定子设备到网关
*
* @param ids 子设备编号列表
* @param gatewayId 网关设备编号
*/
void bindDeviceGateway(Collection<Long> ids, Long gatewayId);
/**
* 解绑子设备与网关
*
* @param ids 子设备编号列表
*/
void unbindDeviceGateway(Collection<Long> ids);
/**
* 获取可绑定到网关的子设备列表
* <p>
* 条件:设备类型为 GATEWAY_SUB 且未绑定任何网关
*
* @param gatewayId 网关设备编号(可选,用于包含已绑定到该网关的设备)
* @return 子设备列表
*/
List<IotDeviceDO> getBindableSubDeviceList(@Nullable Long gatewayId);
/**
* 根据网关编号获取子设备列表
*
* @param gatewayId 网关设备编号
* @return 子设备列表
*/
List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId);
// TODO @AI暂时用不到可以删除。
/**
* 根据网关编号获取子设备数量
*
* @param gatewayId 网关设备编号
* @return 子设备数量
*/
Long getDeviceCountByGatewayId(Long gatewayId);
} }

View File

@@ -514,6 +514,85 @@ public class IotDeviceServiceImpl implements IotDeviceService {
return deviceMapper.selectListByHasLocation(); return deviceMapper.selectListByHasLocation();
} }
// ========== 网关-子设备绑定相关 ==========
@Override
@Transactional(rollbackFor = Exception.class)
public void bindDeviceGateway(Collection<Long> ids, Long gatewayId) {
if (CollUtil.isEmpty(ids)) {
return;
}
// TODO @AI校验应该是 1.1、1.2 统一风格;
// 1. 校验网关设备存在且类型正确
validateGatewayDeviceExists(gatewayId);
// 2. 校验并绑定每个子设备
List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);
if (devices.size() != ids.size()) {
throw exception(DEVICE_NOT_EXISTS);
}
List<IotDeviceDO> updateList = new ArrayList<>();
for (IotDeviceDO device : devices) {
// 2.1 校验是否为子设备类型
if (!IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType())) {
throw exception(DEVICE_NOT_GATEWAY_SUB);
}
// 2.2 校验是否已绑定其他网关
if (device.getGatewayId() != null && !device.getGatewayId().equals(gatewayId)) {
throw exception(DEVICE_GATEWAY_BINDTO_EXISTS);
}
updateList.add(new IotDeviceDO().setId(device.getId()).setGatewayId(gatewayId));
}
// 3. 批量更新数据库
// TODO @AIList<IotDeviceDO> updateList 直接 convertList不用上面 for 里面搞;校验是校验,插入是插入;
deviceMapper.updateBatch(updateList);
// 4. 清空对应缓存
deleteDeviceCache(devices);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unbindDeviceGateway(Collection<Long> ids) {
if (CollUtil.isEmpty(ids)) {
return;
}
// 1. 校验设备存在
List<IotDeviceDO> devices = deviceMapper.selectByIds(ids);
if (devices.size() != ids.size()) {
throw exception(DEVICE_NOT_EXISTS);
}
// 2. 批量更新数据库(将 gatewayId 设置为 null
List<IotDeviceDO> updateList = devices.stream()
.filter(device -> device.getGatewayId() != null)
.map(device -> new IotDeviceDO().setId(device.getId()).setGatewayId(null))
.toList();
if (CollUtil.isNotEmpty(updateList)) {
deviceMapper.updateBatch(updateList);
}
// 3. 清空对应缓存
deleteDeviceCache(devices);
}
@Override
public List<IotDeviceDO> getBindableSubDeviceList(@Nullable Long gatewayId) {
return deviceMapper.selectBindableSubDeviceList(gatewayId);
}
@Override
public List<IotDeviceDO> getDeviceListByGatewayId(Long gatewayId) {
return deviceMapper.selectListByGatewayId(gatewayId);
}
@Override
public Long getDeviceCountByGatewayId(Long gatewayId) {
return deviceMapper.selectCountByGatewayId(gatewayId);
}
private IotDeviceServiceImpl getSelf() { private IotDeviceServiceImpl getSelf() {
return SpringUtil.getBean(getClass()); return SpringUtil.getBean(getClass());
} }