diff --git a/xiaoqu-agent-service/front/src/App.vue b/xiaoqu-agent-service/front/src/App.vue index 4ca28d8..f17dbe0 100644 --- a/xiaoqu-agent-service/front/src/App.vue +++ b/xiaoqu-agent-service/front/src/App.vue @@ -24,8 +24,10 @@ const tokens = ref([]) const openClients = ref([]) const scopes = ref([]) const tools = ref([]) +const allTools = ref([]) const audits = ref([]) const toolFilter = reactive({ category: '', kind: '', keyword: '' }) +const toolPage = reactive({ page: 1, pageSize: 20, total: 0 }) const auditFilter = reactive({ person: '', tokenPrefix: '', tool: '', result: '', client: '', from: '', to: '' }) const tokenDialog = ref(false) const tokenForm = reactive({ id: null, memo: '', scopeIds: [], scopeId: null, expireTime: null }) @@ -40,8 +42,8 @@ const activeScope = ref(null) const selectedToolIds = ref([]) const title = computed(() => menus.find(item => item.id === current.value)?.label || '') -const categories = computed(() => [...new Set(tools.value.map(tool => tool.category))]) -const groupedTools = computed(() => tools.value.reduce((groups, tool) => { +const categories = computed(() => [...new Set(allTools.value.map(tool => tool.category))]) +const groupedTools = computed(() => allTools.value.reduce((groups, tool) => { const category = tool.category || '其他' ;(groups[category] ||= []).push(tool) return groups @@ -96,9 +98,9 @@ async function switchMenu(id) { current.value = id if (id === 'tokens') await loadUsers() if (id === 'clients') await loadOpenClients() - if (id === 'tools') await loadTools() + if (id === 'tools') await Promise.all([loadTools(), loadAllTools()]) if (id === 'scopes') await loadScopes() - if (id === 'audit') await Promise.all([loadAudit(), api('/tools').then(data => { tools.value = data })]) + if (id === 'audit') await Promise.all([loadAudit(), loadAllTools()]) } async function loadUsers(resetPage = false) { @@ -214,8 +216,21 @@ async function rotateClient(client) { }).catch(() => {}) } -async function loadTools() { - tools.value = await api(`/tools${qs(toolFilter)}`) +async function loadTools(resetPage = false) { + if (resetPage) toolPage.page = 1 + const data = await api(`/tools${qs({ ...toolFilter, page: toolPage.page, pageSize: toolPage.pageSize })}`) + tools.value = data.records + toolPage.total = data.total +} + +async function loadAllTools() { + const data = await api('/tools?pageSize=0') + allTools.value = data.records +} + +async function changeToolPage(page) { + toolPage.page = page + await run(loadTools) } async function setToolStatus(tool) { @@ -261,7 +276,7 @@ async function deleteScope(scope) { async function openScopeTools(scope) { activeScope.value = scope - ;[selectedToolIds.value, tools.value] = await Promise.all([api(`/scopes/${scope.id}/tools`), api('/tools')]) + ;[selectedToolIds.value] = await Promise.all([api(`/scopes/${scope.id}/tools`), loadAllTools()]) scopeToolsDialog.value = true } @@ -411,8 +426,8 @@ onMounted(async () => {
- - 查询 + + 查询
@@ -427,6 +442,9 @@ onMounted(async () => { +

“停用”是全局紧急开关,生效后任何 Token 都无法调用该工具。

@@ -456,7 +474,7 @@ onMounted(async () => {
- + diff --git a/xiaoqu-agent-service/front/tests/source-contract.test.mjs b/xiaoqu-agent-service/front/tests/source-contract.test.mjs index fe3286a..2a6c4c6 100644 --- a/xiaoqu-agent-service/front/tests/source-contract.test.mjs +++ b/xiaoqu-agent-service/front/tests/source-contract.test.mjs @@ -19,6 +19,7 @@ test('prototype interactions expose stable test ids', async () => { 'admin-table-clients', 'admin-issued-client-key', 'admin-table-tools', + 'admin-tool-pager', 'admin-btn-new-scope', 'admin-table-scopes', 'admin-table-audit', diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/admin/AdminConsoleController.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/admin/AdminConsoleController.java index 53849fa..2761974 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/admin/AdminConsoleController.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/admin/AdminConsoleController.java @@ -162,16 +162,24 @@ public class AdminConsoleController { return tokenService.rotate(id, adminId(session)); } + /** pageSize=0 表示不分页返回全量,供作用域配置、审计筛选等需要完整工具列表的场景使用。 */ @GetMapping("/tools") - public List tools(@RequestParam(defaultValue = "") String category, - @RequestParam(defaultValue = "") String kind, - @RequestParam(defaultValue = "") String keyword) { + public ToolPageView tools(@RequestParam(defaultValue = "") String category, + @RequestParam(defaultValue = "") String kind, + @RequestParam(defaultValue = "") String keyword, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize) { + int pageNo = Math.max(page, 1); + int size = pageSize == 0 ? 0 : pageSize < 1 ? 20 : Math.min(pageSize, 100); LambdaQueryWrapper query = new LambdaQueryWrapper().eq(Tool::getSoftDelete, 0); if (!category.isBlank()) query.eq(Tool::getCategory, category); if (!kind.isBlank()) query.eq(Tool::getKind, kind); if (!keyword.isBlank()) query.and(w -> w.like(Tool::getName, keyword.trim()) .or().like(Tool::getDescription, keyword.trim())); - List tools = toolMapper.selectList(query.orderByAsc(Tool::getCategory, Tool::getName)); + long total = toolMapper.selectCount(query); + LambdaQueryWrapper paged = query.orderByAsc(Tool::getCategory, Tool::getName); + if (size > 0) paged.last("LIMIT " + (long) (pageNo - 1) * size + "," + size); + List tools = toolMapper.selectList(paged); Map scopes = scopeMapper.selectList(new LambdaQueryWrapper() .eq(AgentScope::getSoftDelete, 0)).stream() .collect(Collectors.toMap(AgentScope::getId, Function.identity())); @@ -180,9 +188,10 @@ public class AdminConsoleController { .filter(r -> scopes.containsKey(r.getScopeId())) .collect(Collectors.groupingBy(ScopeTool::getToolId, Collectors.mapping(r -> scopes.get(r.getScopeId()).getName(), Collectors.toList()))); - return tools.stream().map(tool -> new ToolView(tool.getId(), tool.getName(), tool.getCategory(), - tool.getDescription(), tool.getKind(), tool.getStatus(), tool.getModifyTime(), + List records = tools.stream().map(tool -> new ToolView(tool.getId(), tool.getName(), + tool.getCategory(), tool.getDescription(), tool.getKind(), tool.getStatus(), tool.getModifyTime(), scopeNames.getOrDefault(tool.getId(), List.of()))).toList(); + return new ToolPageView(total, size > 0 ? pageNo : 1, size, records); } @PatchMapping("/tools/{id}/status") @@ -397,6 +406,7 @@ public class AdminConsoleController { public record StatusRequest(byte status) {} public record ToolView(Long id, String name, String category, String description, String kind, Byte status, Date modifyTime, List scopeNames) {} + public record ToolPageView(long total, int page, int pageSize, List records) {} public record ScopeRequest(String name, String code, String memo) {} public record ScopeView(Long id, String name, String code, String memo, Byte status, Date modifyTime, long toolCount) {} diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/admin/AdminConsoleControllerTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/admin/AdminConsoleControllerTest.java index 5dec504..6c6070e 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/admin/AdminConsoleControllerTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/admin/AdminConsoleControllerTest.java @@ -46,11 +46,15 @@ class AdminConsoleControllerTest { new org.apache.ibatis.builder.MapperBuilderAssistant( new com.baomidou.mybatisplus.core.MybatisConfiguration(), ""), me.iiv.agent.entity.m0.User.class); + com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo( + new org.apache.ibatis.builder.MapperBuilderAssistant( + new com.baomidou.mybatisplus.core.MybatisConfiguration(), ""), + me.iiv.agent.entity.svc.Tool.class); controller = new AdminConsoleController(userMapper, authorityMapper, agentMapper, tokenMapper, scopeMapper, toolMapper, scopeToolMapper, auditMapper, tokenScopeMapper, permissionService, tokenService); - when(userMapper.selectCount(any())).thenReturn(0L); - when(userMapper.selectList(any())).thenReturn(List.of()); + org.mockito.Mockito.lenient().when(userMapper.selectCount(any())).thenReturn(0L); + org.mockito.Mockito.lenient().when(userMapper.selectList(any())).thenReturn(List.of()); } @Test @@ -78,4 +82,33 @@ class AdminConsoleControllerTest { assertTrue(sql.contains("enterprise_type"), "用户列表必须过滤物业账号: " + sql); assertTrue(sql.contains("IS NULL"), "历史 NULL 数据需保留: " + sql); } + + @Test + void toolsClampsPageSizeToHundred() { + when(toolMapper.selectCount(any())).thenReturn(30L); + when(toolMapper.selectList(any())).thenReturn(List.of()); + when(scopeMapper.selectList(any())).thenReturn(List.of()); + when(scopeToolMapper.selectList(any())).thenReturn(List.of()); + AdminConsoleController.ToolPageView view = controller.tools("", "", "", 1, 500); + assertEquals(100, view.pageSize()); + assertEquals(1, view.page()); + assertEquals(30, view.total()); + } + + @Test + void toolsPageSizeZeroReturnsAllWithoutLimit() { + when(toolMapper.selectCount(any())).thenReturn(30L); + when(toolMapper.selectList(any())).thenReturn(List.of()); + when(scopeMapper.selectList(any())).thenReturn(List.of()); + when(scopeToolMapper.selectList(any())).thenReturn(List.of()); + AdminConsoleController.ToolPageView view = controller.tools("", "", "", 3, 0); + assertEquals(0, view.pageSize()); + assertEquals(1, view.page()); + assertEquals(30, view.total()); + org.mockito.ArgumentCaptor> + captor = org.mockito.ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper.class); + org.mockito.Mockito.verify(toolMapper).selectList(captor.capture()); + assertTrue(captor.getValue().getSqlSegment() == null || !captor.getValue().getSqlSegment().contains("LIMIT"), + "全量模式不应带 LIMIT: " + captor.getValue().getSqlSegment()); + } }