fix: paginate operational list pages

This commit is contained in:
hectorzhao
2026-07-28 23:20:13 +08:00
parent 87c5b1eccc
commit b8560372cc
40 changed files with 1208 additions and 422 deletions
@@ -12,8 +12,14 @@ export class AdminSmsConfigController {
constructor(private readonly smsConfig: SmsConfigService, private readonly reviewGovernance: ReviewGovernanceService, private readonly deletions: DeletionGovernanceService) {}
@Get('enterprise-applications')
listApplications(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('status') status?: string) {
return this.smsConfig.listApplications({ tenantId, keyword, enterpriseKeyword, applicationKeyword, status, includeConnections: true });
listApplications(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('status') status?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
const query = { tenantId, keyword, enterpriseKeyword, applicationKeyword, status, includeConnections: true, page: Number(page), pageSize: Number(pageSize) };
return page || pageSize ? this.smsConfig.listApplicationsPage(query) : this.smsConfig.listApplications(query);
}
@Get('enterprise-application-options')
listApplicationOptions(@Query('tenantId') tenantId?: string) {
return this.smsConfig.listApplicationOptions(tenantId);
}
@Get('enterprise-applications/:id')
@@ -65,8 +71,14 @@ export class AdminSmsConfigController {
}
@Get('enterprise-signatures')
listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string) {
return this.smsConfig.listSignatures({ tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword });
listSignatures(@Query('tenantId') tenantId?: string, @Query('keyword') keyword?: string, @Query('status') status?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('signatureKeyword') signatureKeyword?: string, @Query('drainageKeyword') drainageKeyword?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
const query = { tenantId, keyword, status, enterpriseKeyword, applicationKeyword, signatureKeyword, drainageKeyword, page: Number(page), pageSize: Number(pageSize) };
return page || pageSize ? this.smsConfig.listSignaturesPage(query) : this.smsConfig.listSignatures(query);
}
@Get('enterprise-signature-options')
listSignatureOptions(@Query('tenantId') tenantId?: string) {
return this.smsConfig.listSignatureOptions(tenantId);
}
@Post('enterprise-signatures')
@@ -113,8 +125,9 @@ export class AdminSmsConfigController {
}
@Get('enterprise-templates')
listTemplates(@Query('tenantId') tenantId?: string, @Query('status') status?: string, @Query('keyword') keyword?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('nameKeyword') nameKeyword?: string, @Query('contentKeyword') contentKeyword?: string) {
return this.smsConfig.listTemplates({ tenantId, status, keyword, enterpriseKeyword, applicationKeyword, nameKeyword, contentKeyword });
listTemplates(@Query('tenantId') tenantId?: string, @Query('status') status?: string, @Query('keyword') keyword?: string, @Query('enterpriseKeyword') enterpriseKeyword?: string, @Query('applicationKeyword') applicationKeyword?: string, @Query('nameKeyword') nameKeyword?: string, @Query('contentKeyword') contentKeyword?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
const query = { tenantId, status, keyword, enterpriseKeyword, applicationKeyword, nameKeyword, contentKeyword, page: Number(page), pageSize: Number(pageSize) };
return page || pageSize ? this.smsConfig.listTemplatesPage(query) : this.smsConfig.listTemplates(query);
}
@Post('enterprise-templates')
@@ -23,8 +23,15 @@ export class ClientSmsConfigController {
constructor(private readonly smsConfig: SmsConfigService, private readonly deletions: DeletionGovernanceService) {}
@Get('applications')
listApplications(@TenantId() tenantId?: string) {
return this.smsConfig.listApplications(tenantId);
listApplications(@TenantId() tenantId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
return page || pageSize
? this.smsConfig.listApplicationsPage({ tenantId, page: Number(page), pageSize: Number(pageSize) })
: this.smsConfig.listApplications(tenantId);
}
@Get('application-options')
listApplicationOptions(@TenantId() tenantId?: string) {
return this.smsConfig.listApplicationOptions(tenantId);
}
@Post('applications')
@@ -64,9 +71,14 @@ export class ClientSmsConfigController {
return this.smsConfig.listClientSignatures(tenantId);
}
@Get('signature-options')
listSignatureOptions(@TenantId() tenantId?: string) {
return this.smsConfig.listSignatureOptions(tenantId);
}
@Get('signatures-workspace')
getSignatureWorkspace(@TenantId() tenantId?: string) {
return this.smsConfig.getClientSignatureWorkspace(tenantId);
getSignatureWorkspace(@TenantId() tenantId?: string, @Query('keyword') keyword?: string, @Query('applicationId') applicationId?: string, @Query('status') status?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
return this.smsConfig.getClientSignatureWorkspace(tenantId, { keyword, applicationId, status, page: Number(page), pageSize: Number(pageSize) });
}
@Post('signatures')
@@ -124,8 +136,10 @@ export class ClientSmsConfigController {
}
@Get('templates')
listTemplates(@TenantId() tenantId?: string, @Query('includeHistory') includeHistory?: string) {
return this.smsConfig.listClientTemplates(tenantId, includeHistory === 'true');
listTemplates(@TenantId() tenantId?: string, @Query('includeHistory') includeHistory?: string, @Query('keyword') keyword?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string) {
return page || pageSize
? this.smsConfig.listTemplatesPage({ tenantId, status: includeHistory === 'true' ? 'all' : 'approved', keyword, page: Number(page), pageSize: Number(pageSize) })
: this.smsConfig.listClientTemplates(tenantId, includeHistory === 'true');
}
@Post('templates')
@@ -69,6 +69,7 @@ function createPrismaMock() {
},
smsSignature: {
groupBy: jest.fn().mockResolvedValue([]),
count: jest.fn().mockResolvedValue(0),
findMany: jest.fn().mockResolvedValue([{
id: 'sig-1',
tenantId: 'tenant-1',
@@ -900,6 +901,7 @@ describe('SmsConfigService', () => {
it('returns real client signature workspace counts from database grouping', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findMany.mockResolvedValue([]);
prisma.smsSignature.count.mockResolvedValue(8);
prisma.smsSignature.groupBy.mockResolvedValue([
{ auditStatus: 'pending', _count: { _all: 2 } },
{ auditStatus: 'approved', _count: { _all: 5 } },
@@ -910,6 +912,9 @@ describe('SmsConfigService', () => {
await expect(service.getClientSignatureWorkspace('tenant-1')).resolves.toEqual({
items: [],
summary: { total: 8, pending: 2, approved: 5, rejected: 1, draft: 0 },
total: 8,
page: 1,
pageSize: 10,
});
expect(prisma.smsSignature.groupBy).toHaveBeenCalledWith(expect.objectContaining({
where: { tenantId: 'tenant-1', auditStatus: { notIn: ['deleted', 'disabled'] } },
+147 -6
View File
@@ -122,6 +122,8 @@ export interface TemplateListQuery {
applicationKeyword?: string;
nameKeyword?: string;
contentKeyword?: string;
page?: number;
pageSize?: number;
}
export interface ApplicationListQuery {
@@ -131,6 +133,8 @@ export interface ApplicationListQuery {
applicationKeyword?: string;
status?: string;
includeConnections?: boolean;
page?: number;
pageSize?: number;
}
export interface SignatureListQuery {
@@ -141,6 +145,8 @@ export interface SignatureListQuery {
applicationKeyword?: string;
signatureKeyword?: string;
drainageKeyword?: string;
page?: number;
pageSize?: number;
}
export interface GatewayDownstreamConnectionEventDto {
@@ -205,7 +211,15 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
ipAllowlist: true,
httpConfig: true,
},
omit: {
secretHash: true,
passwordCipher: true,
},
orderBy: { createdAt: 'desc' },
...(query.page && query.pageSize ? {
skip: (query.page - 1) * query.pageSize,
take: query.pageSize,
} : {}),
});
if (!query.includeConnections) {
return applications;
@@ -243,6 +257,34 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
|| left.id.localeCompare(right.id));
}
async listApplicationsPage(query: ApplicationListQuery) {
const page = Math.max(1, Math.floor(Number(query.page) || 1));
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10)));
const where: Prisma.SmsApplicationWhereInput = {
tenantId: query.tenantId,
status: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
name: query.applicationKeyword ? { contains: query.applicationKeyword } : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ tenant: { name: { contains: query.keyword } } },
] : undefined,
};
const [items, total] = await Promise.all([
this.listApplications({ ...query, page, pageSize }),
this.prisma.smsApplication.count({ where }),
]);
return { items, total, page, pageSize };
}
listApplicationOptions(tenantId?: string) {
return this.prisma.smsApplication.findMany({
where: { tenantId, status: { not: 'deleted' } },
select: { id: true, tenantId: true, name: true, status: true },
orderBy: [{ name: 'asc' }, { id: 'asc' }],
});
}
async getApplication(applicationId: string, tenantId?: string) {
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
@@ -905,6 +947,10 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
reportTasks: { include: { channel: true, drainageInfo: true } },
},
orderBy: { createdAt: 'desc' },
...(query.page && query.pageSize ? {
skip: (query.page - 1) * query.pageSize,
take: query.pageSize,
} : {}),
});
const applicationIds = signatures.map((signature) => signature.applicationId).filter((id): id is string => Boolean(id));
const routes = applicationIds.length ? await this.prisma.channelRouteRule.findMany({
@@ -979,9 +1025,60 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
});
}
async listClientSignatures(tenantId?: string, signatureId?: string) {
async listSignaturesPage(query: SignatureListQuery) {
const page = Math.max(1, Math.floor(Number(query.page) || 1));
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10)));
const where: Prisma.SmsSignatureWhereInput = {
tenantId: query.tenantId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
name: query.signatureKeyword ? { contains: query.signatureKeyword } : undefined,
drainageItems: query.drainageKeyword ? {
some: {
auditStatus: { not: 'deleted' },
OR: [
{ siteName: { contains: query.drainageKeyword } },
{ url: { contains: query.drainageKeyword } },
{ remark: { contains: query.drainageKeyword } },
],
},
} : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ purpose: { contains: query.keyword } },
{ tenant: { name: { contains: query.keyword } } },
{ application: { name: { contains: query.keyword } } },
] : undefined,
};
const [items, total] = await Promise.all([
this.listSignatures({ ...query, page, pageSize }),
this.prisma.smsSignature.count({ where }),
]);
return { items, total, page, pageSize };
}
listSignatureOptions(tenantId?: string) {
return this.prisma.smsSignature.findMany({
where: { tenantId, auditStatus: { not: 'deleted' } },
select: { id: true, tenantId: true, applicationId: true, name: true, auditStatus: true },
orderBy: [{ name: 'asc' }, { id: 'asc' }],
});
}
async listClientSignatures(tenantId?: string, signatureId?: string, query: { keyword?: string; applicationId?: string; status?: string; page?: number; pageSize?: number } = {}) {
const signatures = await this.prisma.smsSignature.findMany({
where: { id: signatureId, tenantId, auditStatus: { notIn: ['deleted', 'disabled'] } },
where: {
id: signatureId,
tenantId,
applicationId: query.applicationId,
auditStatus: query.status || { notIn: ['deleted', 'disabled'] },
OR: query.keyword?.trim() ? [
{ name: { contains: query.keyword.trim() } },
{ purpose: { contains: query.keyword.trim() } },
{ application: { name: { contains: query.keyword.trim() } } },
] : undefined,
},
select: {
id: true,
tenantId: true,
@@ -1020,6 +1117,8 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
_count: { select: { reportMaterials: true } },
},
orderBy: { updatedAt: 'desc' },
skip: query.page && query.pageSize ? (query.page - 1) * query.pageSize : undefined,
take: query.pageSize,
});
return signatures.map((signature) => {
const stored = isRecord(signature.drainageInfo) ? signature.drainageInfo : {};
@@ -1056,9 +1155,22 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
return signature;
}
async getClientSignatureWorkspace(tenantId?: string) {
const [items, statusCounts] = await Promise.all([
this.listClientSignatures(tenantId),
async getClientSignatureWorkspace(tenantId?: string, query: { keyword?: string; applicationId?: string; status?: string; page?: number; pageSize?: number } = {}) {
const page = Math.max(1, Math.floor(Number(query.page) || 1));
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10)));
const filteredWhere: Prisma.SmsSignatureWhereInput = {
tenantId,
applicationId: query.applicationId,
auditStatus: query.status || { notIn: ['deleted', 'disabled'] },
OR: query.keyword?.trim() ? [
{ name: { contains: query.keyword.trim() } },
{ purpose: { contains: query.keyword.trim() } },
{ application: { name: { contains: query.keyword.trim() } } },
] : undefined,
};
const [items, total, statusCounts] = await Promise.all([
this.listClientSignatures(tenantId, undefined, { ...query, page, pageSize }),
this.prisma.smsSignature.count({ where: filteredWhere }),
this.prisma.smsSignature.groupBy({
by: ['auditStatus'],
where: { tenantId, auditStatus: { notIn: ['deleted', 'disabled'] } },
@@ -1073,7 +1185,7 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
summary[item.auditStatus as keyof Omit<typeof summary, 'total'>] = count;
}
}
return { items, summary };
return { items, summary, total, page, pageSize };
}
async listClientDrainageInfos(tenantId?: string, itemId?: string) {
@@ -1464,9 +1576,38 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
},
include: { variables: true, application: true, tenant: true, signature: true },
orderBy: { createdAt: 'desc' },
...(query.page && query.pageSize ? {
skip: (query.page - 1) * query.pageSize,
take: query.pageSize,
} : {}),
});
}
async listTemplatesPage(query: TemplateListQuery) {
const page = Math.max(1, Math.floor(Number(query.page) || 1));
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 10)));
const where: Prisma.SmsTemplateWhereInput = {
tenantId: query.tenantId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
application: query.applicationKeyword ? { name: { contains: query.applicationKeyword } } : undefined,
name: query.nameKeyword ? { contains: query.nameKeyword } : undefined,
content: query.contentKeyword ? { contains: query.contentKeyword } : undefined,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ content: { contains: query.keyword } },
{ category: { contains: query.keyword } },
{ application: { name: { contains: query.keyword } } },
{ tenant: { name: { contains: query.keyword } } },
] : undefined,
};
const [items, total] = await Promise.all([
this.listTemplates({ ...query, page, pageSize }),
this.prisma.smsTemplate.count({ where }),
]);
return { items, total, page, pageSize };
}
listClientTemplates(tenantId: string | undefined, includeHistory = false) {
return this.listTemplates({ tenantId, status: includeHistory ? 'all' : 'approved' });
}