744 lines
23 KiB
TypeScript
744 lines
23 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
export interface CreateChannelDto {
|
|
code: string;
|
|
name: string;
|
|
carrier?: string;
|
|
sendRegion?: string;
|
|
protocol?: string;
|
|
gatewayHost: string;
|
|
gatewayPort: number;
|
|
enterpriseCode?: string;
|
|
account: string;
|
|
passwordCipher: string;
|
|
srcId: string;
|
|
cmppVersion?: string;
|
|
rateLimitPerSecond?: number;
|
|
unitPrice?: number;
|
|
status?: string;
|
|
config?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface CreateChannelGroupDto {
|
|
code: string;
|
|
name: string;
|
|
carrier: string;
|
|
description?: string;
|
|
status?: string;
|
|
retryEnabled?: boolean;
|
|
retryTimeLimitHours?: number;
|
|
}
|
|
|
|
export interface CreateChannelGroupItemDto {
|
|
groupId: string;
|
|
channelId: string;
|
|
carrier?: string;
|
|
province?: string;
|
|
priority?: number;
|
|
weight?: number;
|
|
isBackup?: boolean;
|
|
rateLimitPerSecond?: number;
|
|
}
|
|
|
|
export interface CreateRouteRuleDto {
|
|
tenantId?: string;
|
|
applicationId?: string;
|
|
groupId: string;
|
|
channelId?: string;
|
|
carrier?: string;
|
|
province?: string;
|
|
priority?: number;
|
|
status?: string;
|
|
}
|
|
|
|
export interface CreateReportFieldDto {
|
|
channelId: string;
|
|
code: string;
|
|
name: string;
|
|
fieldType: string;
|
|
required?: boolean;
|
|
description?: string;
|
|
sortOrder?: number;
|
|
status?: string;
|
|
}
|
|
|
|
export interface CreateReportMaterialDto {
|
|
signatureId: string;
|
|
channelId: string;
|
|
fieldCode: string;
|
|
fieldValue?: string;
|
|
fileObjectId?: string;
|
|
}
|
|
|
|
export interface CreateReportTaskDto {
|
|
tenantId: string;
|
|
signatureId: string;
|
|
channelId: string;
|
|
createdById?: string;
|
|
}
|
|
|
|
export interface CreateReportExportDto {
|
|
fileObjectId?: string;
|
|
fileName: string;
|
|
rowCount?: number;
|
|
}
|
|
|
|
export interface CreateReceiptImportDto {
|
|
fileObjectId?: string;
|
|
fileName: string;
|
|
rowCount?: number;
|
|
successCount?: number;
|
|
failedCount?: number;
|
|
statusAfter?: string;
|
|
reason?: string;
|
|
result?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface UpsertConnectionStateDto {
|
|
tenantId?: string;
|
|
channelId: string;
|
|
connectionId: string;
|
|
status: string;
|
|
desiredConnections?: number;
|
|
currentConnections?: number;
|
|
lastConnectedAt?: string;
|
|
lastDisconnectedAt?: string;
|
|
lastHeartbeatAt?: string;
|
|
reconnectCount?: number;
|
|
lastError?: string;
|
|
}
|
|
|
|
export interface ChangeChannelStatusDto {
|
|
status: string;
|
|
operatorId?: string;
|
|
reason?: string;
|
|
}
|
|
|
|
export interface CopyChannelDto {
|
|
name?: string;
|
|
code?: string;
|
|
operatorId?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ChannelsService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
listChannels() {
|
|
return this.prisma.smsChannel.findMany({ orderBy: { createdAt: 'desc' }, take: 100 });
|
|
}
|
|
|
|
createChannel(data: CreateChannelDto) {
|
|
const missingFields = ['code', 'name', 'gatewayHost', 'gatewayPort', 'account', 'passwordCipher', 'srcId'].filter((field) => {
|
|
const value = data[field as keyof CreateChannelDto];
|
|
return value === undefined || value === null || value === '';
|
|
});
|
|
if (missingFields.length > 0) {
|
|
throw new BadRequestException(`Missing required channel fields: ${missingFields.join(', ')}`);
|
|
}
|
|
const gatewayPort = Number(data.gatewayPort);
|
|
if (!Number.isInteger(gatewayPort) || gatewayPort <= 0 || gatewayPort > 65535) {
|
|
throw new BadRequestException('gatewayPort must be an integer between 1 and 65535');
|
|
}
|
|
return this.prisma.smsChannel.create({
|
|
data: {
|
|
code: data.code,
|
|
name: data.name,
|
|
carrier: data.carrier,
|
|
sendRegion: data.sendRegion ?? '全国',
|
|
protocol: data.protocol ?? 'CMPP',
|
|
gatewayHost: data.gatewayHost,
|
|
gatewayPort,
|
|
enterpriseCode: data.enterpriseCode,
|
|
account: data.account,
|
|
passwordCipher: data.passwordCipher,
|
|
srcId: data.srcId,
|
|
cmppVersion: data.cmppVersion ?? '3.0',
|
|
rateLimitPerSecond: data.rateLimitPerSecond ?? 100,
|
|
unitPrice: data.unitPrice ?? 0,
|
|
status: data.status ?? 'active',
|
|
config: data.config as Prisma.InputJsonValue | undefined,
|
|
},
|
|
});
|
|
}
|
|
|
|
async changeChannelStatus(channelId: string, data: ChangeChannelStatusDto) {
|
|
const channel = await this.prisma.smsChannel.findUnique({ where: { id: channelId } });
|
|
if (!channel) {
|
|
throw new NotFoundException('Channel not found');
|
|
}
|
|
const updated = await this.prisma.smsChannel.update({ where: { id: channelId }, data: { status: data.status } });
|
|
await this.prisma.operationLog.create({
|
|
data: {
|
|
userId: data.operatorId,
|
|
action: `sms_channel.${data.status}`,
|
|
resource: 'sms_channel',
|
|
resourceId: channelId,
|
|
detail: {
|
|
statusBefore: channel.status,
|
|
statusAfter: data.status,
|
|
reason: data.reason,
|
|
} as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
return updated;
|
|
}
|
|
|
|
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,
|
|
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: source.status,
|
|
config: source.config as Prisma.InputJsonValue | undefined,
|
|
reportFields: {
|
|
create: source.reportFields.map((field) => ({
|
|
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,
|
|
copiedReportFields: source.reportFields.length,
|
|
copiedReportMaterials: reportMaterials.length,
|
|
} as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
|
|
return nextChannel;
|
|
});
|
|
|
|
return copied;
|
|
}
|
|
|
|
async deleteChannel(channelId: string, data: ChangeChannelStatusDto = { status: 'deleted' }) {
|
|
return this.changeChannelStatus(channelId, { ...data, status: 'deleted' });
|
|
}
|
|
|
|
testChannel(channelId: string) {
|
|
return {
|
|
channelId,
|
|
status: 'queued',
|
|
message: 'Channel test request accepted as a phase-4 placeholder.',
|
|
};
|
|
}
|
|
|
|
listChannelMetrics(channelId: string) {
|
|
return this.prisma.channelHealthMetric.findMany({
|
|
where: { channelId },
|
|
orderBy: { windowStart: 'desc' },
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
listChannelConnections(channelId: string) {
|
|
return this.prisma.cmppConnectionState.findMany({
|
|
where: { channelId },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
async listChannelLinkLogs(channelId: string) {
|
|
const channel = await this.prisma.smsChannel.findUnique({ where: { id: channelId }, select: { id: true } });
|
|
if (!channel) {
|
|
throw new NotFoundException('Channel not found');
|
|
}
|
|
const [connectionStates, logs] = await Promise.all([
|
|
this.prisma.cmppConnectionState.findMany({
|
|
where: { channelId },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 50,
|
|
}),
|
|
this.prisma.operationLog.findMany({
|
|
where: {
|
|
OR: [
|
|
{ resource: 'sms_channel', resourceId: channelId },
|
|
{ resource: 'cmpp_connection', resourceId: { startsWith: `${channelId}:` } },
|
|
],
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 100,
|
|
}),
|
|
]);
|
|
return {
|
|
channelId,
|
|
connectionStates,
|
|
logs: logs.map((log) => ({
|
|
id: log.id,
|
|
time: log.createdAt,
|
|
event: normalizeLinkEvent(log.action),
|
|
action: log.action,
|
|
resourceId: log.resourceId,
|
|
detail: log.detail,
|
|
})),
|
|
};
|
|
}
|
|
|
|
listTenantConnections(tenantId: string) {
|
|
return this.prisma.cmppConnectionState.findMany({
|
|
where: { tenantId },
|
|
include: { channel: true },
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
async upsertConnectionState(data: UpsertConnectionStateDto) {
|
|
const payload = {
|
|
tenantId: data.tenantId,
|
|
status: data.status,
|
|
desiredConnections: data.desiredConnections ?? 1,
|
|
currentConnections: data.currentConnections ?? (data.status === 'online' || data.status === 'connected' ? 1 : 0),
|
|
lastConnectedAt: data.lastConnectedAt ? new Date(data.lastConnectedAt) : undefined,
|
|
lastDisconnectedAt: data.lastDisconnectedAt ? new Date(data.lastDisconnectedAt) : undefined,
|
|
lastHeartbeatAt: data.lastHeartbeatAt ? new Date(data.lastHeartbeatAt) : undefined,
|
|
reconnectCount: data.reconnectCount ?? 0,
|
|
lastError: data.lastError,
|
|
};
|
|
const state = await this.prisma.cmppConnectionState.upsert({
|
|
where: { channelId_connectionId: { channelId: data.channelId, connectionId: data.connectionId } },
|
|
update: payload,
|
|
create: {
|
|
channelId: data.channelId,
|
|
connectionId: data.connectionId,
|
|
...payload,
|
|
},
|
|
});
|
|
await this.prisma.operationLog.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
action: `cmpp_connection.${normalizeConnectionAction(data.status)}`,
|
|
resource: 'cmpp_connection',
|
|
resourceId: `${data.channelId}:${data.connectionId}`,
|
|
detail: {
|
|
status: data.status,
|
|
desiredConnections: state.desiredConnections,
|
|
currentConnections: state.currentConnections,
|
|
lastError: state.lastError,
|
|
} as Prisma.InputJsonValue,
|
|
},
|
|
});
|
|
return state;
|
|
}
|
|
|
|
listGroups() {
|
|
return this.prisma.smsChannelGroup.findMany({
|
|
include: { items: { include: { channel: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
createGroup(data: CreateChannelGroupDto) {
|
|
const retryTimeLimitHours = data.retryTimeLimitHours ?? 72;
|
|
if (!Number.isInteger(retryTimeLimitHours) || retryTimeLimitHours <= 0 || retryTimeLimitHours > 72) {
|
|
throw new BadRequestException('retryTimeLimitHours must be an integer between 1 and 72');
|
|
}
|
|
const carrier = normalizeBusinessCarrier(data.carrier);
|
|
return this.prisma.smsChannelGroup.create({
|
|
data: {
|
|
code: data.code,
|
|
name: data.name,
|
|
carrier,
|
|
description: data.description,
|
|
status: data.status ?? 'active',
|
|
retryEnabled: data.retryEnabled ?? true,
|
|
retryTimeLimitHours,
|
|
},
|
|
});
|
|
}
|
|
|
|
async addGroupItem(data: CreateChannelGroupItemDto) {
|
|
const group = await this.prisma.smsChannelGroup.findUnique({ where: { id: data.groupId } });
|
|
if (!group) {
|
|
throw new NotFoundException('Channel group not found');
|
|
}
|
|
const groupCarrier = normalizeBusinessCarrier(group.carrier);
|
|
const itemCarrier = data.carrier ? normalizeBusinessCarrier(data.carrier) : groupCarrier;
|
|
if (itemCarrier !== groupCarrier) {
|
|
throw new BadRequestException('Channel group items must use the same carrier as the channel group');
|
|
}
|
|
const channel = await this.prisma.smsChannel.findUnique({ where: { id: data.channelId } });
|
|
if (!channel) {
|
|
throw new NotFoundException('Channel not found');
|
|
}
|
|
if (!isChannelCarrierCompatible(channel.carrier, groupCarrier)) {
|
|
throw new BadRequestException('Channel carrier is not compatible with the channel group carrier');
|
|
}
|
|
if (data.province && !isRegionCompatible(channel.sendRegion, data.province)) {
|
|
throw new BadRequestException('Province route must use a channel with the same sendRegion');
|
|
}
|
|
const existing = await this.prisma.smsChannelGroupItem.findFirst({
|
|
where: { groupId: data.groupId, channelId: data.channelId },
|
|
});
|
|
if (existing) {
|
|
throw new BadRequestException('通道组内不能重复配置同一通道');
|
|
}
|
|
if (data.province) {
|
|
const existingProvince = await this.prisma.smsChannelGroupItem.findFirst({
|
|
where: { groupId: data.groupId, province: data.province },
|
|
});
|
|
if (existingProvince) {
|
|
throw new BadRequestException('同一通道组内同一省份只能配置一个通道');
|
|
}
|
|
} else {
|
|
const existingPriority = await this.prisma.smsChannelGroupItem.findFirst({
|
|
where: { groupId: data.groupId, province: null, priority: data.priority ?? 100 },
|
|
});
|
|
if (existingPriority) {
|
|
throw new BadRequestException('同一通道组内全国通道优先级不能重复');
|
|
}
|
|
}
|
|
return this.prisma.smsChannelGroupItem.create({
|
|
data: {
|
|
groupId: data.groupId,
|
|
channelId: data.channelId,
|
|
carrier: itemCarrier,
|
|
province: data.province,
|
|
priority: data.priority ?? 100,
|
|
weight: data.weight ?? 1,
|
|
isBackup: data.isBackup ?? false,
|
|
rateLimitPerSecond: data.rateLimitPerSecond,
|
|
},
|
|
});
|
|
}
|
|
|
|
listRouteRules() {
|
|
return this.prisma.channelRouteRule.findMany({
|
|
include: { group: true, channel: true },
|
|
orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }],
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
async createRouteRule(data: CreateRouteRuleDto) {
|
|
if (!data.applicationId) {
|
|
throw new BadRequestException('applicationId is required for channel group routing');
|
|
}
|
|
if (!data.carrier) {
|
|
throw new BadRequestException('carrier is required for application channel group routing');
|
|
}
|
|
const carrier = normalizeBusinessCarrier(data.carrier);
|
|
if (data.channelId) {
|
|
throw new BadRequestException('Route rules can only bind channel groups, not single channels');
|
|
}
|
|
if (data.province) {
|
|
throw new BadRequestException('Province routing must be configured inside the channel group');
|
|
}
|
|
const group = await this.prisma.smsChannelGroup.findUnique({ where: { id: data.groupId } });
|
|
if (!group) {
|
|
throw new NotFoundException('Channel group not found');
|
|
}
|
|
if (normalizeBusinessCarrier(group.carrier) !== carrier) {
|
|
throw new BadRequestException('Route rule carrier must match the channel group carrier');
|
|
}
|
|
return this.prisma.channelRouteRule.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
applicationId: data.applicationId,
|
|
groupId: data.groupId,
|
|
channelId: undefined,
|
|
carrier,
|
|
province: undefined,
|
|
priority: data.priority ?? 100,
|
|
status: data.status ?? 'active',
|
|
},
|
|
});
|
|
}
|
|
|
|
listReportFields(channelId?: string) {
|
|
return this.prisma.channelReportField.findMany({
|
|
where: channelId ? { channelId } : undefined,
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
|
|
take: 200,
|
|
});
|
|
}
|
|
|
|
createReportField(data: CreateReportFieldDto) {
|
|
return this.prisma.channelReportField.create({
|
|
data: {
|
|
channelId: data.channelId,
|
|
code: data.code,
|
|
name: data.name,
|
|
fieldType: data.fieldType,
|
|
required: data.required ?? false,
|
|
description: data.description,
|
|
sortOrder: data.sortOrder ?? 100,
|
|
status: data.status ?? 'active',
|
|
},
|
|
});
|
|
}
|
|
|
|
listReportMaterials(signatureId?: string, channelId?: string) {
|
|
return this.prisma.signatureReportMaterial.findMany({
|
|
where: {
|
|
signatureId,
|
|
channelId,
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 200,
|
|
});
|
|
}
|
|
|
|
upsertReportMaterial(data: CreateReportMaterialDto) {
|
|
return this.prisma.signatureReportMaterial.upsert({
|
|
where: {
|
|
signatureId_channelId_fieldCode: {
|
|
signatureId: data.signatureId,
|
|
channelId: data.channelId,
|
|
fieldCode: data.fieldCode,
|
|
},
|
|
},
|
|
update: {
|
|
fieldValue: data.fieldValue,
|
|
fileObjectId: data.fileObjectId,
|
|
},
|
|
create: {
|
|
signatureId: data.signatureId,
|
|
channelId: data.channelId,
|
|
fieldCode: data.fieldCode,
|
|
fieldValue: data.fieldValue,
|
|
fileObjectId: data.fileObjectId,
|
|
},
|
|
});
|
|
}
|
|
|
|
listReportTasks(tenantId?: string, status?: string) {
|
|
return this.prisma.channelSignatureReportTask.findMany({
|
|
where: { tenantId, status },
|
|
include: { signature: true, channel: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 100,
|
|
});
|
|
}
|
|
|
|
async createReportTask(data: CreateReportTaskDto) {
|
|
const task = await this.prisma.channelSignatureReportTask.create({
|
|
data: {
|
|
tenantId: data.tenantId,
|
|
signatureId: data.signatureId,
|
|
channelId: data.channelId,
|
|
createdById: data.createdById,
|
|
status: 'pending',
|
|
},
|
|
});
|
|
await this.recordReportTask(task.id, task.channelId, 'create', undefined, 'pending');
|
|
return task;
|
|
}
|
|
|
|
async createReportExport(taskId: string, data: CreateReportExportDto) {
|
|
const task = await this.getReportTaskOrThrow(taskId);
|
|
const exported = await this.prisma.reportExportFile.create({
|
|
data: {
|
|
taskId,
|
|
fileObjectId: data.fileObjectId,
|
|
fileName: data.fileName,
|
|
rowCount: data.rowCount ?? 0,
|
|
},
|
|
});
|
|
await this.updateReportTaskStatus(taskId, task.channelId, task.status, 'exporting', 'export');
|
|
return exported;
|
|
}
|
|
|
|
async importReportReceipt(taskId: string, data: CreateReceiptImportDto) {
|
|
const task = await this.getReportTaskOrThrow(taskId);
|
|
const statusAfter = data.statusAfter ?? (data.failedCount && data.failedCount > 0 ? 'rejected' : 'approved');
|
|
const imported = await this.prisma.reportReceiptImport.create({
|
|
data: {
|
|
taskId,
|
|
fileObjectId: data.fileObjectId,
|
|
fileName: data.fileName,
|
|
rowCount: data.rowCount ?? 0,
|
|
successCount: data.successCount ?? 0,
|
|
failedCount: data.failedCount ?? 0,
|
|
status: 'imported',
|
|
result: data.result as Prisma.InputJsonValue | undefined,
|
|
},
|
|
});
|
|
await this.updateReportTaskStatus(taskId, task.channelId, task.status, statusAfter, 'receipt_import', data.reason);
|
|
await this.prisma.smsSignature.update({
|
|
where: { id: task.signatureId },
|
|
data: { reportStatus: statusAfter },
|
|
});
|
|
return imported;
|
|
}
|
|
|
|
listReportRecords(taskId?: string, channelId?: string) {
|
|
return this.prisma.channelSignatureReportRecord.findMany({
|
|
where: { taskId, channelId },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 200,
|
|
});
|
|
}
|
|
|
|
private async getReportTaskOrThrow(taskId: string) {
|
|
const task = await this.prisma.channelSignatureReportTask.findUnique({ where: { id: taskId } });
|
|
if (!task) {
|
|
throw new NotFoundException('Report task not found');
|
|
}
|
|
return task;
|
|
}
|
|
|
|
private async updateReportTaskStatus(
|
|
taskId: string,
|
|
channelId: string,
|
|
statusBefore: string,
|
|
statusAfter: string,
|
|
action: string,
|
|
reason?: string,
|
|
) {
|
|
await this.prisma.channelSignatureReportTask.update({
|
|
where: { id: taskId },
|
|
data: { status: statusAfter, reason },
|
|
});
|
|
await this.recordReportTask(taskId, channelId, action, statusBefore, statusAfter, reason);
|
|
}
|
|
|
|
private recordReportTask(
|
|
taskId: string,
|
|
channelId: string,
|
|
action: string,
|
|
statusBefore: string | undefined,
|
|
statusAfter: string,
|
|
reason?: string,
|
|
) {
|
|
return this.prisma.channelSignatureReportRecord.create({
|
|
data: {
|
|
taskId,
|
|
channelId,
|
|
action,
|
|
statusBefore,
|
|
statusAfter,
|
|
reason,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function normalizeConnectionAction(status: string) {
|
|
const normalized = status.toLowerCase();
|
|
if (['online', 'connected', 'open'].includes(normalized)) {
|
|
return 'connected';
|
|
}
|
|
if (['heartbeat', 'active_test'].includes(normalized)) {
|
|
return 'heartbeat';
|
|
}
|
|
if (['reconnecting', 'reconnect'].includes(normalized)) {
|
|
return 'reconnecting';
|
|
}
|
|
if (['offline', 'closed', 'disconnected'].includes(normalized)) {
|
|
return 'disconnected';
|
|
}
|
|
return 'updated';
|
|
}
|
|
|
|
function normalizeBusinessCarrier(carrier?: string | null) {
|
|
const normalized = normalizeChannelCarrier(carrier);
|
|
if (!['mobile', 'unicom', 'telecom'].includes(normalized)) {
|
|
throw new BadRequestException('carrier must be mobile, unicom, or telecom');
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeChannelCarrier(carrier?: string | null) {
|
|
const value = String(carrier ?? '').trim().toLowerCase();
|
|
if (['mobile', 'cmcc', '移动', '中国移动'].includes(value)) return 'mobile';
|
|
if (['unicom', 'cucc', '联通', '中国联通'].includes(value)) return 'unicom';
|
|
if (['telecom', 'ctcc', '电信', '中国电信'].includes(value)) return 'telecom';
|
|
if (['all', 'tri', '三网', '全网'].includes(value)) return 'all';
|
|
return value;
|
|
}
|
|
|
|
function isChannelCarrierCompatible(channelCarrier: string | null | undefined, groupCarrier: string) {
|
|
const normalized = normalizeChannelCarrier(channelCarrier);
|
|
return normalized === 'all' || normalized === groupCarrier;
|
|
}
|
|
|
|
function normalizeRegion(region?: string | null) {
|
|
return String(region ?? '').replace(/省|市|自治区|壮族|回族|维吾尔/g, '').trim();
|
|
}
|
|
|
|
function isRegionCompatible(channelRegion: string | null | undefined, itemProvince: string) {
|
|
return normalizeRegion(channelRegion) === normalizeRegion(itemProvince);
|
|
}
|
|
|
|
function normalizeLinkEvent(action: string) {
|
|
if (action.includes('connected')) {
|
|
return '新建';
|
|
}
|
|
if (action.includes('heartbeat')) {
|
|
return '心跳';
|
|
}
|
|
if (action.includes('reconnecting')) {
|
|
return '重连';
|
|
}
|
|
if (action.includes('disconnected')) {
|
|
return '断开';
|
|
}
|
|
if (action.includes('copy')) {
|
|
return '复制';
|
|
}
|
|
if (action.includes('deleted')) {
|
|
return '删除';
|
|
}
|
|
return '更新';
|
|
}
|