121 lines
6.2 KiB
TypeScript
121 lines
6.2 KiB
TypeScript
import { BadRequestException, Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
import { Queue } from 'bullmq';
|
|
import IORedis from 'ioredis';
|
|
import { Prisma } from '@prisma/client';
|
|
import { randomUUID } from 'crypto';
|
|
import { assertMoneyUnits, moneyToNumber } from '../common/money';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import type { CreateChannelDto, UpdateChannelDto, CreateChannelGroupDto, CreateChannelGroupItemDto, UpdateChannelGroupDto, CreateRouteRuleDto, CreateReportFieldDto, ReplaceReportFieldsDto, CreateReportMaterialDto, CreateReportTaskDto, ChangeReportTaskStatusesDto, CreateReportExportDto, CreateReceiptImportDto, UpsertConnectionStateDto, ChangeChannelStatusDto, CopyChannelDto, TestChannelDto } from './channels.contracts';
|
|
import { GATEWAY_CONNECTION_QUEUE, GATEWAY_SUBMIT_QUEUE, GATEWAY_SUBMIT_STREAM, DEFAULT_GATEWAY_CONTROL_URL, DEFAULT_CHANNEL_CONNECTION_ID, DEFAULT_CONNECTING_TIMEOUT_MS, DEFAULT_CONNECTING_TIMEOUT_SCAN_MS, DEFAULT_GATEWAY_STARTUP_RECONNECT_DELAY_MS, DEFAULT_GATEWAY_RECONCILE_INTERVAL_MS, DEFAULT_GATEWAY_CONTROL_TIMEOUT_MS, DEFAULT_HEARTBEAT_INTERVAL_SECONDS, DEFAULT_HEARTBEAT_MISS_THRESHOLD, HEARTBEAT_AUDIT_INTERVAL_MS, CONNECTING_TIMEOUT_ERROR, DEFAULT_CMPP_VERSION, normalizeTestPhones, normalizeTestContent, calculateBillingUnits, buildChannelTestSubmitCommand, getConfigValue, getStringConfigValue, normalizeConnectionAction, normalizeCmppVersion, normalizeGatewayConnectionStatus, defaultChannelConnectionId, getDesiredConnections, ChannelConnectionSettings, getRuntimeConfigInteger, channelConnectionSettingsChanged, channelGroupAuditSnapshot, normalizeChannelRuntimeConfig, normalizeCmppServiceId, normalizeChannelRateLimit, normalizeExtensionDigits, getPositiveRuntimeInteger, bullmqConnection, getPositiveIntegerEnv, parseReceiptContent, splitReceiptLine, stripReceiptCell, findReceiptStatusIndex, normalizeReceiptStatus, deriveReceiptStatus, ChannelReportDeliveryRow, summarizeChannelReportDelivery, sumReportDelivery, percentage, latestDate, currentShanghaiDayRange, normalizeRetryTimeLimitMinutes, normalizeSpreadsheetSize, normalizeBusinessCarrier, normalizeChannelCarrier, isChannelCarrierCompatible, normalizeRegion, isRegionCompatible, validateGroupItems, normalizeReportType, summarizeReportStatuses, normalizeLinkEvent } from './channels.helpers';
|
|
import { ChannelConnectionService } from './channel-connection.service';
|
|
import { detectDrainageContent } from '../send-chain/drainage-content-detection';
|
|
|
|
/** R5 channel domain service composed behind ChannelsService. */
|
|
export class ChannelTestService {
|
|
constructor(private readonly prisma: PrismaService, private readonly connection: ChannelConnectionService) {}
|
|
|
|
async testChannel(channelId: string, data: TestChannelDto = {}) {
|
|
const phoneNumbers = normalizeTestPhones(data);
|
|
const content = normalizeTestContent(data.content);
|
|
const channel = await this.prisma.smsChannel.findUnique({
|
|
where: { id: channelId },
|
|
include: { connectionStates: true },
|
|
});
|
|
if (!channel) {
|
|
throw new NotFoundException('Channel not found');
|
|
}
|
|
if (channel.status !== 'active') {
|
|
throw new BadRequestException('通道未启用,不能发送测试短信');
|
|
}
|
|
const connectedState = channel.connectionStates.find((state) =>
|
|
normalizeGatewayConnectionStatus(state.status) === 'connected' && (state.currentConnections ?? 0) > 0,
|
|
);
|
|
if (!connectedState) {
|
|
throw new BadRequestException('通道当前没有可用 CMPP 连接,请先连接成功后再测试发送');
|
|
}
|
|
|
|
const createdAt = new Date();
|
|
const testNo = `CHTEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
const drainageDetection = await detectDrainageContent(this.prisma, content);
|
|
const results = [];
|
|
for (const [index, phoneNumber] of phoneNumbers.entries()) {
|
|
const messageId = `MSG-TEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
const submitId = `SUB-TEST-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
const session = await this.prisma.cmppSubmitSession.upsert({
|
|
where: { sessionNo: `OPEN-${channel.id}` },
|
|
update: { submitTotal: { increment: 1 } },
|
|
create: { channelId: channel.id, sessionNo: `OPEN-${channel.id}`, submitTotal: 1 },
|
|
});
|
|
const messageRecord = await this.prisma.smsMessageRecord.create({
|
|
data: {
|
|
messageId,
|
|
phoneNumber,
|
|
content,
|
|
...drainageDetection,
|
|
billingUnits: calculateBillingUnits(content),
|
|
unitPrice: 0,
|
|
amountCents: 0,
|
|
queuePriority: 'normal',
|
|
channelId: channel.id,
|
|
submitId,
|
|
status: 'submit_queued',
|
|
submitStatus: 'queued',
|
|
},
|
|
});
|
|
await this.prisma.smsSubmitRecord.create({
|
|
data: {
|
|
messageRecordId: messageRecord.id,
|
|
channelId: channel.id,
|
|
sessionId: session.id,
|
|
submitId,
|
|
submitStatus: 'queued',
|
|
costUnitPrice: channel.unitPrice,
|
|
costAmountCents: moneyToNumber(channel.unitPrice) * messageRecord.billingUnits,
|
|
},
|
|
});
|
|
const command = buildChannelTestSubmitCommand({
|
|
channel,
|
|
content,
|
|
phoneNumber,
|
|
messageId,
|
|
submitId,
|
|
testNo,
|
|
attempt: index,
|
|
accessNo: data.accessNo,
|
|
});
|
|
await this.connection.getGatewaySubmitQueue().add('submit-command', command);
|
|
const streamMessageId = await this.connection.publishGatewaySubmitCommand(command);
|
|
results.push({
|
|
phoneNumber,
|
|
messageRecordId: messageRecord.id,
|
|
submitId,
|
|
streamMessageId,
|
|
});
|
|
}
|
|
|
|
await this.prisma.operationLog.create({
|
|
data: {
|
|
userId: data.operatorId,
|
|
action: 'sms_channel.test_submit',
|
|
resource: 'sms_channel',
|
|
resourceId: channel.id,
|
|
detail: {
|
|
testNo,
|
|
phoneTotal: phoneNumbers.length,
|
|
messageRecordIds: results.map((item) => item.messageRecordId),
|
|
connectionId: connectedState.connectionId,
|
|
} as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
|
|
return {
|
|
channelId,
|
|
status: 'submit_queued',
|
|
testNo,
|
|
submitted: results.length,
|
|
messages: results,
|
|
queuedAt: createdAt,
|
|
};
|
|
}
|
|
}
|