From 06e9321f95f7b785db628bbdc0e1cb5389539874 Mon Sep 17 00:00:00 2001 From: xiaoqu-ai Date: Tue, 20 Oct 2026 17:32:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor(mcp):=20hardware.statistics.query=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8D=95=E6=AC=A1=E6=9F=A5=E8=AF=A2=20+=20Ja?= =?UTF-8?q?va=20=E4=BE=A7=E5=88=86=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原实现按"每物业 × 每状态"各查一次(20 物业 = 80 次 count),与"统计接口"的定位不符。 改为: - 一条 LambdaQueryWrapper 只 select(property_id, status),权限过滤照旧 - Java 侧按物业分组累计 total/online/fault/offline - 测试新增断言:多物业场景下 selectList 只调用 1 次、权限过滤只调用 1 次 理由:PermissionService 未公开"可见物业集合",无法用 QueryWrapper 直接 GROUP BY 再套权限; 用单次两列查询 + 内存分组,既保持行级权限语义,又把查询次数从 4N 降到 1。 测试:DeviceStatisticsToolsTest 6 例全过。 --- .../agent/mcp/tool/DeviceStatisticsTools.java | 61 ++++++++++++------- .../mcp/tool/DeviceStatisticsToolsTest.java | 45 +++++++++++--- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/DeviceStatisticsTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/DeviceStatisticsTools.java index 4c79946..e6f816d 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/DeviceStatisticsTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/DeviceStatisticsTools.java @@ -82,15 +82,47 @@ public class DeviceStatisticsTools implements ToolObject { boolean withUnbound = Boolean.TRUE.equals(includeUnbound); User user = AuthContextHolder.get().getUser(); - List> items = new ArrayList<>(targets.size()); + // 一条查询取回 (property_id, status) 两列,在 Java 侧分组计数。 + // 之前按"每物业 × 每状态"各查一次(20 物业 = 80 次);现在固定 1 次。 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .select(HardwareView::getPropertyId, HardwareView::getStatus) + .eq(HardwareView::getSoftDelete, 0) + .in(HardwareView::getPropertyId, targets); + if (!withUnbound) { + wrapper.gt(HardwareView::getPropertyId, 0); + } + permissionService.applyHardwareRowFilter(wrapper, user); + + Map counters = new LinkedHashMap<>(); for (Long target : targets) { + counters.put(target, new long[4]); // total, online, fault, offline + } + for (HardwareView row : hardwareViewMapper.selectList(wrapper)) { + long[] c = counters.get(row.getPropertyId()); + if (c == null) { + continue; + } + c[0]++; + Integer status = row.getStatus(); + if (status != null) { + switch (status) { + case 1 -> c[1]++; + case 2 -> c[2]++; + case 3 -> c[3]++; + default -> { } + } + } + } + + List> items = new ArrayList<>(targets.size()); + for (Map.Entry entry : counters.entrySet()) { + long[] c = entry.getValue(); Map row = new LinkedHashMap<>(); - row.put("property_id", target); - // 每个计数重建 wrapper:项目内没有 clone() 使用先例,避免依赖未验证的克隆语义。 - row.put("total", countFor(user, target, withUnbound, null)); - row.put("online", countFor(user, target, withUnbound, 1)); - row.put("fault", countFor(user, target, withUnbound, 2)); - row.put("offline", countFor(user, target, withUnbound, 3)); + row.put("property_id", entry.getKey()); + row.put("total", c[0]); + row.put("online", c[1]); + row.put("fault", c[2]); + row.put("offline", c[3]); items.add(row); } @@ -102,21 +134,6 @@ public class DeviceStatisticsTools implements ToolObject { return result; } - /** 统计某物业在指定状态下的设备数;status 为 null 表示不按状态过滤。 */ - private Long countFor(User user, Long target, boolean withUnbound, Integer status) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper() - .eq(HardwareView::getSoftDelete, 0) - .eq(HardwareView::getPropertyId, target); - if (!withUnbound) { - wrapper.gt(HardwareView::getPropertyId, 0); - } - if (status != null) { - wrapper.eq(HardwareView::getStatus, status); - } - permissionService.applyHardwareRowFilter(wrapper, user); - return hardwareViewMapper.selectCount(wrapper); - } - private static void validatePositive(Long value, String name) { if (value != null && value < 1) { throw new IllegalArgumentException(name + " 必须为正整数"); diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/DeviceStatisticsToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/DeviceStatisticsToolsTest.java index 2bb1040..f3b9346 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/DeviceStatisticsToolsTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/DeviceStatisticsToolsTest.java @@ -81,10 +81,12 @@ class DeviceStatisticsToolsTest { @Test @SuppressWarnings("unchecked") - void returnsFourCountsPerPropertyAndAppliesRowFilter() { - // 依次返回 total/online/fault/offline - when(hardwareViewMapper.selectCount(any(LambdaQueryWrapper.class))) - .thenReturn(10L, 6L, 2L, 2L); + void returnsCountsFromSingleQueryAndAppliesRowFilter() { + List rows = new ArrayList<>(); + rows.addAll(devicesOf(17L, 1, 6)); // 在线 6 + rows.addAll(devicesOf(17L, 2, 2)); // 故障 2 + rows.addAll(devicesOf(17L, 3, 2)); // 离线 2 + when(hardwareViewMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(rows); Map result = tools.hardwareStatisticsQuery(17L, null, false); @@ -97,13 +99,42 @@ class DeviceStatisticsToolsTest { assertEquals(2L, row.get("fault")); assertEquals(2L, row.get("offline")); assertTrue(result.containsKey("data_cutoff_at")); - // 总/在线/故障/离线 各一次查询 = 4 次,且每次都过行级权限 - verify(permissionService, times(4)).applyHardwareRowFilter(any(LambdaQueryWrapper.class), any(User.class)); + // 关键:无论几个物业,只查一次 + verify(hardwareViewMapper, times(1)).selectList(any(LambdaQueryWrapper.class)); + verify(permissionService, times(1)).applyHardwareRowFilter(any(LambdaQueryWrapper.class), any(User.class)); + } + + @Test + @SuppressWarnings("unchecked") + void countsMultiplePropertiesInOneQuery() { + List rows = new ArrayList<>(); + rows.addAll(devicesOf(17L, 1, 3)); + rows.addAll(devicesOf(18L, 3, 1)); + when(hardwareViewMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(rows); + + Map result = tools.hardwareStatisticsQuery(null, List.of(17L, 18L), false); + + List> items = (List>) result.get("items"); + assertEquals(2, items.size()); + assertEquals(3L, items.get(0).get("online")); + assertEquals(1L, items.get(1).get("offline")); + verify(hardwareViewMapper, times(1)).selectList(any(LambdaQueryWrapper.class)); + } + + private static List devicesOf(Long propertyId, Integer status, int count) { + List list = new ArrayList<>(); + for (int i = 0; i < count; i++) { + HardwareView view = new HardwareView(); + view.setPropertyId(propertyId); + view.setStatus(status); + list.add(view); + } + return list; } @Test void deduplicatesPropertyIdsAndAcceptsList() { - when(hardwareViewMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(1L); + when(hardwareViewMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(List.of()); Map result = tools.hardwareStatisticsQuery(null, List.of(17L, 17L), false);