fix: harden admin and CMPP delivery workflows
This commit is contained in:
@@ -130,7 +130,8 @@ function createPrismaMock() {
|
||||
findUnique: jest.fn().mockResolvedValue(null),
|
||||
create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'downstream-1', ...data })),
|
||||
update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'downstream-1', ...data })),
|
||||
updateMany: jest.fn().mockResolvedValue({ count: 0 }),
|
||||
delete: jest.fn().mockResolvedValue({ id: 'downstream-1' }),
|
||||
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
|
||||
},
|
||||
smsMessageRecord: {
|
||||
groupBy: jest.fn().mockResolvedValue([
|
||||
@@ -264,7 +265,7 @@ describe('SmsConfigService', () => {
|
||||
tenantId: 'tenant-1',
|
||||
name: '优先应用',
|
||||
cmppAccount: '123456',
|
||||
cmppEnterpriseCode: 'CUSTOM-EC',
|
||||
cmppEnterpriseCode: '123456',
|
||||
secretHash: '1234567890abcdef',
|
||||
cmppMaxConnections: 3,
|
||||
cmppWindowSize: 32,
|
||||
@@ -343,6 +344,7 @@ describe('SmsConfigService', () => {
|
||||
where: { id: 'app-1' },
|
||||
data: expect.objectContaining({
|
||||
name: '新应用',
|
||||
cmppEnterpriseCode: '100001',
|
||||
customerUnitPrice: 300,
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
|
||||
@@ -467,6 +469,22 @@ describe('SmsConfigService', () => {
|
||||
expect(prisma.operationLog.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes a disconnected downstream session instead of retaining connection history', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
prisma.cmppDownstreamConnection.findUnique.mockResolvedValue({ id: 'downstream-1' });
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.recordDownstreamConnectionEvent({
|
||||
account: '100001',
|
||||
connectionId: 'gateway-1-1',
|
||||
status: 'disconnected',
|
||||
errorMessage: 'client closed',
|
||||
})).resolves.toEqual(expect.objectContaining({ status: 'disconnected', deleted: true }));
|
||||
|
||||
expect(prisma.cmppDownstreamConnection.delete).toHaveBeenCalledWith({ where: { id: 'downstream-1' } });
|
||||
expect(prisma.cmppDownstreamConnection.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('lists enterprise signatures with keyword filters and real relations', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
@@ -185,7 +185,7 @@ export class SmsConfigService {
|
||||
const applicationIds = applications.map((application) => application.id);
|
||||
const [connections, messageStats] = await Promise.all([
|
||||
this.prisma.cmppDownstreamConnection.findMany({
|
||||
where: { applicationId: { in: applicationIds } },
|
||||
where: { applicationId: { in: applicationIds }, status: 'connected' },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
}),
|
||||
this.prisma.smsMessageRecord.groupBy({
|
||||
@@ -297,7 +297,7 @@ export class SmsConfigService {
|
||||
const queuePriority = normalizeApplicationQueuePriority(data.queuePriority);
|
||||
const interfaceType = normalizeApplicationInterfaceType(data.interfaceType);
|
||||
const cmppAccount = data.cmppAccount ? await this.validateAndReserveCmppAccount(data.cmppAccount) : await this.generateCmppAccount();
|
||||
const cmppEnterpriseCode = await this.resolveCmppEnterpriseCode(data.cmppEnterpriseCode, data.tenantId);
|
||||
const cmppEnterpriseCode = cmppAccount;
|
||||
return this.prisma.smsApplication.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
@@ -337,9 +337,7 @@ export class SmsConfigService {
|
||||
const cmppAccount = data.cmppAccount === undefined
|
||||
? undefined
|
||||
: await this.validateAndReserveCmppAccount(data.cmppAccount, applicationId);
|
||||
const cmppEnterpriseCode = data.cmppEnterpriseCode === undefined
|
||||
? undefined
|
||||
: normalizeEnterpriseCode(data.cmppEnterpriseCode);
|
||||
const cmppEnterpriseCode = cmppAccount ?? application.cmppAccount;
|
||||
const interfaceType = data.interfaceType === undefined
|
||||
? undefined
|
||||
: normalizeApplicationInterfaceType(data.interfaceType);
|
||||
@@ -552,17 +550,6 @@ export class SmsConfigService {
|
||||
throw new BadRequestException('Unable to generate unique CMPP account');
|
||||
}
|
||||
|
||||
private async resolveCmppEnterpriseCode(cmppEnterpriseCode: string | undefined, tenantId: string) {
|
||||
if (cmppEnterpriseCode !== undefined) {
|
||||
return normalizeEnterpriseCode(cmppEnterpriseCode);
|
||||
}
|
||||
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId }, select: { code: true } });
|
||||
if (!tenant) {
|
||||
throw new BadRequestException('tenantId does not reference an existing tenant');
|
||||
}
|
||||
return normalizeEnterpriseCode(tenant.code);
|
||||
}
|
||||
|
||||
async recordDownstreamConnectionEvent(data: GatewayDownstreamConnectionEventDto) {
|
||||
const application = await this.prisma.smsApplication.findUnique({
|
||||
where: { cmppAccount: data.account },
|
||||
@@ -574,7 +561,20 @@ export class SmsConfigService {
|
||||
const observedAt = parseGatewayDate(data.observedAt) ?? new Date();
|
||||
const connectedAt = parseGatewayDate(data.connectedAt) ?? observedAt;
|
||||
const existing = await this.prisma.cmppDownstreamConnection.findUnique({ where: { connectionId: data.connectionId } });
|
||||
const status = data.status === 'disconnected' ? 'disconnected' : 'connected';
|
||||
if (data.status === 'disconnected') {
|
||||
if (existing) {
|
||||
await this.prisma.cmppDownstreamConnection.delete({ where: { id: existing.id } });
|
||||
}
|
||||
await this.writeOperationLog(application.tenantId, undefined, 'cmpp_downstream_connection.disconnected', 'cmpp_downstream_connection', data.connectionId, {
|
||||
applicationId: application.id,
|
||||
account: data.account,
|
||||
remoteIp: data.remoteIp,
|
||||
protocol: data.protocol,
|
||||
status: 'disconnected',
|
||||
errorMessage: data.errorMessage,
|
||||
});
|
||||
return { connectionId: data.connectionId, status: 'disconnected', deleted: Boolean(existing) };
|
||||
}
|
||||
const payload = {
|
||||
tenantId: application.tenantId,
|
||||
applicationId: application.id,
|
||||
@@ -582,18 +582,18 @@ export class SmsConfigService {
|
||||
enterpriseCode: application.cmppEnterpriseCode,
|
||||
remoteIp: data.remoteIp,
|
||||
protocol: data.protocol,
|
||||
status,
|
||||
status: 'connected',
|
||||
connectedAt: existing?.connectedAt ?? connectedAt,
|
||||
lastHeartbeatAt: data.status === 'connected' || data.status === 'heartbeat' ? observedAt : existing?.lastHeartbeatAt,
|
||||
lastSubmitAt: data.status === 'submit' ? observedAt : existing?.lastSubmitAt,
|
||||
lastDeliverAt: data.status === 'deliver' ? observedAt : existing?.lastDeliverAt,
|
||||
disconnectedAt: data.status === 'disconnected' ? observedAt : null,
|
||||
lastError: data.status === 'disconnected' ? data.errorMessage ?? existing?.lastError ?? null : null,
|
||||
disconnectedAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
const connection = existing
|
||||
? await this.prisma.cmppDownstreamConnection.update({ where: { id: existing.id }, data: payload })
|
||||
: await this.prisma.cmppDownstreamConnection.create({ data: { connectionId: data.connectionId, ...payload } });
|
||||
if (data.status === 'connected' || data.status === 'disconnected') {
|
||||
if (data.status === 'connected') {
|
||||
await this.writeOperationLog(application.tenantId, undefined, `cmpp_downstream_connection.${data.status}`, 'cmpp_downstream_connection', data.connectionId, {
|
||||
applicationId: application.id,
|
||||
account: data.account,
|
||||
@@ -608,13 +608,8 @@ export class SmsConfigService {
|
||||
async markTimedOutDownstreamConnections(now = new Date()) {
|
||||
const timeoutMs = getPositiveIntegerEnv('CMPP_DOWNSTREAM_HEARTBEAT_TIMEOUT_MS', DEFAULT_DOWNSTREAM_HEARTBEAT_TIMEOUT_MS);
|
||||
const cutoff = new Date(now.getTime() - timeoutMs);
|
||||
return this.prisma.cmppDownstreamConnection.updateMany({
|
||||
return this.prisma.cmppDownstreamConnection.deleteMany({
|
||||
where: { status: 'connected', lastHeartbeatAt: { lt: cutoff } },
|
||||
data: {
|
||||
status: 'heartbeat_timeout',
|
||||
disconnectedAt: now,
|
||||
lastError: `CMPP heartbeat timeout after ${Math.round(timeoutMs / 1000)} seconds`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1326,17 +1321,6 @@ interface TemplateVariableInput {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
function normalizeEnterpriseCode(value: string) {
|
||||
const enterpriseCode = value.trim();
|
||||
if (!enterpriseCode) {
|
||||
throw new BadRequestException('cmppEnterpriseCode is required');
|
||||
}
|
||||
if (enterpriseCode.length > 32) {
|
||||
throw new BadRequestException('cmppEnterpriseCode must be at most 32 characters');
|
||||
}
|
||||
return enterpriseCode;
|
||||
}
|
||||
|
||||
function normalizeApplicationPassword(value: string | undefined) {
|
||||
const password = value?.trim() || generateApplicationPassword();
|
||||
if (password.length !== 16) {
|
||||
|
||||
Reference in New Issue
Block a user