feat: complete cmpp gateway delivery recovery workflows

This commit is contained in:
hectorzhao
2026-07-08 16:30:06 +08:00
parent cc628d0214
commit 8144f08652
60 changed files with 8901 additions and 94 deletions
+36 -3
View File
@@ -8,6 +8,9 @@ export interface CreateSmsApplicationDto {
name: string;
scene?: string;
callbackUrl?: string;
cmppAccount?: string;
cmppMaxConnections?: number;
cmppWindowSize?: number;
dailyLimit?: number;
customerUnitPrice?: number;
queuePriority?: string;
@@ -153,7 +156,7 @@ export class SmsConfigService {
async createApplication(data: CreateSmsApplicationDto) {
const secret = randomBytes(24).toString('hex');
const queuePriority = normalizeApplicationQueuePriority(data.queuePriority);
const cmppAccount = await this.generateCmppAccount();
const cmppAccount = data.cmppAccount ? await this.validateAndReserveCmppAccount(data.cmppAccount) : await this.generateCmppAccount();
return this.prisma.smsApplication.create({
data: {
tenantId: data.tenantId,
@@ -162,6 +165,8 @@ export class SmsConfigService {
callbackUrl: data.callbackUrl,
cmppAccount,
secretHash: hashSecret(secret),
cmppMaxConnections: getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
cmppWindowSize: getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
dailyLimit: data.dailyLimit,
customerUnitPrice: data.customerUnitPrice ?? 0,
queuePriority,
@@ -183,6 +188,9 @@ export class SmsConfigService {
const queuePriority = data.queuePriority === undefined
? undefined
: normalizeApplicationQueuePriority(data.queuePriority);
const cmppAccount = data.cmppAccount === undefined
? undefined
: await this.validateAndReserveCmppAccount(data.cmppAccount, applicationId);
return this.prisma.$transaction(async (tx) => {
if (data.ipAllowlist) {
@@ -194,6 +202,9 @@ export class SmsConfigService {
name: data.name,
scene: data.scene,
callbackUrl: data.callbackUrl,
cmppAccount,
cmppMaxConnections: data.cmppMaxConnections === undefined ? undefined : getPositiveInteger(data.cmppMaxConnections, 1, 'cmppMaxConnections'),
cmppWindowSize: data.cmppWindowSize === undefined ? undefined : getPositiveInteger(data.cmppWindowSize, 16, 'cmppWindowSize'),
dailyLimit: data.dailyLimit,
customerUnitPrice: data.customerUnitPrice,
queuePriority,
@@ -350,13 +361,24 @@ export class SmsConfigService {
account: application.cmppAccount,
passwordCipher: application.secretHash,
srcId: channel?.srcId ?? '',
maxConnections: channel?.config && typeof channel.config === 'object' && 'maxConnections' in channel.config ? Number(channel.config.maxConnections) : 1,
maxConnections: application.cmppMaxConnections,
heartbeatSeconds: 30,
windowSize: 16,
windowSize: application.cmppWindowSize,
protocolVersion: channel?.cmppVersion ?? '3.0',
};
}
private async validateAndReserveCmppAccount(cmppAccount: string, currentApplicationId?: string) {
if (!/^\d{6}$/.test(cmppAccount)) {
throw new BadRequestException('cmppAccount must be a 6-digit number');
}
const exists = await this.prisma.smsApplication.findUnique({ where: { cmppAccount } });
if (exists && exists.id !== currentApplicationId) {
throw new BadRequestException('cmppAccount already exists');
}
return cmppAccount;
}
private async generateCmppAccount() {
for (let attempt = 0; attempt < 20; attempt += 1) {
const cmppAccount = String(randomInt(100000, 1000000));
@@ -769,6 +791,17 @@ function normalizeApplicationQueuePriority(value?: string): ApplicationQueuePrio
return queuePriority as ApplicationQueuePriority;
}
function getPositiveInteger(value: number | undefined, fallback: number, fieldName: string) {
if (value === undefined || value === null) {
return fallback;
}
const normalized = Number(value);
if (!Number.isInteger(normalized) || normalized <= 0) {
throw new BadRequestException(`${fieldName} must be a positive integer`);
}
return normalized;
}
function normalizeApplicationCmppStatus(connections: Array<{ status: string; currentConnections: number }>, applicationStatus: string) {
if (applicationStatus !== 'active') {
return 'inactive';