feat(iot,ops): 区域设备关联接口返回更多设备信息,修复 N+1 和代码质量问题

- IotDeviceSimpleRespDTO 新增 nickname、serialNumber、state、deviceType 字段
- IotDeviceQueryApi 新增 batchGetDevices 批量查询接口
- IotDeviceQueryApiImpl 提取 toSimpleDTO 统一转换、通过产品缓存解析 productName、
  移除 blanket try-catch 让异常正确传播、删除无用 import
- AreaDeviceRelationRespVO 新增 nickname、serialNumber、deviceState、deviceType 字段
- AreaDeviceRelationServiceImpl.listByAreaId 改为批量查询避免 N+1 RPC、
  增加 null 防护;bindDevice 改为 fail-fast 不再存脏数据
- ErrorCodeConstants 新增 IOT_SERVICE_UNAVAILABLE 错误码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-07 21:06:10 +08:00
parent 3bcdb4119f
commit 26c4ce07eb
6 changed files with 203 additions and 133 deletions

View File

@@ -10,6 +10,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
/**
@@ -38,4 +39,9 @@ public interface IotDeviceQueryApi {
@Parameter(name = "id", description = "设备ID", required = true)
CommonResult<IotDeviceSimpleRespDTO> getDevice(@RequestParam("id") Long id);
@GetMapping(PREFIX + "/batch-get")
@Operation(summary = "批量获取设备详情")
@Parameter(name = "ids", description = "设备ID列表", required = true, example = "[1,2,3]")
CommonResult<List<IotDeviceSimpleRespDTO>> batchGetDevices(@RequestParam("ids") Collection<Long> ids);
}

View File

@@ -29,4 +29,16 @@ public class IotDeviceSimpleRespDTO {
@Schema(description = "产品名称", example = "客流计数器")
private String productName;
@Schema(description = "设备备注名称", example = "A栋3层客流计数器")
private String nickname;
@Schema(description = "设备序列号", example = "SN20250101001")
private String serialNumber;
@Schema(description = "设备状态0-未激活 1-在线 2-离线)", example = "1")
private Integer state;
@Schema(description = "设备类型", example = "10")
private Integer deviceType;
}

View File

@@ -2,9 +2,10 @@ package com.viewsh.module.iot.api.device;
import com.viewsh.framework.common.pojo.CommonResult;
import com.viewsh.module.iot.api.device.dto.IotDeviceSimpleRespDTO;
import com.viewsh.module.iot.controller.admin.device.vo.device.IotDeviceRespVO;
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 io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -14,8 +15,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static com.viewsh.framework.common.pojo.CommonResult.success;
import static com.viewsh.module.iot.api.device.IotDeviceQueryApi.PREFIX;
@@ -36,55 +37,74 @@ public class IotDeviceQueryApiImpl implements IotDeviceQueryApi {
@Resource
private IotDeviceService deviceService;
@Resource
private IotProductService productService;
@Override
@GetMapping(PREFIX + "/simple-list")
@Operation(summary = "获取设备精简列表(按类型/产品筛选)")
public CommonResult<List<IotDeviceSimpleRespDTO>> getDeviceSimpleList(
@RequestParam(value = "deviceType", required = false) Integer deviceType,
@RequestParam(value = "productId", required = false) Long productId) {
try {
List<IotDeviceDO> list = deviceService.getDeviceListByCondition(deviceType, productId);
List<IotDeviceSimpleRespDTO> result = list.stream()
.map(device -> {
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
dto.setId(device.getId());
dto.setDeviceName(device.getDeviceName());
dto.setProductId(device.getProductId());
dto.setProductKey(device.getProductKey());
// TODO: 从产品服务获取产品名称
dto.setProductName("产品_" + device.getProductKey());
return dto;
})
.collect(Collectors.toList());
return success(result);
} catch (Exception e) {
log.error("[getDeviceSimpleList] 查询设备列表失败: deviceType={}, productId={}", deviceType, productId, e);
return success(List.of());
}
List<IotDeviceDO> list = deviceService.getDeviceListByCondition(deviceType, productId);
List<IotDeviceSimpleRespDTO> result = list.stream()
.map(this::toSimpleDTO)
.toList();
return success(result);
}
@Override
@GetMapping(PREFIX + "/get")
@Operation(summary = "获取设备详情")
public CommonResult<IotDeviceSimpleRespDTO> getDevice(@RequestParam("id") Long id) {
try {
IotDeviceDO device = deviceService.getDevice(id);
if (device == null) {
return success(null);
}
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
dto.setId(device.getId());
dto.setDeviceName(device.getDeviceName());
dto.setProductId(device.getProductId());
dto.setProductKey(device.getProductKey());
dto.setProductName("产品_" + device.getProductKey());
return success(dto);
} catch (Exception e) {
log.error("[getDevice] 查询设备详情失败: id={}", id, e);
IotDeviceDO device = deviceService.getDevice(id);
if (device == null) {
return success(null);
}
return success(toSimpleDTO(device));
}
@Override
@GetMapping(PREFIX + "/batch-get")
@Operation(summary = "批量获取设备详情")
public CommonResult<List<IotDeviceSimpleRespDTO>> batchGetDevices(
@RequestParam("ids") Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return success(List.of());
}
List<IotDeviceDO> devices = deviceService.getDeviceList(ids);
List<IotDeviceSimpleRespDTO> result = devices.stream()
.map(this::toSimpleDTO)
.toList();
return success(result);
}
private IotDeviceSimpleRespDTO toSimpleDTO(IotDeviceDO device) {
IotDeviceSimpleRespDTO dto = new IotDeviceSimpleRespDTO();
dto.setId(device.getId());
dto.setDeviceName(device.getDeviceName());
dto.setProductId(device.getProductId());
dto.setProductKey(device.getProductKey());
dto.setNickname(device.getNickname());
dto.setSerialNumber(device.getSerialNumber());
dto.setState(device.getState());
dto.setDeviceType(device.getDeviceType());
// 从产品缓存获取产品名称
dto.setProductName(resolveProductName(device.getProductId()));
return dto;
}
private String resolveProductName(Long productId) {
if (productId == null) {
return null;
}
try {
IotProductDO product = productService.getProductFromCache(productId);
return product != null ? product.getName() : null;
} catch (Exception e) {
log.warn("[resolveProductName] 获取产品名称失败: productId={}, error={}", productId, e.getMessage());
return null;
}
}
}

View File

@@ -21,5 +21,6 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_ALREADY_BOUND = new ErrorCode(1_020_002_001, "该工牌已绑定至此区域");
ErrorCode DEVICE_TYPE_ALREADY_BOUND = new ErrorCode(1_020_002_002, "该区域已绑定{},一个区域只能绑定一个");
ErrorCode DEVICE_RELATION_NOT_FOUND = new ErrorCode(1_020_002_003, "设备关联关系不存在");
ErrorCode IOT_SERVICE_UNAVAILABLE = new ErrorCode(1_020_002_004, "IoT 设备服务不可用,请稍后重试");
}

View File

@@ -33,6 +33,18 @@ public class AreaDeviceRelationRespVO {
@Schema(description = "设备名称", example = "客流计数器001")
private String deviceName;
@Schema(description = "设备备注名称", example = "A栋3层客流计数器")
private String nickname;
@Schema(description = "设备序列号", example = "SN20250101001")
private String serialNumber;
@Schema(description = "设备状态0-未激活 1-在线 2-离线)", example = "1")
private Integer deviceState;
@Schema(description = "设备类型", example = "10")
private Integer deviceType;
@Schema(description = "产品ID", example = "10")
private Long productId;

View File

@@ -18,9 +18,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.viewsh.module.ops.enums.AreaTypeEnum.*;
@@ -34,17 +34,17 @@ import static com.viewsh.module.ops.enums.AreaTypeEnum.*;
@Slf4j
public class AreaDeviceRelationServiceImpl implements AreaDeviceRelationService {
@Resource
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private IotDeviceQueryApi iotDeviceQueryApi;
@Resource
private AreaDeviceService areaDeviceService;
@Resource
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private IotDeviceQueryApi iotDeviceQueryApi;
@Resource
private AreaDeviceService areaDeviceService;
private static final String TYPE_TRAFFIC_COUNTER = "TRAFFIC_COUNTER";
private static final String TYPE_BEACON = "BEACON";
@@ -53,9 +53,19 @@ public class AreaDeviceRelationServiceImpl implements AreaDeviceRelationService
@Override
public List<AreaDeviceRelationRespVO> listByAreaId(Long areaId) {
List<OpsAreaDeviceRelationDO> list = opsAreaDeviceRelationMapper.selectListByAreaId(areaId);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
// 批量查询设备信息,避免 N+1 RPC 调用
Set<Long> deviceIds = list.stream()
.map(OpsAreaDeviceRelationDO::getDeviceId)
.collect(Collectors.toSet());
Map<Long, IotDeviceSimpleRespDTO> deviceMap = batchGetDeviceMap(deviceIds);
return list.stream()
.map(this::convertToRespVO)
.collect(java.util.stream.Collectors.toList());
.map(relation -> convertToRespVO(relation, deviceMap.get(relation.getDeviceId())))
.toList();
}
@Override
@@ -87,43 +97,27 @@ public class AreaDeviceRelationServiceImpl implements AreaDeviceRelationService
}
}
// 调用 IoT 接口获取设备信息
String deviceKey = "DEVICE_" + bindReq.getDeviceId();
Long productId = 0L;
String productKey = "PRODUCT_DEFAULT";
try {
CommonResult<IotDeviceSimpleRespDTO> result = iotDeviceQueryApi.getDevice(bindReq.getDeviceId());
if (result != null && result.getData() != null) {
IotDeviceSimpleRespDTO device = result.getData();
deviceKey = device.getDeviceName() != null ? device.getDeviceName() : deviceKey;
productId = device.getProductId() != null ? device.getProductId() : 0L;
productKey = device.getProductKey() != null ? device.getProductKey() : productKey;
}
} catch (Exception e) {
log.warn("[bindDevice] 调用 IoT 接口获取设备信息失败: deviceId={}, error={}",
bindReq.getDeviceId(), e.getMessage());
// 降级:使用默认值
}
// 调用 IoT 接口获取设备信息(失败则阻断绑定)
IotDeviceSimpleRespDTO device = getDeviceOrThrow(bindReq.getDeviceId());
OpsAreaDeviceRelationDO relation = OpsAreaDeviceRelationDO.builder()
.areaId(bindReq.getAreaId())
.deviceId(bindReq.getDeviceId())
.deviceKey(deviceKey)
.productId(productId)
.productKey(productKey)
.deviceKey(device.getDeviceName())
.productId(device.getProductId())
.productKey(device.getProductKey())
.relationType(bindReq.getRelationType())
.configData(bindReq.getConfigData() != null ? bindReq.getConfigData() : new HashMap<>())
.enabled(true)
.build();
opsAreaDeviceRelationMapper.insert(relation);
// 清除可能存在的 NULL_CACHE 标记
areaDeviceService.evictConfigCache(relation.getAreaId(), relation.getRelationType());
return relation.getId();
}
opsAreaDeviceRelationMapper.insert(relation);
// 清除可能存在的 NULL_CACHE 标记
areaDeviceService.evictConfigCache(relation.getAreaId(), relation.getRelationType());
return relation.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
@@ -141,60 +135,85 @@ public class AreaDeviceRelationServiceImpl implements AreaDeviceRelationService
relation.setConfigData(updateReq.getConfigData());
}
// enabled 更新
if (updateReq.getEnabled() != null) {
relation.setEnabled(updateReq.getEnabled());
}
opsAreaDeviceRelationMapper.updateById(relation);
// 删缓存以同步 Redis
areaDeviceService.evictConfigCache(existing.getAreaId(), existing.getRelationType());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean unbindDevice(Long id) {
OpsAreaDeviceRelationDO existing = opsAreaDeviceRelationMapper.selectById(id);
if (existing == null) {
return false;
}
boolean deleted = opsAreaDeviceRelationMapper.deleteById(id) > 0;
if (deleted) {
// 同步 Redis 缓存
areaDeviceService.evictConfigCache(existing.getAreaId(), existing.getRelationType());
}
return deleted;
}
/**
* 转换为响应 VO
* 从 IoT 模块获取设备名称和产品名称(带降级)
*/
private AreaDeviceRelationRespVO convertToRespVO(OpsAreaDeviceRelationDO relation) {
AreaDeviceRelationRespVO respVO = BeanUtil.copyProperties(relation, AreaDeviceRelationRespVO.class);
// 从 IoT 模块获取设备信息
try {
CommonResult<IotDeviceSimpleRespDTO> result = iotDeviceQueryApi.getDevice(relation.getDeviceId());
if (result != null && result.getData() != null) {
IotDeviceSimpleRespDTO device = result.getData();
respVO.setDeviceName(device.getDeviceName());
respVO.setProductName(device.getProductName());
} else {
// 降级:使用默认值
respVO.setDeviceName("设备_" + relation.getDeviceId());
respVO.setProductName("产品_" + relation.getProductKey());
}
} catch (Exception e) {
log.warn("[convertToRespVO] 调用 IoT 接口获取设备信息失败: deviceId={}, error={}",
relation.getDeviceId(), e.getMessage());
// 降级:使用默认值
respVO.setDeviceName("设备_" + relation.getDeviceId());
respVO.setProductName("产品_" + relation.getProductKey());
// enabled 更新
if (updateReq.getEnabled() != null) {
relation.setEnabled(updateReq.getEnabled());
}
opsAreaDeviceRelationMapper.updateById(relation);
// 删缓存以同步 Redis
areaDeviceService.evictConfigCache(existing.getAreaId(), existing.getRelationType());
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean unbindDevice(Long id) {
OpsAreaDeviceRelationDO existing = opsAreaDeviceRelationMapper.selectById(id);
if (existing == null) {
return false;
}
boolean deleted = opsAreaDeviceRelationMapper.deleteById(id) > 0;
if (deleted) {
// 同步 Redis 缓存
areaDeviceService.evictConfigCache(existing.getAreaId(), existing.getRelationType());
}
return deleted;
}
/**
* 批量查询设备信息,返回 deviceId → DTO 映射
*/
private Map<Long, IotDeviceSimpleRespDTO> batchGetDeviceMap(Set<Long> deviceIds) {
if (deviceIds.isEmpty()) {
return Collections.emptyMap();
}
try {
CommonResult<List<IotDeviceSimpleRespDTO>> result = iotDeviceQueryApi.batchGetDevices(deviceIds);
if (result != null && result.getData() != null) {
return result.getData().stream()
.collect(Collectors.toMap(IotDeviceSimpleRespDTO::getId, Function.identity()));
}
} catch (Exception e) {
log.warn("[batchGetDeviceMap] 批量查询设备信息失败: deviceIds={}, error={}", deviceIds, e.getMessage());
}
return Collections.emptyMap();
}
/**
* 获取单个设备信息,失败则抛出异常阻断操作
*/
private IotDeviceSimpleRespDTO getDeviceOrThrow(Long deviceId) {
try {
CommonResult<IotDeviceSimpleRespDTO> result = iotDeviceQueryApi.getDevice(deviceId);
if (result != null && result.getData() != null) {
return result.getData();
}
throw ServiceExceptionUtil.exception(ErrorCodeConstants.DEVICE_NOT_FOUND);
} catch (com.viewsh.framework.common.exception.ServiceException e) {
throw e;
} catch (Exception e) {
log.error("[getDeviceOrThrow] 调用 IoT 接口获取设备信息失败: deviceId={}, error={}",
deviceId, e.getMessage());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.IOT_SERVICE_UNAVAILABLE);
}
}
/**
* 转换为响应 VO使用预查询的设备信息避免 N+1
*/
private AreaDeviceRelationRespVO convertToRespVO(OpsAreaDeviceRelationDO relation,
IotDeviceSimpleRespDTO device) {
AreaDeviceRelationRespVO respVO = BeanUtil.copyProperties(relation, AreaDeviceRelationRespVO.class);
if (device != null) {
respVO.setDeviceName(device.getDeviceName());
respVO.setProductName(device.getProductName());
respVO.setNickname(device.getNickname());
respVO.setSerialNumber(device.getSerialNumber());
respVO.setDeviceState(device.getState());
respVO.setDeviceType(device.getDeviceType());
}
return respVO;
}