feat: integrate analytics and fragment receipt improvements
This commit is contained in:
@@ -107,6 +107,7 @@ async exportMessages(query: MessageQuery) {
|
||||
submitStatus: true,
|
||||
deliveredAt: true,
|
||||
content: true,
|
||||
hasDrainageContent: true,
|
||||
tenant: { select: { name: true } },
|
||||
application: { select: { name: true } },
|
||||
channel: { select: { name: true } },
|
||||
@@ -114,7 +115,7 @@ async exportMessages(query: MessageQuery) {
|
||||
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
const rows = [
|
||||
['消息编号', '企业', '应用', '提交时间', '手机号', '地区', '运营商', '计费条数', '金额', '通道', '状态', '回执时间', '短信内容'],
|
||||
['消息编号', '企业', '应用', '提交时间', '手机号', '地区', '运营商', '计费条数', '金额', '通道', '状态', '是否含引流', '回执时间', '短信内容'],
|
||||
...items.map((item) => [
|
||||
item.messageId,
|
||||
item.tenant?.name ?? '',
|
||||
@@ -127,6 +128,7 @@ async exportMessages(query: MessageQuery) {
|
||||
String(moneyToNumber(item.amountCents)),
|
||||
item.channel?.name ?? '',
|
||||
item.status === 'submit_failed' || ['rejected', 'timeout'].includes(item.submitStatus ?? '') ? 'submit_failed' : item.status,
|
||||
item.hasDrainageContent === true ? '是' : item.hasDrainageContent === false ? '否' : '未检测',
|
||||
item.deliveredAt?.toISOString() ?? '',
|
||||
item.content,
|
||||
]),
|
||||
|
||||
@@ -37,7 +37,7 @@ async statistics(query: { tenantId?: string; groupBy?: string }) {
|
||||
}
|
||||
async sendQuality(date?: string) {
|
||||
const day = qualityBusinessDay(date);
|
||||
const [channels, signatures, summaryRows, applications] = await Promise.all([
|
||||
const [channels, signatureSplits, summaryRows, applications] = await Promise.all([
|
||||
this.prisma.$queryRaw<Array<{
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
@@ -150,7 +150,7 @@ async sendQuality(date?: string) {
|
||||
WITH base AS (
|
||||
SELECT
|
||||
message."signatureId" AS signature_id,
|
||||
(message."drainageInfoId" IS NOT NULL) AS has_drainage,
|
||||
(message."hasDrainageContent" IS TRUE) AS has_drainage,
|
||||
message.status,
|
||||
message."submitStatus" AS submit_status,
|
||||
message."receiptStatus" AS receipt_status,
|
||||
@@ -303,6 +303,8 @@ async sendQuality(date?: string) {
|
||||
ORDER BY total DESC, application.name
|
||||
`),
|
||||
]);
|
||||
const signatures = aggregateSignatureRows(signatureSplits);
|
||||
const drainageSignatures = signatureSplits.filter((item) => item.hasDrainage);
|
||||
const summary = summaryRows[0] ?? {
|
||||
total: 0,
|
||||
successCount: 0,
|
||||
@@ -310,7 +312,7 @@ async sendQuality(date?: string) {
|
||||
failureCount: 0,
|
||||
successRate: 0,
|
||||
};
|
||||
return { date: day.key, summary, channels, signatures, applications };
|
||||
return { date: day.key, summary, channels, signatures, drainageSignatures, applications };
|
||||
}
|
||||
async signatureQuality(query: SignatureQualityQuery) {
|
||||
const day = qualityBusinessDay(query.date);
|
||||
@@ -413,13 +415,14 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
OFFSET ${(page - 1) * pageSize}
|
||||
`);
|
||||
const signatureIds = summaries.map((item) => item.signatureId);
|
||||
const breakdowns = signatureIds.length === 0
|
||||
const drainageBreakdowns = signatureIds.length === 0
|
||||
? []
|
||||
: await this.prisma.$queryRaw<Array<{
|
||||
signatureId: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
carrier: string;
|
||||
drainageState: 'with' | 'without' | 'unknown';
|
||||
total: number;
|
||||
acceptedCount: number;
|
||||
submitFailureCount: number;
|
||||
@@ -435,6 +438,11 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
submit."channelId" AS channel_id,
|
||||
channel.name AS channel_name,
|
||||
COALESCE(NULLIF(message.carrier, ''), 'unknown') AS carrier,
|
||||
CASE
|
||||
WHEN message."hasDrainageContent" IS TRUE THEN 'with'
|
||||
WHEN message."hasDrainageContent" IS FALSE THEN 'without'
|
||||
ELSE 'unknown'
|
||||
END AS drainage_state,
|
||||
submit."submitStatus" AS submit_status,
|
||||
receipt."deliveredAt" AS delivered_at,
|
||||
failed_receipt."failedAt" AS failed_at,
|
||||
@@ -498,6 +506,7 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
channel_id AS "channelId",
|
||||
MAX(channel_name) AS "channelName",
|
||||
carrier,
|
||||
drainage_state AS "drainageState",
|
||||
COUNT(*)::integer AS total,
|
||||
COUNT(*) FILTER (WHERE submit_status = 'accepted')::integer AS "acceptedCount",
|
||||
COUNT(*) FILTER (WHERE delivery_status = 'submit_failed')::integer AS "submitFailureCount",
|
||||
@@ -514,8 +523,8 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
END AS "successRate",
|
||||
ROUND(AVG(arrival_ms) FILTER (WHERE delivery_status = 'success' AND arrival_ms IS NOT NULL))::integer AS "averageArrivalMs"
|
||||
FROM classified
|
||||
GROUP BY signature_id, channel_id, carrier
|
||||
ORDER BY signature_id, COUNT(*) DESC, channel_id, carrier
|
||||
GROUP BY signature_id, channel_id, carrier, drainage_state
|
||||
ORDER BY signature_id, COUNT(*) DESC, channel_id, carrier, drainage_state
|
||||
`);
|
||||
const carrierOverview = signatureIds.length === 0
|
||||
? []
|
||||
@@ -561,12 +570,14 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
ORDER BY message."signatureId", COUNT(*) DESC, carrier
|
||||
`);
|
||||
const items = summaries.map(({ rowCount: _rowCount, ...summary }) => {
|
||||
const signatureBreakdowns = breakdowns.filter((item) => item.signatureId === summary.signatureId);
|
||||
const signatureDrainageBreakdowns = drainageBreakdowns.filter((item) => item.signatureId === summary.signatureId);
|
||||
const signatureBreakdowns = aggregateChannelCarrierRows(signatureDrainageBreakdowns);
|
||||
return {
|
||||
...summary,
|
||||
channelSubmitTotal: signatureBreakdowns.reduce((sum, item) => sum + item.total, 0),
|
||||
carrierOverview: carrierOverview.filter((item) => item.signatureId === summary.signatureId),
|
||||
breakdowns: signatureBreakdowns,
|
||||
drainageBreakdowns: signatureDrainageBreakdowns,
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -578,3 +589,89 @@ async signatureQuality(query: SignatureQualityQuery) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type SignatureSplitRow = {
|
||||
id: string;
|
||||
signatureId: string;
|
||||
signatureName: string;
|
||||
tenantId: string;
|
||||
tenantName: string;
|
||||
hasDrainage: boolean;
|
||||
total: number;
|
||||
acceptedCount: number;
|
||||
submitFailureCount: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
successRate: number;
|
||||
averageArrivalMs: number | null;
|
||||
};
|
||||
|
||||
function aggregateSignatureRows(rows: SignatureSplitRow[]) {
|
||||
const grouped = new Map<string, SignatureSplitRow[]>();
|
||||
rows.forEach((row) => grouped.set(row.signatureId, [...(grouped.get(row.signatureId) ?? []), row]));
|
||||
return [...grouped.values()].map((parts) => {
|
||||
const first = parts[0];
|
||||
const total = parts.reduce((sum, item) => sum + item.total, 0);
|
||||
const acceptedCount = parts.reduce((sum, item) => sum + item.acceptedCount, 0);
|
||||
const successCount = parts.reduce((sum, item) => sum + item.successCount, 0);
|
||||
const arrivalWeight = parts.reduce((sum, item) => sum + (item.averageArrivalMs == null ? 0 : item.successCount), 0);
|
||||
return {
|
||||
...first,
|
||||
id: first.signatureId,
|
||||
hasDrainage: false,
|
||||
total,
|
||||
acceptedCount,
|
||||
submitFailureCount: parts.reduce((sum, item) => sum + item.submitFailureCount, 0),
|
||||
successCount,
|
||||
unknownCount: parts.reduce((sum, item) => sum + item.unknownCount, 0),
|
||||
failureCount: parts.reduce((sum, item) => sum + item.failureCount, 0),
|
||||
successRate: acceptedCount === 0 ? 0 : Math.round(successCount * 1000 / acceptedCount) / 10,
|
||||
averageArrivalMs: arrivalWeight === 0 ? null : Math.round(parts.reduce((sum, item) => sum + (item.averageArrivalMs ?? 0) * item.successCount, 0) / arrivalWeight),
|
||||
};
|
||||
}).sort((left, right) => right.successCount - left.successCount || right.total - left.total || left.signatureName.localeCompare(right.signatureName));
|
||||
}
|
||||
|
||||
type DrainageBreakdownRow = {
|
||||
signatureId: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
carrier: string;
|
||||
drainageState: 'with' | 'without' | 'unknown';
|
||||
total: number;
|
||||
acceptedCount: number;
|
||||
submitFailureCount: number;
|
||||
successCount: number;
|
||||
unknownCount: number;
|
||||
failureCount: number;
|
||||
successRate: number;
|
||||
averageArrivalMs: number | null;
|
||||
};
|
||||
|
||||
function aggregateChannelCarrierRows(rows: DrainageBreakdownRow[]) {
|
||||
const grouped = new Map<string, DrainageBreakdownRow[]>();
|
||||
rows.forEach((row) => {
|
||||
const key = `${row.signatureId}\u0000${row.channelId}\u0000${row.carrier}`;
|
||||
grouped.set(key, [...(grouped.get(key) ?? []), row]);
|
||||
});
|
||||
return [...grouped.values()].map((parts) => {
|
||||
const first = parts[0];
|
||||
const acceptedCount = parts.reduce((sum, item) => sum + item.acceptedCount, 0);
|
||||
const successCount = parts.reduce((sum, item) => sum + item.successCount, 0);
|
||||
const arrivalWeight = parts.reduce((sum, item) => sum + (item.averageArrivalMs == null ? 0 : item.successCount), 0);
|
||||
return {
|
||||
signatureId: first.signatureId,
|
||||
channelId: first.channelId,
|
||||
channelName: first.channelName,
|
||||
carrier: first.carrier,
|
||||
total: parts.reduce((sum, item) => sum + item.total, 0),
|
||||
acceptedCount,
|
||||
submitFailureCount: parts.reduce((sum, item) => sum + item.submitFailureCount, 0),
|
||||
successCount,
|
||||
unknownCount: parts.reduce((sum, item) => sum + item.unknownCount, 0),
|
||||
failureCount: parts.reduce((sum, item) => sum + item.failureCount, 0),
|
||||
successRate: acceptedCount === 0 ? 0 : Math.round(successCount * 1000 / acceptedCount) / 10,
|
||||
averageArrivalMs: arrivalWeight === 0 ? null : Math.round(parts.reduce((sum, item) => sum + (item.averageArrivalMs ?? 0) * item.successCount, 0) / arrivalWeight),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user