feat: optimize signature workflows and high-frequency queries

This commit is contained in:
hectorzhao
2026-09-02 15:45:48 +08:00
parent 9b8196ecab
commit ad89e8fed7
48 changed files with 1334 additions and 352 deletions
@@ -99,6 +99,11 @@ export class AdminOperationsController {
response.send(`\uFEFF${exported.content}`);
}
@Get('messages/:id')
getMessage(@Param('id') id: string) {
return this.operations.getMessage(id);
}
@Get('message-segment-audits')
messageSegmentAudits(
@Query('messageId') messageId?: string,
+27 -1
View File
@@ -18,6 +18,7 @@ function createPrismaMock() {
},
smsMessageRecord: {
findMany: jest.fn().mockResolvedValue([{ messageId: 'MSG-1' }]),
findUnique: jest.fn().mockResolvedValue({ id: 'message-1', messageId: 'MSG-1', submitRecords: [], receiptRecords: [], downstreamDeliveries: [] }),
count: jest.fn().mockResolvedValue(51),
groupBy: jest.fn().mockResolvedValue([{ status: 'delivered', _count: { _all: 2 }, _sum: { amountCents: 20, billingUnits: 2 } }]),
aggregate: jest.fn().mockResolvedValue({ _count: { _all: 2 }, _sum: { amountCents: 20, billingUnits: 2 } }),
@@ -311,7 +312,7 @@ describe('OperationsService', () => {
}));
});
it('paginates message records in PostgreSQL and limits heavy relations to the requested page', async () => {
it('paginates message summaries without preloading detail relations', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
@@ -326,7 +327,17 @@ describe('OperationsService', () => {
skip: 25,
take: 25,
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
select: expect.objectContaining({
id: true,
content: true,
hasDrainageContent: true,
tenant: { select: { id: true, name: true } },
}),
}));
const call = prisma.smsMessageRecord.findMany.mock.calls.at(-1)?.[0];
expect(call.select).not.toHaveProperty('submitRecords');
expect(call.select).not.toHaveProperty('receiptRecords');
expect(call.select).not.toHaveProperty('downstreamDeliveries');
expect(prisma.smsMessageRecord.count).toHaveBeenCalledWith({
where: expect.objectContaining({ tenantId: 'tenant-1' }),
});
@@ -359,6 +370,21 @@ describe('OperationsService', () => {
});
});
it('loads heavy message relations only for one requested detail', async () => {
const prisma = createPrismaMock();
const service = new OperationsService(prisma as never);
await expect(service.getMessage('message-1')).resolves.toEqual(expect.objectContaining({ id: 'message-1' }));
expect(prisma.smsMessageRecord.findUnique).toHaveBeenCalledWith(expect.objectContaining({
where: { id: 'message-1' },
include: expect.objectContaining({
submitRecords: expect.any(Object),
receiptRecords: expect.any(Object),
downstreamDeliveries: expect.any(Object),
}),
}));
});
it('returns the matched message record and the distinct uplink gateway message id to the client view', async () => {
const prisma = createPrismaMock();
prisma.smsUplinkMessage.findMany.mockResolvedValue([{
+4
View File
@@ -48,6 +48,10 @@ export class OperationsService {
return this.messagesQueries.listMessagesPage(query);
}
async getMessage(id: string) {
return this.messagesQueries.getMessage(id);
}
async exportMessages(query: MessageQuery) {
return this.messagesQueries.exportMessages(query);
}
+65 -36
View File
@@ -45,44 +45,26 @@ async listMessagesPage(query: MessageQuery) {
const [items, total] = await Promise.all([
this.prisma.smsMessageRecord.findMany({
where,
include: {
select: {
id: true,
tenantId: true,
applicationId: true,
channelId: true,
messageId: true,
phoneNumber: true,
carrier: true,
province: true,
content: true,
hasDrainageContent: true,
drainageDetection: true,
billingUnits: true,
amountCents: true,
status: true,
submitStatus: true,
queuedAt: true,
tenant: { select: { id: true, name: true } },
application: { select: { id: true, name: true } },
channel: { select: { id: true, name: true, srcId: true } },
submitRecords: {
select: {
id: true,
submitId: true,
channelId: true,
channelGroupId: true,
channelGroupName: true,
gatewayMessageId: true,
submitStatus: true,
submittedAt: true,
createdAt: true,
channel: { select: { id: true, name: true } },
channelGroup: { select: { id: true, name: true } },
},
},
receiptRecords: {
select: {
id: true,
messageId: true,
gatewayMessageId: true,
receiptStatus: true,
rawStatus: true,
errorCode: true,
errorMessage: true,
deliveredAt: true,
createdAt: true,
channelId: true,
channel: { select: { id: true, name: true } },
},
},
downstreamDeliveries: {
where: { deliveryType: 'receipt' },
select: { id: true, deliveryType: true, status: true, deliveredAt: true, lastError: true },
},
channel: { select: { id: true, name: true } },
},
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
skip: (page - 1) * pageSize,
@@ -92,6 +74,53 @@ async listMessagesPage(query: MessageQuery) {
]);
return { items, total, page, pageSize };
}
async getMessage(id: string) {
const item = await this.prisma.smsMessageRecord.findUnique({
where: { id },
include: {
tenant: { select: { id: true, name: true } },
application: { select: { id: true, name: true } },
channel: { select: { id: true, name: true, srcId: true } },
submitRecords: {
select: {
id: true,
submitId: true,
channelId: true,
channelGroupId: true,
channelGroupName: true,
gatewayMessageId: true,
submitStatus: true,
submittedAt: true,
createdAt: true,
channel: { select: { id: true, name: true } },
channelGroup: { select: { id: true, name: true } },
},
},
receiptRecords: {
select: {
id: true,
messageId: true,
gatewayMessageId: true,
receiptStatus: true,
rawStatus: true,
errorCode: true,
errorMessage: true,
deliveredAt: true,
createdAt: true,
channelId: true,
channel: { select: { id: true, name: true } },
},
},
downstreamDeliveries: {
where: { deliveryType: 'receipt' },
select: { id: true, deliveryType: true, status: true, deliveredAt: true, lastError: true },
},
},
});
if (!item) throw new NotFoundException('Message record not found');
return item;
}
async exportMessages(query: MessageQuery) {
const items = await this.prisma.smsMessageRecord.findMany({
where: messageWhere(query),
@@ -82,6 +82,21 @@ export class AdminSmsConfigController {
return this.smsConfig.listSignatureOptions(tenantId);
}
@Get('enterprise-signatures/:id')
getSignature(@Param('id') id: string) {
return this.smsConfig.getSignature(id);
}
@Get('enterprise-signatures/:id/report-targets')
getSignatureReportTargets(@Param('id') id: string) {
return this.smsConfig.getSignatureReportTargets(id);
}
@Get('drainage-infos/:id/report-targets')
getDrainageReportTargets(@Param('id') id: string) {
return this.smsConfig.getDrainageReportTargets(id);
}
@Post('enterprise-signatures')
createSignature(@Body() body: CreateSmsSignatureDto) {
return this.smsConfig.createSignature(body, { initialAuditStatus: 'approved' });
+109 -10
View File
@@ -72,12 +72,13 @@ export class SmsSignatureService {
private readonly reportValidation: SmsReportValidationService,
private readonly audit: SmsAuditService,
) {}
async listSignatures(queryOrTenantId?: string | SignatureListQuery) {
async listSignatures(queryOrTenantId?: string | SignatureListQuery, summaryOnly = false) {
const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : (queryOrTenantId ?? {});
const signatureSort =
query.signatureSort === 'asc' || query.signatureSort === 'desc' ? query.signatureSort : undefined;
const signatures = await this.prisma.smsSignature.findMany({
where: {
id: query.signatureId,
tenantId: query.tenantId,
auditStatus: query.status && query.status !== 'all' ? query.status : { not: 'deleted' },
tenant: query.enterpriseKeyword ? { name: { contains: query.enterpriseKeyword } } : undefined,
@@ -105,12 +106,54 @@ export class SmsSignatureService {
]
: undefined,
},
include: {
materials: true,
tenant: true,
application: true,
drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } },
reportTasks: { include: { channel: true, drainageInfo: true } },
select: {
id: true,
tenantId: true,
applicationId: true,
name: true,
purpose: true,
drainageInfo: true,
auditStatus: true,
reportStatus: true,
rejectReason: true,
materialVersion: true,
pendingReport: true,
reportChangedAt: true,
createdAt: true,
updatedAt: true,
materials: !summaryOnly,
tenant: { select: { id: true, name: true, code: true, status: true } },
application: { select: { id: true, tenantId: true, name: true, status: true } },
drainageItems: {
where: { auditStatus: { not: 'deleted' } },
orderBy: { updatedAt: 'desc' },
select: {
id: true,
siteName: true,
url: true,
remark: true,
reportValues: !summaryOnly,
auditStatus: true,
rejectReason: true,
submittedAt: true,
reviewedAt: true,
createdAt: true,
updatedAt: true,
},
},
reportTasks: {
select: {
id: true,
signatureId: true,
channelId: true,
carrier: true,
status: true,
approvedAt: true,
approvalScope: true,
reportType: true,
drainageItemId: true,
},
},
reportBatchItems: {
where: { batch: { status: { in: ['completed', 'partial_failed'] } } },
select: { reportType: true, materialVersion: true, snapshot: true },
@@ -130,7 +173,28 @@ export class SmsSignatureService {
const routes = applicationIds.length
? await this.prisma.channelRouteRule.findMany({
where: { applicationId: { in: applicationIds }, status: 'active' },
include: { group: { include: { items: { include: { channel: { include: { reportFields: true } } } } } } },
select: {
applicationId: true,
group: {
select: {
status: true,
items: {
select: {
channel: {
select: {
id: true,
name: true,
carrier: true,
carriers: true,
status: true,
reportFields: { select: { status: true, reportType: true } },
},
},
},
},
},
},
},
})
: [];
const hasCommonDrainageFields = await this.prisma.commonReportField
@@ -208,7 +272,7 @@ export class SmsSignatureService {
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
}));
return {
const view = {
...signatureView,
name: normalizeSmsSignature(signature.name),
drainageInfo: { ...legacyPayload, links: drainageLinks },
@@ -330,6 +394,20 @@ export class SmsSignatureService {
}),
),
};
if (!summaryOnly) return view;
const {
materials: _materials,
reportTasks: _reportTasks,
reportTargets: _reportTargets,
drainageReportTargets: _drainageReportTargets,
...summary
} = view;
return {
...summary,
drainageInfo: {
links: drainageLinks.map(({ reportValues: _reportValues, ...link }) => link),
},
};
});
}
@@ -365,12 +443,33 @@ export class SmsSignatureService {
: undefined,
};
const [items, total] = await Promise.all([
this.listSignatures({ ...query, page, pageSize }),
this.listSignatures({ ...query, page, pageSize }, true),
this.prisma.smsSignature.count({ where }),
]);
return { items, total, page, pageSize };
}
async getSignature(id: string) {
const [item] = await this.listSignatures({ signatureId: id });
if (!item || item.auditStatus === 'deleted') throw new NotFoundException('Signature not found');
return item;
}
async getSignatureReportTargets(id: string) {
const item = await this.getSignature(id);
return 'reportTargets' in item ? item.reportTargets ?? [] : [];
}
async getDrainageReportTargets(id: string) {
const drainage = await this.prisma.smsDrainageInfo.findUnique({
where: { id },
select: { id: true, signatureId: true, auditStatus: true },
});
if (!drainage || drainage.auditStatus === 'deleted') throw new NotFoundException('Drainage info not found');
const signature = await this.getSignature(drainage.signatureId);
return 'drainageReportTargets' in signature ? signature.drainageReportTargets?.[id] ?? [] : [];
}
listSignatureOptions(tenantId?: string) {
return this.prisma.smsSignature.findMany({
where: { tenantId, auditStatus: { not: 'deleted' } },
@@ -136,6 +136,7 @@ export interface ApplicationListQuery {
}
export interface SignatureListQuery {
signatureId?: string;
tenantId?: string;
keyword?: string;
status?: string;
+20 -5
View File
@@ -796,12 +796,12 @@ describe('SmsConfigService', () => {
name: { contains: '签名' },
drainageItems: expect.objectContaining({ some: expect.objectContaining({ OR: expect.any(Array) }) }),
}),
include: expect.objectContaining({
select: expect.objectContaining({
materials: true,
tenant: true,
application: true,
drainageItems: { where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } },
reportTasks: { include: { channel: true, drainageInfo: true } },
tenant: { select: { id: true, name: true, code: true, status: true } },
application: { select: { id: true, tenantId: true, name: true, status: true } },
drainageItems: expect.objectContaining({ where: { auditStatus: { not: 'deleted' } }, orderBy: { updatedAt: 'desc' } }),
reportTasks: expect.objectContaining({ select: expect.any(Object) }),
reportBatchItems: expect.any(Object),
}),
orderBy: [{ name: 'asc' }, { id: 'asc' }],
@@ -1215,6 +1215,21 @@ describe('SmsConfigService', () => {
expect(prisma.smsDrainageInfo.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: expect.objectContaining({ submittedAt: expectedRange }) }));
});
it('returns a signature page summary without edit materials or report target arrays', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.count.mockResolvedValue(1);
const service = new SmsConfigService(prisma as never);
const result = await service.listSignaturesPage({ page: 1, pageSize: 10 });
expect(result.total).toBe(1);
expect(result.items[0]).toEqual(expect.objectContaining({ id: 'sig-1', name: '【签名A】' }));
expect(result.items[0]).not.toHaveProperty('materials');
expect(result.items[0]).not.toHaveProperty('reportTasks');
expect(result.items[0]).not.toHaveProperty('reportTargets');
expect(result.items[0]).not.toHaveProperty('drainageReportTargets');
});
it.each([
'【带 空格】',
' 【外部空格】',
+12
View File
@@ -123,6 +123,18 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
return this.signatures.listSignaturesPage(query);
}
async getSignature(id: string) {
return this.signatures.getSignature(id);
}
async getSignatureReportTargets(id: string) {
return this.signatures.getSignatureReportTargets(id);
}
async getDrainageReportTargets(id: string) {
return this.signatures.getDrainageReportTargets(id);
}
listSignatureOptions(tenantId?: string) {
return this.signatures.listSignatureOptions(tenantId);
}
+5
View File
@@ -18,6 +18,11 @@ export class TenantsController {
return this.tenants.listManagementRows();
}
@Get('options')
listOptions() {
return this.tenants.listOptions();
}
@Get(':id')
get(@Param('id') id: string) {
return this.tenants.get(id);
+13
View File
@@ -44,6 +44,19 @@ function createPrismaMock() {
}
describe('TenantsService', () => {
it('returns lightweight non-deleted tenant options', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
await service.listOptions();
expect(prisma.tenant.findMany).toHaveBeenCalledWith({
where: { status: { not: 'deleted' } },
select: { id: true, name: true, code: true, status: true },
orderBy: [{ name: 'asc' }, { id: 'asc' }],
});
});
it('creates tenants with a real enterprise profile', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
+8
View File
@@ -44,6 +44,14 @@ export class TenantsService {
}).then((items) => items.map(withEnterpriseProfile));
}
listOptions() {
return this.prisma.tenant.findMany({
where: { status: { not: 'deleted' } },
select: { id: true, name: true, code: true, status: true },
orderBy: [{ name: 'asc' }, { id: 'asc' }],
});
}
async listManagementRows() {
const sinceToday = startOfToday();
const [tenants, accounts, todaySpendGroups, todayRefundGroups] = await Promise.all([