feat: complete cmpp gateway delivery recovery workflows
This commit is contained in:
@@ -9,6 +9,8 @@ function createPrismaMock() {
|
||||
name: '应用A',
|
||||
status: 'active',
|
||||
cmppAccount: '100001',
|
||||
cmppMaxConnections: 2,
|
||||
cmppWindowSize: 32,
|
||||
queuePriority: 'normal',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
messageRecords: [{ status: 'delivered' }, { status: 'undelivered' }],
|
||||
@@ -19,6 +21,8 @@ function createPrismaMock() {
|
||||
name: '应用A',
|
||||
status: 'active',
|
||||
cmppAccount: '100001',
|
||||
cmppMaxConnections: 2,
|
||||
cmppWindowSize: 32,
|
||||
queuePriority: 'normal',
|
||||
secretHash: 'secret-hash',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
@@ -170,6 +174,7 @@ describe('SmsConfigService', () => {
|
||||
gatewayHost: '127.0.0.1',
|
||||
gatewayPort: 17890,
|
||||
maxConnections: 2,
|
||||
windowSize: 32,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -181,6 +186,9 @@ describe('SmsConfigService', () => {
|
||||
await expect(service.createApplication({
|
||||
tenantId: 'tenant-1',
|
||||
name: '优先应用',
|
||||
cmppAccount: '123456',
|
||||
cmppMaxConnections: 3,
|
||||
cmppWindowSize: 32,
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: ['10.0.0.1/32'],
|
||||
})).resolves.toEqual(expect.objectContaining({ id: 'app-new' }));
|
||||
@@ -189,7 +197,9 @@ describe('SmsConfigService', () => {
|
||||
data: expect.objectContaining({
|
||||
tenantId: 'tenant-1',
|
||||
name: '优先应用',
|
||||
cmppAccount: expect.stringMatching(/^\d{6}$/),
|
||||
cmppAccount: '123456',
|
||||
cmppMaxConnections: 3,
|
||||
cmppWindowSize: 32,
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
|
||||
}),
|
||||
@@ -209,6 +219,23 @@ describe('SmsConfigService', () => {
|
||||
expect(prisma.smsApplication.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects duplicate or invalid CMPP application accounts', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.createApplication({
|
||||
tenantId: 'tenant-1',
|
||||
name: '异常应用',
|
||||
cmppAccount: 'abc',
|
||||
})).rejects.toThrow('cmppAccount must be a 6-digit number');
|
||||
|
||||
await expect(service.createApplication({
|
||||
tenantId: 'tenant-1',
|
||||
name: '重复应用',
|
||||
cmppAccount: '100001',
|
||||
})).rejects.toThrow('cmppAccount already exists');
|
||||
});
|
||||
|
||||
it('updates enterprise application profile and allowlist through a transaction', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user