fix: 修复上行归属并实现签名质量日报优化
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user