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'; /** R5 channel domain service composed behind ChannelsService. */ export class ChannelCopyService { constructor(private readonly prisma: PrismaService) {} async copyChannel(channelId: string, data: CopyChannelDto = {}) { const source = await this.prisma.smsChannel.findUnique({ where: { id: channelId }, include: { reportFields: true }, }); if (!source) { throw new NotFoundException('Channel not found'); } const suffix = Date.now().toString(36).toUpperCase(); const nextName = data.name ?? `${source.name}副本`; const nextCode = data.code ?? `${source.code}-COPY-${suffix}`; const copied = await this.prisma.$transaction(async (tx) => { const nextChannel = await tx.smsChannel.create({ data: { code: nextCode, name: nextName, carrier: source.carrier, carriers: source.carriers, protocol: source.protocol, gatewayHost: source.gatewayHost, gatewayPort: source.gatewayPort, enterpriseCode: source.enterpriseCode, account: source.account, passwordCipher: source.passwordCipher, srcId: source.srcId, sendRegion: source.sendRegion, cmppVersion: source.cmppVersion, rateLimitPerSecond: source.rateLimitPerSecond, unitPrice: source.unitPrice, status: 'disabled', config: source.config as Prisma.InputJsonValue | undefined, reportFields: { create: source.reportFields.map((field) => ({ drainageFieldId: field.drainageFieldId, reportType: field.reportType, code: field.code, name: field.name, fieldType: field.fieldType, required: field.required, description: field.description, sortOrder: field.sortOrder, status: field.status, })), }, }, include: { reportFields: true }, }); const reportMaterials = await tx.signatureReportMaterial.findMany({ where: { channelId } }); if (reportMaterials.length > 0) { await tx.signatureReportMaterial.createMany({ data: reportMaterials.map((material) => ({ signatureId: material.signatureId, channelId: nextChannel.id, fieldCode: material.fieldCode, fieldValue: material.fieldValue, fileObjectId: material.fileObjectId, })), skipDuplicates: true, }); } await tx.operationLog.create({ data: { userId: data.operatorId, action: 'sms_channel.copy', resource: 'sms_channel', resourceId: nextChannel.id, detail: { sourceChannelId: source.id, sourceCode: source.code, sourceStatus: source.status, copiedStatus: 'disabled', copiedReportFields: source.reportFields.length, copiedReportMaterials: reportMaterials.length, } as Prisma.InputJsonValue, }, }); return nextChannel; }); return copied; } }