150 lines
6.5 KiB
TypeScript
150 lines
6.5 KiB
TypeScript
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,
|
|
hasDrainageContent: 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.hasDrainageContent === true ? '是' : item.hasDrainageContent === false ? '否' : '未检测',
|
|
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) };
|
|
}
|
|
}
|