feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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';
|
||||
|
||||
// R2 messages query domain. Method bodies are preserved byte-for-byte from the facade baseline.
|
||||
export class OperationsMessageQueries {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
listBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
return this.prisma.smsBatchTask.findMany({
|
||||
where: { tenantId: query.tenantId, status: query.status, sourceType: 'client' },
|
||||
include: { apiRequests: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async listClientBatchTasks(query: { tenantId?: string; status?: string }) {
|
||||
const items = await this.listBatchTasks(query);
|
||||
return items.map(clientBatchTaskView);
|
||||
}
|
||||
listMessages(query: MessageQuery) {
|
||||
return this.prisma.smsMessageRecord.findMany({
|
||||
where: messageWhere(query),
|
||||
include: {
|
||||
tenant: true,
|
||||
application: true,
|
||||
channel: true,
|
||||
submitRecords: { include: { channel: true, channelGroup: true } },
|
||||
receiptRecords: { include: { channel: true } },
|
||||
downstreamDeliveries: {
|
||||
where: { deliveryType: 'receipt' },
|
||||
select: { id: true, deliveryType: true, status: true, deliveredAt: true, lastError: true },
|
||||
},
|
||||
},
|
||||
orderBy: { queuedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
async listMessagesPage(query: MessageQuery) {
|
||||
const page = Math.max(1, Math.floor(Number(query.page) || 1));
|
||||
const pageSize = Math.min(100, Math.max(1, Math.floor(Number(query.pageSize) || 25)));
|
||||
const where = messageWhere(query);
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.smsMessageRecord.findMany({
|
||||
where,
|
||||
include: {
|
||||
tenant: { select: { id: true, name: true } },
|
||||
application: { select: { id: true, name: true } },
|
||||
channel: { select: { id: true, name: true, srcId: true } },
|
||||
submitRecords: {
|
||||
select: {
|
||||
id: true,
|
||||
submitId: true,
|
||||
channelId: true,
|
||||
channelGroupId: true,
|
||||
channelGroupName: true,
|
||||
gatewayMessageId: true,
|
||||
submitStatus: true,
|
||||
submittedAt: true,
|
||||
createdAt: true,
|
||||
channel: { select: { id: true, name: true } },
|
||||
channelGroup: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
receiptRecords: {
|
||||
select: {
|
||||
id: true,
|
||||
messageId: true,
|
||||
gatewayMessageId: true,
|
||||
receiptStatus: true,
|
||||
rawStatus: true,
|
||||
errorCode: true,
|
||||
errorMessage: true,
|
||||
deliveredAt: true,
|
||||
createdAt: true,
|
||||
channelId: true,
|
||||
channel: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
downstreamDeliveries: {
|
||||
where: { deliveryType: 'receipt' },
|
||||
select: { id: true, deliveryType: true, status: true, deliveredAt: true, lastError: true },
|
||||
},
|
||||
},
|
||||
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.smsMessageRecord.count({ where }),
|
||||
]);
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
async exportMessages(query: MessageQuery) {
|
||||
const items = await this.prisma.smsMessageRecord.findMany({
|
||||
where: messageWhere(query),
|
||||
select: {
|
||||
messageId: true,
|
||||
queuedAt: true,
|
||||
phoneNumber: true,
|
||||
province: true,
|
||||
carrier: true,
|
||||
billingUnits: true,
|
||||
amountCents: true,
|
||||
status: true,
|
||||
submitStatus: true,
|
||||
deliveredAt: true,
|
||||
content: true,
|
||||
tenant: { select: { name: true } },
|
||||
application: { select: { name: true } },
|
||||
channel: { select: { name: true } },
|
||||
},
|
||||
orderBy: [{ queuedAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
const rows = [
|
||||
['消息编号', '企业', '应用', '提交时间', '手机号', '地区', '运营商', '计费条数', '金额', '通道', '状态', '回执时间', '短信内容'],
|
||||
...items.map((item) => [
|
||||
item.messageId,
|
||||
item.tenant?.name ?? '',
|
||||
item.application?.name ?? '',
|
||||
item.queuedAt.toISOString(),
|
||||
item.phoneNumber,
|
||||
item.province ?? '',
|
||||
item.carrier ?? '',
|
||||
String(item.billingUnits),
|
||||
String(moneyToNumber(item.amountCents)),
|
||||
item.channel?.name ?? '',
|
||||
item.status === 'submit_failed' || ['rejected', 'timeout'].includes(item.submitStatus ?? '') ? 'submit_failed' : item.status,
|
||||
item.deliveredAt?.toISOString() ?? '',
|
||||
item.content,
|
||||
]),
|
||||
];
|
||||
return {
|
||||
fileName: `sms-records-${formatExportTimestamp(new Date())}.csv`,
|
||||
content: rows.map((row) => row.map((cell) => escapeCsvCell(String(cell))).join(',')).join('\n'),
|
||||
};
|
||||
}
|
||||
async listClientMessages(query: MessageQuery) {
|
||||
const items = await this.listMessages(query);
|
||||
return items.map(clientMessageView);
|
||||
}
|
||||
async listClientMessagesPage(query: MessageQuery) {
|
||||
const result = await this.listMessagesPage(query);
|
||||
return { ...result, items: result.items.map(clientMessageView) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user