diff --git a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApi.java b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApi.java index 1a5025d..24ee8ab 100644 --- a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApi.java +++ b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApi.java @@ -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 getDevice(@RequestParam("id") Long id); + @GetMapping(PREFIX + "/batch-get") + @Operation(summary = "批量获取设备详情") + @Parameter(name = "ids", description = "设备ID列表", required = true, example = "[1,2,3]") + CommonResult> batchGetDevices(@RequestParam("ids") Collection ids); + } diff --git a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/dto/IotDeviceSimpleRespDTO.java b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/dto/IotDeviceSimpleRespDTO.java index a130329..1ca23d7 100644 --- a/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/dto/IotDeviceSimpleRespDTO.java +++ b/viewsh-module-iot/viewsh-module-iot-api/src/main/java/com/viewsh/module/iot/api/device/dto/IotDeviceSimpleRespDTO.java @@ -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; + } diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApiImpl.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApiImpl.java index fed268f..1f08750 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApiImpl.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/api/device/IotDeviceQueryApiImpl.java @@ -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> getDeviceSimpleList( @RequestParam(value = "deviceType", required = false) Integer deviceType, @RequestParam(value = "productId", required = false) Long productId) { - try { - List list = deviceService.getDeviceListByCondition(deviceType, productId); - List 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 list = deviceService.getDeviceListByCondition(deviceType, productId); + List result = list.stream() + .map(this::toSimpleDTO) + .toList(); + return success(result); } @Override @GetMapping(PREFIX + "/get") @Operation(summary = "获取设备详情") public CommonResult 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> batchGetDevices( + @RequestParam("ids") Collection ids) { + if (ids == null || ids.isEmpty()) { + return success(List.of()); + } + List devices = deviceService.getDeviceList(ids); + List 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; + } } } diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ErrorCodeConstants.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ErrorCodeConstants.java index 6d60cce..ca60e2c 100644 --- a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ErrorCodeConstants.java +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/ErrorCodeConstants.java @@ -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 设备服务不可用,请稍后重试"); } diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/vo/area/AreaDeviceRelationRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/vo/area/AreaDeviceRelationRespVO.java index 1f58fe3..caf86f1 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/vo/area/AreaDeviceRelationRespVO.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/dataobject/vo/area/AreaDeviceRelationRespVO.java @@ -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; diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceRelationServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceRelationServiceImpl.java index 04a1238..ba1a63d 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceRelationServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/area/AreaDeviceRelationServiceImpl.java @@ -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 listByAreaId(Long areaId) { List list = opsAreaDeviceRelationMapper.selectListByAreaId(areaId); + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + + // 批量查询设备信息,避免 N+1 RPC 调用 + Set deviceIds = list.stream() + .map(OpsAreaDeviceRelationDO::getDeviceId) + .collect(Collectors.toSet()); + Map 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 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 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 batchGetDeviceMap(Set deviceIds) { + if (deviceIds.isEmpty()) { + return Collections.emptyMap(); + } + try { + CommonResult> 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 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; }