fix: connect operations pages to real backend

This commit is contained in:
hectorzhao
2026-07-02 15:16:34 +08:00
parent ab421cf8a7
commit 321cf716f2
22 changed files with 1255 additions and 429 deletions
+145 -3
View File
@@ -57,16 +57,56 @@ export interface TemplateListQuery {
keyword?: string;
}
export interface ApplicationListQuery {
tenantId?: string;
keyword?: string;
includeConnections?: boolean;
}
@Injectable()
export class SmsConfigService {
constructor(private readonly prisma: PrismaService) {}
listApplications(tenantId?: string) {
async listApplications(queryOrTenantId?: string | ApplicationListQuery) {
const query = typeof queryOrTenantId === 'string' ? { tenantId: queryOrTenantId } : queryOrTenantId ?? {};
return this.prisma.smsApplication.findMany({
where: tenantId ? { tenantId } : undefined,
include: { ipAllowlist: true },
where: {
tenantId: query.tenantId,
OR: query.keyword ? [
{ name: { contains: query.keyword } },
{ tenant: { name: { contains: query.keyword } } },
] : undefined,
},
include: {
tenant: true,
ipAllowlist: true,
messageRecords: { where: { queuedAt: { gte: startOfToday() } }, take: 1000 },
},
orderBy: { createdAt: 'desc' },
take: 100,
}).then(async (applications) => {
if (!query.includeConnections) {
return applications;
}
const tenantIds = [...new Set(applications.map((application) => application.tenantId))];
const connections = await this.prisma.cmppConnectionState.findMany({
where: { tenantId: { in: tenantIds } },
include: { channel: true },
orderBy: { updatedAt: 'desc' },
take: 500,
});
return applications.map((application) => {
const appConnections = connections.filter((connection) => connection.tenantId === application.tenantId);
const todayTotal = application.messageRecords.length;
const delivered = application.messageRecords.filter((message) => message.status === 'delivered').length;
return {
...application,
cmppConnections: appConnections,
cmppStatus: normalizeApplicationCmppStatus(appConnections, application.status),
sentToday: todayTotal,
deliveryRate: todayTotal > 0 ? Number(((delivered / todayTotal) * 100).toFixed(1)) : 0,
};
});
});
}
@@ -121,6 +161,89 @@ export class SmsConfigService {
return updated;
}
async listApplicationConnections(applicationId: string) {
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
include: { tenant: true },
});
if (!application) {
throw new NotFoundException('Application not found');
}
const connections = await this.prisma.cmppConnectionState.findMany({
where: { tenantId: application.tenantId },
include: { channel: true },
orderBy: { updatedAt: 'desc' },
take: 100,
});
return {
application,
connections,
summary: {
desiredConnections: connections.reduce((sum, connection) => sum + connection.desiredConnections, 0),
currentConnections: connections.reduce((sum, connection) => sum + connection.currentConnections, 0),
status: normalizeApplicationCmppStatus(connections, application.status),
},
};
}
async getApplicationCmppParams(applicationId: string) {
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
include: { tenant: true },
});
if (!application) {
throw new NotFoundException('Application not found');
}
const channel = await this.prisma.smsChannel.findFirst({
where: { status: { not: 'deleted' } },
orderBy: { createdAt: 'desc' },
});
return {
applicationId: application.id,
applicationName: application.name,
tenantId: application.tenantId,
tenantName: application.tenant.name,
appCode: application.id,
gatewayHost: channel?.gatewayHost ?? '',
gatewayPort: channel?.gatewayPort ?? 0,
enterpriseCode: channel?.enterpriseCode ?? application.tenant.code,
account: channel?.account ?? application.tenant.code,
passwordCipher: channel?.passwordCipher ?? application.secretHash,
srcId: channel?.srcId ?? '',
maxConnections: channel?.config && typeof channel.config === 'object' && 'maxConnections' in channel.config ? Number(channel.config.maxConnections) : 1,
heartbeatSeconds: 30,
windowSize: 16,
protocolVersion: channel?.cmppVersion ?? '3.0',
};
}
async disconnectApplicationConnection(applicationId: string, connectionId: string, data: StatusChangeDto = { status: 'disconnected' }) {
const application = await this.prisma.smsApplication.findUnique({ where: { id: applicationId } });
if (!application) {
throw new NotFoundException('Application not found');
}
const connection = await this.prisma.cmppConnectionState.findFirst({
where: { tenantId: application.tenantId, connectionId },
});
if (!connection) {
throw new NotFoundException('Connection not found');
}
const updated = await this.prisma.cmppConnectionState.update({
where: { channelId_connectionId: { channelId: connection.channelId, connectionId } },
data: {
status: 'disconnected',
currentConnections: 0,
lastDisconnectedAt: new Date(),
lastError: data.reason,
},
});
await this.writeOperationLog(application.tenantId, data.operatorId, 'cmpp_connection.disconnected', 'cmpp_connection', `${connection.channelId}:${connectionId}`, {
applicationId,
reason: data.reason,
});
return updated;
}
listSignatures(tenantId?: string) {
return this.prisma.smsSignature.findMany({
where: tenantId ? { tenantId } : undefined,
@@ -407,3 +530,22 @@ function inferTemplateVariables(content: string): TemplateVariableInput[] {
const matches = content.match(/\$\{[a-zA-Z0-9_]+\}/g) ?? [];
return [...new Set(matches)].map((match) => ({ name: match.slice(2, -1), required: true }));
}
function startOfToday() {
const date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}
function normalizeApplicationCmppStatus(connections: Array<{ status: string; currentConnections: number }>, applicationStatus: string) {
if (applicationStatus !== 'active') {
return 'inactive';
}
if (connections.some((connection) => ['online', 'connected', 'open'].includes(connection.status) && connection.currentConnections > 0)) {
return 'connected';
}
if (connections.some((connection) => ['auth_failed', 'heartbeat_timeout', 'reconnecting'].includes(connection.status))) {
return 'degraded';
}
return 'disconnected';
}