feat(contracts,api): page-mode pagination + synced profile on admin users list

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 18:06:12 +08:00
parent 91c85e196a
commit 2f6b6afc92
7 changed files with 337 additions and 15 deletions
@@ -211,6 +211,7 @@ describe('账号查询 GET /admin/users (e2e)', () => {
'hasMore',
'items',
'nextCursor',
'total',
]);
// 管理列表包含前缀模式排除的非激活用户(本 spec 的排除组)。
const adminAccounts = (
@@ -285,4 +286,73 @@ describe('账号查询 GET /admin/users (e2e)', () => {
expect(admin.status).toBe(200);
expect(admin.body.items).toHaveLength(2);
});
it('page 模式:page/pageSize 偏移分页 + total;synced 档案快照合并;游标/前缀模式形状不受染', async () => {
// 档案种子:albert 挂映射带 profile 快照(G4);alice 无映射。
await prisma.external_user_mappings.create({
data: {
userId: albertId,
sourceSystem: 'uniservice',
externalUserId: 'ext-albert',
sourceVersion: 1,
profile: { sn: 'A001', email: 'albert@x.com', companyAbbr: '合塑' },
},
});
// 'al' 前缀域恒 5 账号:alice/albert/aldis/alpend/allock(顺序按 createdAt
// +ID 倒序,不依赖种子时序断言具体页位)。
const page1 = await lookup(adminToken, { query: 'al', page: 1, pageSize: 2 });
expect(page1.status).toBe(200);
expect(page1.body.total).toBe(5);
expect(page1.body.hasMore).toBe(false);
expect(page1.body.nextCursor).toBeNull();
expect(page1.body.items).toHaveLength(2);
const page2 = await lookup(adminToken, { query: 'al', page: 2, pageSize: 2 });
const page3 = await lookup(adminToken, { query: 'al', page: 3, pageSize: 2 });
expect(page2.body.items).toHaveLength(2);
expect(page3.body.items).toHaveLength(1);
// 走全量:三页并集 = 5 账号、无重复。
const all = [
...(page1.body.items as { account: string }[]),
...(page2.body.items as { account: string }[]),
...(page3.body.items as { account: string }[]),
];
expect(new Set(all.map((item) => item.account)).size).toBe(5);
// synced 合并:albert(有映射)带快照且字段原样;其余行 synced=null。
const albert = all
.concat() as unknown as { account: string; synced: unknown }[];
const albertRow = albert.find((item) => item.account === 'albert');
expect(albertRow?.synced).toEqual({
sn: 'A001',
email: 'albert@x.com',
companyAbbr: '合塑',
});
const aliceRow = albert.find((item) => item.account === 'alice');
expect(aliceRow?.synced).toBeNull();
// 越界页:空 items、total 仍返回。
const beyond = await lookup(adminToken, { query: 'al', page: 4, pageSize: 2 });
expect(beyond.status).toBe(200);
expect(beyond.body.items).toEqual([]);
expect(beyond.body.total).toBe(5);
// 游标模式(不带 page):total=null(键存在但空——兼容旧客户端语义)。
const cursorMode = await lookup(adminToken, { query: 'al', limit: 2 });
expect(cursorMode.status).toBe(200);
expect(cursorMode.body.total).toBeNull();
// 前缀模式条目干净:不合并 synced(邀请候选无需档案)。
const prefix = await lookup(staffToken, { accountPrefix: 'al' });
expect(prefix.status).toBe(200);
for (const item of prefix.body.items as object[]) {
expect('synced' in item).toBe(false);
}
// 参数校验:page=0 / pageSize=101 / 非整数 → 400。
expect((await lookup(adminToken, { page: 0 })).status).toBe(400);
expect((await lookup(adminToken, { page: 1, pageSize: 101 })).status).toBe(400);
expect((await lookup(adminToken, { page: 'x' })).status).toBe(400);
});
});