feat: add signature quality period trends
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user