feat: add signature quality period trends

This commit is contained in:
hectorzhao
2026-08-09 22:54:32 +08:00
parent 608662a054
commit 35de17a2d4
11 changed files with 385 additions and 30 deletions
+21 -3
View File
@@ -677,6 +677,7 @@ describe('OperationsService', () => {
averageArrivalMs: 1200,
rowCount: 12,
}])
.mockResolvedValueOnce([{ count: 4 }])
.mockResolvedValueOnce([
{
signatureId: 'signature-1',
@@ -727,6 +728,18 @@ describe('OperationsService', () => {
averageArrivalMs: 1800,
},
]);
prisma.$queryRaw.mockResolvedValueOnce([
{
signatureId: 'signature-1',
channelId: 'channel-1',
channelName: '通道一',
carrier: 'mobile',
date: '2026-07-24',
businessMessageCount: 3,
deliveredMessageCount: 2,
deliveryRate: 66.7,
},
]);
const service = new OperationsService(prisma as never);
await expect(service.signatureQuality({
@@ -736,6 +749,7 @@ describe('OperationsService', () => {
pageSize: 5,
})).resolves.toEqual({
date: '2026-07-24',
selectedDaySignatureCount: 4,
items: [expect.objectContaining({
signatureId: 'signature-1',
signatureName: '【测试签名】',
@@ -763,27 +777,31 @@ describe('OperationsService', () => {
expect.objectContaining({ channelId: 'channel-1', carrier: 'mobile', drainageState: 'with', total: 4 }),
expect.objectContaining({ channelId: 'channel-2', carrier: 'telecom', drainageState: 'without', total: 2 }),
],
dailyBreakdowns: [
expect.objectContaining({ channelId: 'channel-1', carrier: 'mobile', date: '2026-07-24', businessMessageCount: 3 }),
],
})],
total: 12,
page: 2,
pageSize: 5,
});
expect(prisma.$queryRaw).toHaveBeenCalledTimes(3);
expect(prisma.$queryRaw).toHaveBeenCalledTimes(5);
});
it('does not query channel details when the selected date has no registered signatures', async () => {
const prisma = createPrismaMock();
prisma.$queryRaw.mockResolvedValueOnce([]);
prisma.$queryRaw.mockResolvedValueOnce([]).mockResolvedValueOnce([{ count: 0 }]);
const service = new OperationsService(prisma as never);
await expect(service.signatureQuality({ date: '2026-07-24' })).resolves.toEqual({
date: '2026-07-24',
selectedDaySignatureCount: 0,
items: [],
total: 0,
page: 1,
pageSize: 10,
});
expect(prisma.$queryRaw).toHaveBeenCalledTimes(1);
expect(prisma.$queryRaw).toHaveBeenCalledTimes(2);
});
it('returns trace details and reconciliation diffs', async () => {
+58 -5
View File
@@ -316,11 +316,13 @@ async sendQuality(date?: string) {
}
async signatureQuality(query: SignatureQualityQuery) {
const day = qualityBusinessDay(query.date);
// The selected date is an inclusive business cutoff, so a 30-day window starts 29 full days earlier.
const thirtyDayStartAt = new Date(day.startAt.getTime() - 29 * 24 * 60 * 60 * 1000);
const page = positiveInteger(query.page, 1);
const pageSize = Math.min(50, positiveInteger(query.pageSize, 10));
const keyword = query.keyword?.trim() || null;
const keywordPattern = keyword ? `%${keyword}%` : null;
const summaries = await this.prisma.$queryRaw<Array<{
const [summaries, selectedDaySignatureRows] = await Promise.all([this.prisma.$queryRaw<Array<{
signatureId: string;
signatureName: string;
tenantId: string;
@@ -351,7 +353,7 @@ async signatureQuality(query: SignatureQualityQuery) {
END AS arrival_ms
FROM "SmsMessageRecord" message
WHERE message."signatureId" IS NOT NULL
AND message."queuedAt" >= ${day.startAt}
AND message."queuedAt" >= ${thirtyDayStartAt}
AND message."queuedAt" < ${day.endAt}
)
SELECT
@@ -413,7 +415,13 @@ async signatureQuality(query: SignatureQualityQuery) {
ORDER BY total DESC, signature.name
LIMIT ${pageSize}
OFFSET ${(page - 1) * pageSize}
`);
`), this.prisma.$queryRaw<Array<{ count: number }>>(Prisma.sql`
SELECT COUNT(DISTINCT message."signatureId")::integer AS count
FROM "SmsMessageRecord" message
WHERE message."signatureId" IS NOT NULL
AND message."queuedAt" >= ${day.startAt}
AND message."queuedAt" < ${day.endAt}
`)]);
const signatureIds = summaries.map((item) => item.signatureId);
const drainageBreakdowns = signatureIds.length === 0
? []
@@ -486,7 +494,7 @@ async signatureQuality(query: SignatureQualityQuery) {
) failed_receipt ON TRUE
WHERE message."signatureId" IN (${Prisma.join(signatureIds)})
AND submit."submitStatus" IN ('accepted', 'rejected', 'timeout')
AND COALESCE(submit."submittedAt", submit."createdAt") >= ${day.startAt}
AND COALESCE(submit."submittedAt", submit."createdAt") >= ${thirtyDayStartAt}
AND COALESCE(submit."submittedAt", submit."createdAt") < ${day.endAt}
), classified AS (
SELECT
@@ -564,11 +572,54 @@ async signatureQuality(query: SignatureQualityQuery) {
))::integer AS "averageArrivalMs"
FROM "SmsMessageRecord" message
WHERE message."signatureId" IN (${Prisma.join(signatureIds)})
AND message."queuedAt" >= ${day.startAt}
AND message."queuedAt" >= ${thirtyDayStartAt}
AND message."queuedAt" < ${day.endAt}
GROUP BY message."signatureId", COALESCE(NULLIF(message.carrier, ''), 'unknown')
ORDER BY message."signatureId", COUNT(*) DESC, carrier
`);
const dailyBreakdowns = signatureIds.length === 0
? []
: await this.prisma.$queryRaw<Array<{
signatureId: string;
channelId: string;
channelName: string;
carrier: string;
date: string;
businessMessageCount: number;
deliveredMessageCount: number;
deliveryRate: number;
}>>(Prisma.sql`
SELECT
message."signatureId" AS "signatureId",
COALESCE(message."channelId", 'unassigned') AS "channelId",
COALESCE(channel.name, '未分配通道') AS "channelName",
COALESCE(NULLIF(message.carrier, ''), 'unknown') AS carrier,
TO_CHAR((message."queuedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD') AS date,
COUNT(*)::integer AS "businessMessageCount",
COUNT(*) FILTER (
WHERE message.status = 'delivered'
OR message."receiptStatus" = 'delivered'
)::integer AS "deliveredMessageCount",
ROUND(
COUNT(*) FILTER (
WHERE message.status = 'delivered'
OR message."receiptStatus" = 'delivered'
) * 100.0 / COUNT(*),
1
)::double precision AS "deliveryRate"
FROM "SmsMessageRecord" message
LEFT JOIN "SmsChannel" channel ON channel.id = message."channelId"
WHERE message."signatureId" IN (${Prisma.join(signatureIds)})
AND message."queuedAt" >= ${thirtyDayStartAt}
AND message."queuedAt" < ${day.endAt}
GROUP BY
message."signatureId",
COALESCE(message."channelId", 'unassigned'),
COALESCE(channel.name, '未分配通道'),
COALESCE(NULLIF(message.carrier, ''), 'unknown'),
TO_CHAR((message."queuedAt" AT TIME ZONE 'UTC') AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM-DD')
ORDER BY message."signatureId", date, "channelId", carrier
`);
const items = summaries.map(({ rowCount: _rowCount, ...summary }) => {
const signatureDrainageBreakdowns = drainageBreakdowns.filter((item) => item.signatureId === summary.signatureId);
const signatureBreakdowns = aggregateChannelCarrierRows(signatureDrainageBreakdowns);
@@ -578,10 +629,12 @@ async signatureQuality(query: SignatureQualityQuery) {
carrierOverview: carrierOverview.filter((item) => item.signatureId === summary.signatureId),
breakdowns: signatureBreakdowns,
drainageBreakdowns: signatureDrainageBreakdowns,
dailyBreakdowns: dailyBreakdowns.filter((item) => item.signatureId === summary.signatureId),
};
});
return {
date: day.key,
selectedDaySignatureCount: selectedDaySignatureRows[0]?.count ?? 0,
items,
total: summaries[0]?.rowCount ?? 0,
page,