fix: correct operational statistics and form interactions

This commit is contained in:
hectorzhao
2026-09-09 23:15:42 +08:00
parent 6d63eb5452
commit 5bcdbb2a03
33 changed files with 2920 additions and 829 deletions
+52 -33
View File
@@ -1,16 +1,24 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { randomUUID } from 'node:crypto';
import { moneyToNumber } from '../../common/money';
import { PrismaService } from '../../prisma/prisma.service';
import type { MessageQuery, TraceQuery, OperationLogQuery, GatewaySubmitDeadLetterQuery, DownstreamDeliveryQuery, DownstreamDeliveryDashboardQuery, DownstreamRecoveryStatusQuery, MessageSegmentAuditQuery, SignatureQualityQuery } from '../operations.contracts';
import { messageWhere, recognizedCarrierValues, carrierWhere, startOfShanghaiDay, endOfShanghaiDay, qualityBusinessDay, shanghaiDateKey, normalizeGroupBy, returnedTransactionWhere, createdAtRange, downstreamAlertPendingMinutes, downstreamAlertRecentFailedHours, downstreamAlertWindows, downstreamAlertWhere, stalledPendingWhere, downstreamDeliveryScopedWhere, parseDateBoundary, downstreamRecoveryStatusWhere, escapeCsvCell, formatCsvDate, formatExportTimestamp, clientApplicationView, clientReceiptView, clientMessageView, clientBatchTaskView, clientUplinkView, clientAccountView, clientRechargeView, summarizeMessageGroups, groupDownstreamByType, groupDownstreamByApplication, positiveInteger, operationLogLevelWhere, normalizeOperationLog, sanitizeGatewaySubmitException, redactGatewayCommandValue } from '../operations.helpers';
import {
messageWhere,
qualityBusinessDay,
returnedTransactionWhere,
downstreamAlertWindows,
stalledPendingWhere,
clientBatchTaskView,
clientAccountView,
clientRechargeView,
summarizeMessageGroups,
} from '../operations.helpers';
// R2 dashboard query domain. Method bodies are preserved byte-for-byte from the facade baseline.
export class OperationsDashboardQueries {
constructor(private readonly prisma: PrismaService) {}
async dashboard(query: { tenantId?: string }) {
async dashboard(query: { tenantId?: string }) {
const businessDay = qualityBusinessDay();
const sinceToday = businessDay.startAt;
const downstreamAlertWindow = downstreamAlertWindows();
@@ -94,13 +102,15 @@ async dashboard(query: { tenantId?: string }) {
orderBy: { createdAt: 'desc' },
take: 10,
}),
this.prisma.$queryRaw<Array<{
tenantId: string;
tenantName: string;
todaySpendCents: bigint;
balanceCents: bigint;
creditCents: bigint;
}>>(Prisma.sql`
this.prisma.$queryRaw<
Array<{
tenantId: string;
tenantName: string;
todaySpendCents: bigint;
balanceCents: bigint;
creditCents: bigint;
}>
>(Prisma.sql`
SELECT
tenant.id AS "tenantId",
tenant.name AS "tenantName",
@@ -118,12 +128,14 @@ async dashboard(query: { tenantId?: string }) {
GROUP BY tenant.id, tenant.name, account."balanceCents", account."creditCents"
ORDER BY "todaySpendCents" DESC, tenant.name ASC
`),
this.prisma.$queryRaw<Array<{
segmentCount: bigint;
deliveredSegmentCount: bigint;
billedCents: bigint;
costCents: bigint;
}>>(Prisma.sql`
this.prisma.$queryRaw<
Array<{
segmentCount: bigint;
deliveredSegmentCount: bigint;
billedCents: bigint;
costCents: bigint;
}>
>(Prisma.sql`
WITH segment_metrics AS (
SELECT
COUNT(segment.id)::bigint AS "segmentCount",
@@ -208,11 +220,13 @@ async dashboard(query: { tenantId?: string }) {
updatedAt: { gte: downstreamAlertWindow.recentFailedAt },
},
}),
this.prisma.$queryRaw<Array<{
hour: number;
submittedCount: bigint;
successCount: bigint;
}>>(Prisma.sql`
this.prisma.$queryRaw<
Array<{
hour: number;
submittedCount: bigint;
successCount: bigint;
}>
>(Prisma.sql`
SELECT
EXTRACT(
HOUR FROM (message."queuedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai'
@@ -227,11 +241,13 @@ async dashboard(query: { tenantId?: string }) {
ORDER BY 1
`),
// Signature/template tables have no review timestamps, so their latest pending audit is paired with the review audit.
this.prisma.$queryRaw<Array<{
category: string;
count: bigint;
averageProcessingMs: bigint | null;
}>>(Prisma.sql`
this.prisma.$queryRaw<
Array<{
category: string;
count: bigint;
averageProcessingMs: bigint | null;
}>
>(Prisma.sql`
WITH review_samples AS (
SELECT
'enterpriseCertifications'::text AS category,
@@ -302,7 +318,8 @@ async dashboard(query: { tenantId?: string }) {
]);
const todayTotals = summarizeMessageGroups(todayMessageGroups);
const todayBusinessMetrics = todayBusinessMetricsRows[0];
const segmentCount = Number(todayBusinessMetrics?.segmentCount ?? 0);
const supplierSegmentCount = Number(todayBusinessMetrics?.segmentCount ?? 0);
const segmentCount = todayTotals.billingUnits;
const deliveredSegmentCount = Number(todayBusinessMetrics?.deliveredSegmentCount ?? 0);
const billedCents = moneyToNumber(todayBusinessMetrics?.billedCents);
const costCents = moneyToNumber(todayBusinessMetrics?.costCents);
@@ -334,7 +351,8 @@ async dashboard(query: { tenantId?: string }) {
averageProcessingMs: row?.averageProcessingMs == null ? null : Number(row.averageProcessingMs),
};
});
const downstreamAlertCount = downstreamStalledPendingCount + downstreamStalledAckCount + downstreamRecentFailedCount;
const downstreamAlertCount =
downstreamStalledPendingCount + downstreamStalledAckCount + downstreamRecentFailedCount;
return {
taskCount,
messageStatus: messageGroups,
@@ -349,7 +367,8 @@ async dashboard(query: { tenantId?: string }) {
billingUnits: todayTotals.billingUnits,
segmentCount,
deliveredSegmentCount,
arrivalRate: segmentCount > 0 ? Number(((deliveredSegmentCount / segmentCount) * 100).toFixed(1)) : 0,
arrivalRate:
supplierSegmentCount > 0 ? Number(((deliveredSegmentCount / supplierSegmentCount) * 100).toFixed(1)) : 0,
billedCents,
profitCents,
profitRate: billedCents > 0 ? Number(((profitCents / billedCents) * 100).toFixed(1)) : 0,
@@ -383,7 +402,7 @@ async dashboard(query: { tenantId?: string }) {
recentRecharges,
};
}
async clientDashboard(query: { tenantId?: string }) {
async clientDashboard(query: { tenantId?: string }) {
const tenantId = query.tenantId;
const [dashboard, tenant, approvedCertification, signatureCount, pendingBatchTaskCount] = await Promise.all([
this.dashboard(query),
@@ -432,7 +451,7 @@ async clientDashboard(query: { tenantId?: string }) {
},
};
}
pendingAudits(tenantId?: string) {
pendingAudits(tenantId?: string) {
return Promise.all([
this.prisma.smsTemplate.count({ where: { tenantId, auditStatus: 'pending' } }),
this.prisma.smsSignature.count({ where: { tenantId, auditStatus: 'pending' } }),