fix: restore daily operations quality metrics
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { moneyToNumber } from '../common/money';
|
||||
@@ -354,6 +354,135 @@ export class OperationsService {
|
||||
});
|
||||
}
|
||||
|
||||
async sendQuality(date?: string) {
|
||||
const day = qualityBusinessDay(date);
|
||||
const [channels, signatures] = await Promise.all([
|
||||
this.prisma.$queryRaw<Array<{
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
total: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
successRate: number;
|
||||
unknownRate: number;
|
||||
failureRate: number;
|
||||
averageArrivalMs: number | null;
|
||||
}>>(Prisma.sql`
|
||||
WITH base AS (
|
||||
SELECT
|
||||
submit."channelId" AS channel_id,
|
||||
channel.name AS channel_name,
|
||||
receipt."deliveredAt" AS delivered_at,
|
||||
failed_receipt."failedAt" AS failed_at,
|
||||
CASE
|
||||
WHEN receipt."deliveredAt" >= COALESCE(submit."submittedAt", submit."createdAt")
|
||||
THEN EXTRACT(EPOCH FROM (receipt."deliveredAt" - COALESCE(submit."submittedAt", submit."createdAt"))) * 1000
|
||||
END AS arrival_ms
|
||||
FROM "SmsSubmitRecord" submit
|
||||
JOIN "SmsChannel" channel ON channel.id = submit."channelId"
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT MIN(receipt."deliveredAt") AS "deliveredAt"
|
||||
FROM "SmsReceiptRecord" receipt
|
||||
WHERE receipt."gatewayMessageId" = submit."gatewayMessageId"
|
||||
AND receipt."channelId" = submit."channelId"
|
||||
AND receipt."receiptStatus" = 'delivered'
|
||||
) receipt ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT MIN(receipt."deliveredAt") AS "failedAt"
|
||||
FROM "SmsReceiptRecord" receipt
|
||||
WHERE receipt."gatewayMessageId" = submit."gatewayMessageId"
|
||||
AND receipt."channelId" = submit."channelId"
|
||||
AND receipt."receiptStatus" = 'undelivered'
|
||||
) failed_receipt ON TRUE
|
||||
WHERE submit."submitStatus" = 'accepted'
|
||||
AND COALESCE(submit."submittedAt", submit."createdAt") >= ${day.startAt}
|
||||
AND COALESCE(submit."submittedAt", submit."createdAt") < ${day.endAt}
|
||||
)
|
||||
SELECT
|
||||
channel_id AS "channelId",
|
||||
MAX(channel_name) AS "channelName",
|
||||
COUNT(*)::integer AS total,
|
||||
COUNT(*) FILTER (WHERE delivered_at IS NOT NULL)::integer AS "successCount",
|
||||
COUNT(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NULL)::integer AS "unknownCount",
|
||||
COUNT(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NOT NULL)::integer AS "failureCount",
|
||||
CASE WHEN COUNT(*) = 0 THEN 0 ELSE ROUND(COUNT(*) FILTER (WHERE delivered_at IS NOT NULL) * 100.0 / COUNT(*), 1)::double precision END AS "successRate",
|
||||
CASE WHEN COUNT(*) = 0 THEN 0 ELSE ROUND(COUNT(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NULL) * 100.0 / COUNT(*), 1)::double precision END AS "unknownRate",
|
||||
CASE WHEN COUNT(*) = 0 THEN 0 ELSE ROUND(COUNT(*) FILTER (WHERE delivered_at IS NULL AND failed_at IS NOT NULL) * 100.0 / COUNT(*), 1)::double precision END AS "failureRate",
|
||||
ROUND(AVG(arrival_ms) FILTER (WHERE arrival_ms IS NOT NULL))::integer AS "averageArrivalMs"
|
||||
FROM base
|
||||
GROUP BY channel_id
|
||||
ORDER BY COUNT(*) DESC, channel_id
|
||||
`),
|
||||
this.prisma.$queryRaw<Array<{
|
||||
id: string;
|
||||
signatureId: string;
|
||||
signatureName: string;
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
hasDrainage: boolean;
|
||||
total: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
successRate: number;
|
||||
averageArrivalMs: number | null;
|
||||
}>>(Prisma.sql`
|
||||
WITH base AS (
|
||||
SELECT
|
||||
message."signatureId" AS signature_id,
|
||||
(message."drainageInfoId" IS NOT NULL) AS has_drainage,
|
||||
message.status,
|
||||
message."receiptStatus" AS receipt_status,
|
||||
CASE
|
||||
WHEN (message.status = 'delivered' OR message."receiptStatus" = 'delivered')
|
||||
AND message."submittedAt" IS NOT NULL
|
||||
AND message."deliveredAt" >= message."submittedAt"
|
||||
THEN EXTRACT(EPOCH FROM (message."deliveredAt" - message."submittedAt")) * 1000
|
||||
END AS arrival_ms
|
||||
FROM "SmsMessageRecord" message
|
||||
WHERE message."signatureId" IS NOT NULL
|
||||
AND message."queuedAt" >= ${day.startAt}
|
||||
AND message."queuedAt" < ${day.endAt}
|
||||
)
|
||||
SELECT
|
||||
signature.id || ':' || CASE WHEN base.has_drainage THEN 'drainage' ELSE 'plain' END AS id,
|
||||
signature.id AS "signatureId",
|
||||
signature.name AS "signatureName",
|
||||
tenant.id AS "tenantId",
|
||||
tenant.name AS "tenantName",
|
||||
base.has_drainage AS "hasDrainage",
|
||||
COUNT(*) FILTER (WHERE COALESCE(base.status, '') <> 'rejected')::integer AS total,
|
||||
COUNT(*) FILTER (WHERE base.status = 'delivered' OR base.receipt_status = 'delivered')::integer AS "successCount",
|
||||
COUNT(*) FILTER (
|
||||
WHERE COALESCE(base.status, '') <> 'rejected'
|
||||
AND NOT (COALESCE(base.status = 'delivered', false) OR COALESCE(base.receipt_status = 'delivered', false))
|
||||
AND NOT (COALESCE(base.status IN ('submit_failed', 'failed', 'timeout'), false) OR COALESCE(base.receipt_status = 'undelivered', false))
|
||||
)::integer AS "unknownCount",
|
||||
COUNT(*) FILTER (
|
||||
WHERE NOT (COALESCE(base.status = 'delivered', false) OR COALESCE(base.receipt_status = 'delivered', false))
|
||||
AND (COALESCE(base.status IN ('submit_failed', 'failed', 'timeout'), false) OR COALESCE(base.receipt_status = 'undelivered', false))
|
||||
)::integer AS "failureCount",
|
||||
CASE
|
||||
WHEN COUNT(*) FILTER (WHERE COALESCE(base.status, '') <> 'rejected') = 0 THEN 0
|
||||
ELSE ROUND(
|
||||
COUNT(*) FILTER (WHERE base.status = 'delivered' OR base.receipt_status = 'delivered')
|
||||
* 100.0
|
||||
/ COUNT(*) FILTER (WHERE COALESCE(base.status, '') <> 'rejected'),
|
||||
1
|
||||
)::double precision
|
||||
END AS "successRate",
|
||||
ROUND(AVG(base.arrival_ms) FILTER (WHERE base.arrival_ms IS NOT NULL))::integer AS "averageArrivalMs"
|
||||
FROM base
|
||||
JOIN "SmsSignature" signature ON signature.id = base.signature_id
|
||||
JOIN "Tenant" tenant ON tenant.id = signature."tenantId"
|
||||
GROUP BY signature.id, signature.name, tenant.id, tenant.name, base.has_drainage
|
||||
ORDER BY "successCount" DESC, total DESC, signature.name
|
||||
`),
|
||||
]);
|
||||
return { date: day.key, channels, signatures };
|
||||
}
|
||||
|
||||
async auditLogs(query: { tenantId?: string; userId?: string; page?: number; pageSize?: number }) {
|
||||
const page = positiveInteger(query.page, 1);
|
||||
const pageSize = Math.min(100, positiveInteger(query.pageSize, 20));
|
||||
@@ -955,6 +1084,33 @@ function endOfShanghaiDay(value: string) {
|
||||
return new Date(`${value}T23:59:59.999+08:00`);
|
||||
}
|
||||
|
||||
function qualityBusinessDay(value?: string) {
|
||||
const key = value || shanghaiDateKey();
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(key)) {
|
||||
throw new BadRequestException('统计日期格式必须为 YYYY-MM-DD');
|
||||
}
|
||||
const startAt = startOfShanghaiDay(key);
|
||||
if (Number.isNaN(startAt.getTime()) || shanghaiDateKey(startAt) !== key) {
|
||||
throw new BadRequestException('统计日期无效');
|
||||
}
|
||||
return {
|
||||
key,
|
||||
startAt,
|
||||
endAt: new Date(startAt.getTime() + 24 * 60 * 60 * 1000),
|
||||
};
|
||||
}
|
||||
|
||||
function shanghaiDateKey(value = new Date()) {
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
}).formatToParts(value);
|
||||
const byType = new Map(parts.map((part) => [part.type, part.value]));
|
||||
return `${byType.get('year')}-${byType.get('month')}-${byType.get('day')}`;
|
||||
}
|
||||
|
||||
function normalizeGroupBy(groupBy?: string) {
|
||||
if (groupBy === 'tenant' || groupBy === 'tenantId') {
|
||||
return 'tenantId';
|
||||
|
||||
Reference in New Issue
Block a user