Files
lislgosms/api/src/signature-analytics/analytics-read.ts
T

218 lines
9.2 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { addDays, analyticsDate, analyticsPage, databaseDay, todayKey } from './analytics-date';
export interface ActivityQuery {
date?: string;
dimensionType: string;
page?: number;
pageSize?: number;
tenantName?: string;
applicationName?: string;
signatureName?: string;
channelName?: string;
}
export class SignatureAnalyticsRead {
constructor(private readonly db: PrismaService) {}
async metadata(date: string, now = new Date()) {
const record = await this.db.signatureAnalyticsDay.findUnique({ where: { businessDate: databaseDay(date) } });
const run = await this.db.signatureAnalyticsRun.findUnique({
where: { scope_businessDate: { scope: 'daily', businessDate: databaseDay(date) } },
});
const reportState =
run?.state === 'running'
? 'refreshing'
: ['retry_wait', 'failed'].includes(run?.state ?? '')
? 'failed'
: (record?.state ?? 'missing');
return {
dataSource: 'report' as const,
businessDate: date,
serverBusinessDate: todayKey(now),
reportState,
frozen: date <= addDays(todayKey(now), -4),
generatedAt: record?.generatedAt ?? null,
sourceAsOf: record?.sourceAsOf ?? null,
generationId: record?.publishedGenerationId ?? null,
schemaVersion: record?.schemaVersion ?? 1,
provenance: record?.provenance ?? null,
};
}
async quality(query: { date?: string; keyword?: string; page?: number; pageSize?: number }) {
const date = analyticsDate(query.date);
const { page, pageSize } = analyticsPage(query.page, query.pageSize);
return this.db.$transaction(
async (tx) => {
const meta = await new SignatureAnalyticsRead(tx as PrismaService).metadata(date);
if (!meta.generationId) return { date, items: [], total: 0, page, pageSize, ...meta };
const where: Prisma.SignatureQualityDailyWhereInput = {
generationId: meta.generationId,
...(query.keyword?.trim()
? {
OR: ['signatureName', 'tenantName', 'applicationNames'].map((field) => ({
[field]: { contains: query.keyword!.trim(), mode: 'insensitive' },
})),
}
: {}),
};
const total = await tx.signatureQualityDaily.count({ where });
const rows = await tx.signatureQualityDaily.findMany({
where,
orderBy: [{ total: 'desc' }, { signatureName: 'asc' }, { signatureId: 'asc' }],
skip: (page - 1) * pageSize,
take: pageSize,
});
return { date, items: rows.map((r) => r.payload), total, page, pageSize, ...meta };
},
{ isolationLevel: 'RepeatableRead', timeout: 15_000 },
);
}
async unreported(query: { date?: string; keyword?: string; page?: number; pageSize?: number }) {
const date = analyticsDate(query.date);
const { page, pageSize } = analyticsPage(query.page, query.pageSize);
return this.db.$transaction(
async (tx) => {
const meta = await new SignatureAnalyticsRead(tx as PrismaService).metadata(date);
if (!meta.generationId) return { date, items: [], total: 0, page, pageSize, ...meta };
const where: Prisma.UnreportedSignatureDailyWhereInput = {
generationId: meta.generationId,
...(query.keyword?.trim()
? {
OR: ['signatureName', 'tenantName', 'applicationName'].map((field) => ({
[field]: { contains: query.keyword!.trim(), mode: 'insensitive' },
})),
}
: {}),
};
const total = await tx.unreportedSignatureDaily.count({ where });
const rows = await tx.unreportedSignatureDaily.findMany({
where,
orderBy: [{ messageCount: 'desc' }, { dimensionKey: 'asc' }],
skip: (page - 1) * pageSize,
take: pageSize,
});
return {
date,
items: rows.map((r) => ({ ...r, signatureId: r.dimensionKey })),
total,
page,
pageSize,
...meta,
};
},
{ isolationLevel: 'RepeatableRead', timeout: 15_000 },
);
}
async activity(query: ActivityQuery) {
const date = analyticsDate(query.date);
if (!['enterprise', 'channel'].includes(query.dimensionType))
throw new BadRequestException('必须指定企业或通道维度');
const { page, pageSize } = analyticsPage(query.page, query.pageSize);
const dates = Array.from({ length: 30 }, (_, i) => addDays(date, -i - 1));
return this.db.$transaction(
async (tx) => {
await tx.$executeRawUnsafe("SET LOCAL statement_timeout='12s'");
const manifests = await tx.signatureAnalyticsDay.findMany({
where: { businessDate: { in: dates.map(databaseDay) } },
});
const runs = await tx.signatureAnalyticsRun.findMany({
where: { scope: 'daily', businessDate: { in: dates.map(databaseDay) } },
});
const runStates = new Map(runs.map((r) => [r.businessDate.toISOString().slice(0, 10), r.state]));
const byDate = new Map(manifests.map((r) => [r.businessDate.toISOString().slice(0, 10), r]));
const coverage = dates.map((d) => {
const r = byDate.get(d);
return {
date: d,
generationId: r?.publishedGenerationId ?? null,
reportState:
runStates.get(d) === 'running'
? 'refreshing'
: ['failed', 'retry_wait'].includes(runStates.get(d) ?? '')
? 'failed'
: (r?.state ?? 'missing'),
generatedAt: r?.generatedAt ?? null,
sourceAsOf: r?.sourceAsOf ?? null,
frozen: d <= addDays(todayKey(), -4),
};
});
const generations = coverage.flatMap((c) => (c.generationId ? [c.generationId] : []));
if (!generations.length)
return { date, items: [], dimensions: [], total: 0, page, pageSize, coverage, complete: false };
const filters = [
['tenantName', query.tenantName],
['applicationName', query.applicationName],
['signatureName', query.signatureName],
['channelName', query.channelName],
]
.filter(([, value]) => value?.trim())
.map(([field, value]) => Prisma.sql`AND r.${Prisma.raw(`"${field}"`)} ILIKE ${`%${value!.trim()}%`}`);
const dimensions = await tx.$queryRaw<
Array<{
dimensionKey: string;
dimensionType: string;
signatureId: string;
channelKey: string;
carrier: string;
signatureName: string;
channelName: string;
tenantName: string;
applicationName: string;
approvedAt: Date | null;
total: number;
rowCount: number;
}>
>(Prisma.sql`
WITH selected AS (
SELECT * FROM "SignatureActivityDaily" WHERE "generationId" IN (${Prisma.join(generations)}) AND "dimensionType"=${query.dimensionType}
), latest AS (
SELECT DISTINCT ON ("dimensionKey") * FROM selected ORDER BY "dimensionKey","businessDate" DESC
), sums AS (SELECT "dimensionKey",SUM("acceptedBusinessCount")::integer AS total FROM selected GROUP BY 1)
SELECT r.*,s.total,COUNT(*) OVER()::integer AS "rowCount" FROM latest r JOIN sums s USING("dimensionKey")
WHERE TRUE ${filters.length ? Prisma.join(filters, ' ') : Prisma.empty} ORDER BY s.total DESC,r."signatureName",r."dimensionKey"
LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`);
// An empty out-of-range page still reports the filtered total.
const emptyPageCount = dimensions.length
? []
: await tx.$queryRaw<Array<{ total: number }>>(Prisma.sql`
WITH latest AS (
SELECT DISTINCT ON ("dimensionKey") * FROM "SignatureActivityDaily"
WHERE "generationId" IN (${Prisma.join(generations)}) AND "dimensionType"=${query.dimensionType}
ORDER BY "dimensionKey","businessDate" DESC
) SELECT COUNT(*)::integer AS total FROM latest r WHERE TRUE ${filters.length ? Prisma.join(filters, ' ') : Prisma.empty}`);
const items = dimensions.length
? await tx.signatureActivityDaily.findMany({
where: {
generationId: { in: generations },
dimensionType: query.dimensionType,
dimensionKey: { in: dimensions.map((d) => d.dimensionKey) },
},
})
: [];
return {
date,
dimensions: dimensions.map((d) => ({ ...d, channelId: d.channelKey || null })),
items: items.map((r) => ({
...r,
id: `${r.generationId}:${r.dimensionKey}`,
channelId: r.channelKey || null,
activityDate: r.businessDate.toISOString().slice(0, 10),
status: r.applicability,
})),
total: dimensions[0]?.rowCount ?? emptyPageCount[0]?.total ?? 0,
page,
pageSize,
coverage,
complete: coverage.every((c) => Boolean(c.generationId)),
};
},
{ isolationLevel: 'RepeatableRead', timeout: 15_000 },
);
}
}