diff --git a/xiaoqu-agent-service/front/src/App.vue b/xiaoqu-agent-service/front/src/App.vue
index 635d0b5..8ea33bc 100644
--- a/xiaoqu-agent-service/front/src/App.vue
+++ b/xiaoqu-agent-service/front/src/App.vue
@@ -16,6 +16,7 @@ const session = reactive({ name: '', csrfToken: '' })
const loginForm = reactive({ account: '', password: '' })
const users = ref([])
const userKeyword = ref('')
+const userPage = reactive({ page: 1, pageSize: 20, total: 0 })
const selectedUser = ref(null)
const permission = ref(null)
const tokens = ref([])
@@ -80,14 +81,22 @@ async function switchMenu(id) {
if (id === 'audit') await Promise.all([loadAudit(), api('/tools').then(data => { tools.value = data })])
}
-async function loadUsers() {
- users.value = await api(`/users${qs({ keyword: userKeyword.value })}`)
+async function loadUsers(resetPage = false) {
+ if (resetPage) userPage.page = 1
+ const data = await api(`/users${qs({ keyword: userKeyword.value, page: userPage.page, pageSize: userPage.pageSize })}`)
+ users.value = data.records
+ userPage.total = data.total
const existing = users.value.find(user => user.id === selectedUser.value?.id)
if (existing) selectedUser.value = existing
else if (users.value.length) await selectUser(users.value[0])
else { selectedUser.value = null; permission.value = null; tokens.value = [] }
}
+async function changeUserPage(page) {
+ userPage.page = page
+ await run(loadUsers)
+}
+
async function selectUser(user) {
selectedUser.value = user
;[permission.value, tokens.value, scopes.value] = await Promise.all([
@@ -248,8 +257,8 @@ onMounted(async () => {
人员来自现有 user 表
- 搜索
+ data-testid="admin-input-user-search" @keyup.enter="run(() => loadUsers(true))" />
+ loadUsers(true))">搜索
+
数据权限归属只读 · 在现有系统维护
diff --git a/xiaoqu-agent-service/front/src/style.css b/xiaoqu-agent-service/front/src/style.css
index a759b30..1244039 100644
--- a/xiaoqu-agent-service/front/src/style.css
+++ b/xiaoqu-agent-service/front/src/style.css
@@ -40,6 +40,7 @@ button, input { font: inherit; }
.user-row.selected { background: #ecf5ff; }
.user-row span:first-child { display: flex; flex-direction: column; gap: 4px; }
.user-row small { color: #909399; }
+.user-pager { margin-top: 10px; justify-content: flex-end; }
.permission-card dl { margin: 0; border-top: 1px solid #ebeef5; }
.permission-card dl > div { display: grid; grid-template-columns: 92px 1fr; border-bottom: 1px solid #ebeef5; }
.permission-card dt, .permission-card dd { margin: 0; padding: 10px 6px; }
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 50ad2fa..a059811 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
@@ -82,19 +82,26 @@ public class AdminConsoleController {
}
@GetMapping("/users")
- public List users(@RequestParam(defaultValue = "") String keyword) {
+ public UserPageView users(@RequestParam(defaultValue = "") String keyword,
+ @RequestParam(defaultValue = "1") int page,
+ @RequestParam(defaultValue = "20") int pageSize) {
if (keyword.length() > 100) bad("搜索关键词最多 100 字");
+ int pageNo = Math.max(page, 1);
+ int size = pageSize < 1 ? 20 : Math.min(pageSize, 100);
LambdaQueryWrapper query = new LambdaQueryWrapper().eq(User::getSoftDelete, 0);
if (!keyword.isBlank()) query.and(w -> w.like(User::getName, keyword.trim())
.or().like(User::getMobile, keyword.trim()));
- List users = userMapper.selectList(query.orderByDesc(User::getId).last("LIMIT 100"));
+ Long total = userMapper.selectCount(query);
+ List users = userMapper.selectList(query.orderByDesc(User::getId)
+ .last("LIMIT " + (long) (pageNo - 1) * size + "," + size));
Set ids = users.stream().map(User::getId).collect(Collectors.toSet());
Map counts = ids.isEmpty() ? Map.of() : tokenMapper.selectList(
new LambdaQueryWrapper().in(AgentApiKey::getUserId, ids)
.eq(AgentApiKey::getSoftDelete, 0))
.stream().collect(Collectors.groupingBy(AgentApiKey::getUserId, Collectors.counting()));
- return users.stream().map(user -> new UserView(user.getId(), user.getName(), maskMobile(user.getMobile()),
- counts.getOrDefault(user.getId(), 0L))).toList();
+ List records = users.stream().map(user -> new UserView(user.getId(), user.getName(),
+ maskMobile(user.getMobile()), counts.getOrDefault(user.getId(), 0L))).toList();
+ return new UserPageView(total, pageNo, size, records);
}
@GetMapping("/users/{id}/permission")
@@ -375,6 +382,7 @@ public class AdminConsoleController {
}
public record UserView(Long id, String name, String mobile, long tokenCount) {}
+ public record UserPageView(long total, int page, int pageSize, List records) {}
public record PermissionView(String authorityName, Integer isAgent, String agentName,
Integer enterpriseType, String visibleRange) {}
public record TokenRequest(String memo, List scopeIds, Date expireTime) {}
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
new file mode 100644
index 0000000..ccbf91d
--- /dev/null
+++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/admin/AdminConsoleControllerTest.java
@@ -0,0 +1,65 @@
+package me.iiv.agent.admin;
+
+import me.iiv.agent.mapper.m0.AgentMapper;
+import me.iiv.agent.mapper.m0.AuthorityMapper;
+import me.iiv.agent.mapper.m0.UserMapper;
+import me.iiv.agent.mapper.svc.AgentApiKeyMapper;
+import me.iiv.agent.mapper.svc.AgentScopeMapper;
+import me.iiv.agent.mapper.svc.AuditLogMapper;
+import me.iiv.agent.mapper.svc.ScopeToolMapper;
+import me.iiv.agent.mapper.svc.TokenScopeMapper;
+import me.iiv.agent.mapper.svc.ToolMapper;
+import me.iiv.agent.permission.PermissionService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AdminConsoleControllerTest {
+
+ @Mock UserMapper userMapper;
+ @Mock AuthorityMapper authorityMapper;
+ @Mock AgentMapper agentMapper;
+ @Mock AgentApiKeyMapper tokenMapper;
+ @Mock AgentScopeMapper scopeMapper;
+ @Mock ToolMapper toolMapper;
+ @Mock ScopeToolMapper scopeToolMapper;
+ @Mock AuditLogMapper auditMapper;
+ @Mock TokenScopeMapper tokenScopeMapper;
+ @Mock PermissionService permissionService;
+ @Mock AdminTokenService tokenService;
+
+ AdminConsoleController controller;
+
+ @BeforeEach
+ void setUp() {
+ 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());
+ }
+
+ @Test
+ void usersClampsPageSizeToHundred() {
+ AdminConsoleController.UserPageView view = controller.users("", 1, 500);
+ assertEquals(100, view.pageSize());
+ assertEquals(1, view.page());
+ assertEquals(0, view.total());
+ }
+
+ @Test
+ void usersDefaultsInvalidPageAndSize() {
+ AdminConsoleController.UserPageView view = controller.users("", 0, 0);
+ assertEquals(1, view.page());
+ assertEquals(20, view.pageSize());
+ }
+}