feat: add access number routing and admin list improvements

This commit is contained in:
hectorzhao
2026-07-15 15:45:27 +08:00
parent 27c464246b
commit 3e114ee729
19 changed files with 547 additions and 508 deletions
+85 -2
View File
@@ -10,6 +10,9 @@ export interface CreateSmsApplicationDto {
callbackUrl?: string;
cmppAccount?: string;
cmppEnterpriseCode?: string;
cmppApplicationExtension?: string;
cmppAccessNumberFillEnabled?: boolean;
cmppAccessNumberFillPrefix?: string;
passwordCipher?: string;
interfaceEnabled?: boolean;
interfaceType?: string;
@@ -206,7 +209,9 @@ export class SmsConfigService {
sentToday: todayTotal,
deliveryRate: todayTotal > 0 ? Number(((delivered / todayTotal) * 100).toFixed(1)) : 0,
};
});
}).sort((left, right) => right.sentToday - left.sentToday
|| left.name.localeCompare(right.name, 'zh-CN')
|| left.id.localeCompare(right.id));
}
async getApplication(applicationId: string, tenantId?: string) {
@@ -350,6 +355,8 @@ export class SmsConfigService {
const interfaceType = normalizeApplicationInterfaceType(data.interfaceType);
const cmppAccount = data.cmppAccount ? await this.validateAndReserveCmppAccount(data.cmppAccount) : await this.generateCmppAccount();
const cmppEnterpriseCode = cmppAccount;
const accessNumber = normalizeCmppAccessNumberConfig(data);
await this.validateClientSrcIdAvailable(accessNumber.clientSrcId);
return this.prisma.smsApplication.create({
data: {
tenantId: data.tenantId,
@@ -358,6 +365,10 @@ export class SmsConfigService {
callbackUrl: data.callbackUrl,
cmppAccount,
cmppEnterpriseCode,
cmppApplicationExtension: accessNumber.applicationExtension,
cmppAccessNumberFillEnabled: accessNumber.fillEnabled,
cmppAccessNumberFillPrefix: accessNumber.fillPrefix,
cmppClientSrcId: accessNumber.clientSrcId,
secretHash: secret,
interfaceEnabled: data.interfaceEnabled ?? true,
interfaceType,
@@ -396,6 +407,15 @@ export class SmsConfigService {
const secretHash = data.passwordCipher === undefined
? undefined
: normalizeApplicationPassword(data.passwordCipher);
const accessNumberChanged = data.cmppApplicationExtension !== undefined
|| data.cmppAccessNumberFillEnabled !== undefined
|| data.cmppAccessNumberFillPrefix !== undefined;
const accessNumber = accessNumberChanged
? normalizeCmppAccessNumberConfig(data, application)
: undefined;
if (accessNumber?.clientSrcId && accessNumber.clientSrcId !== application.cmppClientSrcId) {
await this.validateClientSrcIdAvailable(accessNumber.clientSrcId, applicationId);
}
return this.prisma.$transaction(async (tx) => {
if (data.ipAllowlist) {
@@ -409,6 +429,10 @@ export class SmsConfigService {
callbackUrl: data.callbackUrl,
cmppAccount,
cmppEnterpriseCode,
cmppApplicationExtension: accessNumber?.applicationExtension,
cmppAccessNumberFillEnabled: accessNumber?.fillEnabled,
cmppAccessNumberFillPrefix: accessNumber?.fillPrefix,
cmppClientSrcId: accessNumber?.clientSrcId,
secretHash,
interfaceEnabled: data.interfaceEnabled,
interfaceType,
@@ -570,7 +594,10 @@ export class SmsConfigService {
enterpriseCode: application.cmppEnterpriseCode,
account: application.cmppAccount,
passwordCipher: application.secretHash,
srcId: channel?.srcId ?? '',
srcId: application.cmppClientSrcId ?? '',
applicationExtension: application.cmppApplicationExtension,
accessNumberFillEnabled: application.cmppAccessNumberFillEnabled,
accessNumberFillPrefix: application.cmppAccessNumberFillPrefix,
interfaceEnabled: application.interfaceEnabled,
interfaceType: application.interfaceType,
maxConnections: application.cmppMaxConnections,
@@ -591,6 +618,14 @@ export class SmsConfigService {
return cmppAccount;
}
private async validateClientSrcIdAvailable(clientSrcId: string | null, currentApplicationId?: string) {
if (!clientSrcId) return;
const exists = await this.prisma.smsApplication.findUnique({ where: { cmppClientSrcId: clientSrcId } });
if (exists && exists.id !== currentApplicationId) {
throw new BadRequestException('client CMPP Src_Id already exists');
}
}
private async generateCmppAccount() {
for (let attempt = 0; attempt < 20; attempt += 1) {
const cmppAccount = String(randomInt(100000, 1000000));
@@ -1422,6 +1457,54 @@ function normalizeApplicationInterfaceType(value?: string): ApplicationInterface
return interfaceType as ApplicationInterfaceType;
}
function normalizeCmppAccessNumberConfig(
data: Pick<CreateSmsApplicationDto, 'cmppApplicationExtension' | 'cmppAccessNumberFillEnabled' | 'cmppAccessNumberFillPrefix'>,
current?: {
cmppApplicationExtension?: string | null;
cmppAccessNumberFillEnabled?: boolean | null;
cmppAccessNumberFillPrefix?: string | null;
},
) {
const applicationExtension = (
data.cmppApplicationExtension === undefined
? current?.cmppApplicationExtension
: data.cmppApplicationExtension
)?.trim() || null;
const fillEnabled = data.cmppAccessNumberFillEnabled
?? current?.cmppAccessNumberFillEnabled
?? false;
const configuredPrefix = (
data.cmppAccessNumberFillPrefix === undefined
? current?.cmppAccessNumberFillPrefix
: data.cmppAccessNumberFillPrefix
)?.trim() || null;
if (applicationExtension && !/^\d+$/.test(applicationExtension)) {
throw new BadRequestException('cmppApplicationExtension must contain digits only');
}
if (applicationExtension && applicationExtension.length > 21) {
throw new BadRequestException('cmppApplicationExtension must not exceed 21 digits');
}
if (fillEnabled && !applicationExtension) {
throw new BadRequestException('cmppApplicationExtension is required when access number filling is enabled');
}
if (fillEnabled && !configuredPrefix) {
throw new BadRequestException('cmppAccessNumberFillPrefix is required when access number filling is enabled');
}
if (configuredPrefix && !/^\d+$/.test(configuredPrefix)) {
throw new BadRequestException('cmppAccessNumberFillPrefix must contain digits only');
}
const fillPrefix = fillEnabled ? configuredPrefix : null;
const clientSrcId = applicationExtension
? `${fillPrefix ?? ''}${applicationExtension}`
: null;
if (clientSrcId && clientSrcId.length > 21) {
throw new BadRequestException('client CMPP Src_Id must not exceed 21 digits');
}
return { applicationExtension, fillEnabled, fillPrefix, clientSrcId };
}
function getPositiveInteger(value: number | undefined, fallback: number, fieldName: string) {
if (value === undefined || value === null) {
return fallback;