103 lines
5.4 KiB
TypeScript
103 lines
5.4 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';
|
|
|
|
|
|
/** 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;
|
|
}
|
|
}
|