feat(B10): 产品<->设备反向查询 - 设备列表可按产品过滤并展示所属产品

后端:
- DeviceQuery 新增 productId(精确) 与 productKeyword(按产品名/编码模糊)
- DeviceServiceImpl.pageDevices 支持两种产品维度过滤
  productKeyword 场景: 先解析匹配的 product_id 集合再约束 device,
  集合为空时用 1=0 保证不误返回全部
- 新增 fillProductInfo(): 批量回填设备所属产品名称与业务编码
  (一次 selectBatchIds, 避免 N+1)
- Device 实体新增非持久化展示字段 productName / productKey

验证: mvn -pl iot-common,iot-device -am compile 通过
This commit is contained in:
2995840376 2026-10-27 12:35:52 +08:00
parent d7e34efb58
commit a8c2d005a8
3 changed files with 64 additions and 1 deletions

View File

@ -49,6 +49,12 @@ public class DeviceQuery {
@Schema(description = "是否为网关(1=网关)") @Schema(description = "是否为网关(1=网关)")
private Integer isGateway; private Integer isGateway;
@Schema(description = "所属产品ID(按产品过滤设备)")
private Long productId;
@Schema(description = "所属产品关键字(按产品名/编码模糊查询)")
private String productKeyword;
@Schema(description = "页码") @Schema(description = "页码")
@Builder.Default @Builder.Default
private Integer pageNum = 1; private Integer pageNum = 1;

View File

@ -130,6 +130,16 @@ public class Device extends BaseEntity {
@Schema(description = "备注") @Schema(description = "备注")
private String remark; private String remark;
// ===== 关联展示字段(非持久化,列表查询时按 product_id 批量回填)=====
@Schema(description = "所属产品名称")
@TableField(exist = false)
private String productName;
@Schema(description = "所属产品业务编码")
@TableField(exist = false)
private String productKey;
// ===== 中控屏本地执行相关(executionSource=LOCAL_PANEL 时生效) ===== // ===== 中控屏本地执行相关(executionSource=LOCAL_PANEL 时生效) =====
/** /**

View File

@ -26,6 +26,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* 设备服务实现 * 设备服务实现
@ -150,9 +153,53 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
// 网关→子设备:按父网关过滤 // 网关→子设备:按父网关过滤
.eq(StringUtils.hasText(query.getParentDeviceId()), Device::getParentDeviceId, query.getParentDeviceId()) .eq(StringUtils.hasText(query.getParentDeviceId()), Device::getParentDeviceId, query.getParentDeviceId())
.eq(query.getIsGateway() != null, Device::getIsGateway, query.getIsGateway()) .eq(query.getIsGateway() != null, Device::getIsGateway, query.getIsGateway())
// 产品维度过滤:支持按 productId 精确查,或按产品名/编码模糊查
.eq(query.getProductId() != null && query.getProductId() > 0, Device::getProductId, query.getProductId())
.orderByDesc(Device::getUpdateTime); .orderByDesc(Device::getUpdateTime);
return page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); // 按产品关键字过滤:先解析出匹配的 product_id 集合,再约束 device
if (StringUtils.hasText(query.getProductKeyword())) {
List<Long> pids = productMapper.selectList(new LambdaQueryWrapper<Product>()
.like(StringUtils.hasText(query.getProductKeyword()), Product::getName, query.getProductKeyword())
.or().like(StringUtils.hasText(query.getProductKeyword()), Product::getProductKey, query.getProductKeyword()))
.stream().map(Product::getId).toList();
wrapper.and(w -> {
if (pids.isEmpty()) {
w.apply("1 = 0");
} else {
w.in(Device::getProductId, pids);
}
});
}
Page<Device> result = page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper);
fillProductInfo(result.getRecords());
return result;
}
/**
* 批量回填设备所属产品名称与业务编码(供后台设备列表展示"所属产品"列)。
*/
private void fillProductInfo(List<Device> devices) {
if (devices == null || devices.isEmpty()) {
return;
}
Set<Long> pids = devices.stream()
.map(Device::getProductId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (pids.isEmpty()) {
return;
}
Map<Long, Product> productMap = productMapper.selectBatchIds(pids).stream()
.collect(Collectors.toMap(Product::getId, p -> p));
for (Device d : devices) {
Product p = d.getProductId() == null ? null : productMap.get(d.getProductId());
if (p != null) {
d.setProductName(p.getName());
d.setProductKey(p.getProductKey());
}
}
} }
@Override @Override