feat: add application queue priority routing
This commit is contained in:
@@ -8,6 +8,7 @@ function createPrismaMock() {
|
||||
tenantId: 'tenant-1',
|
||||
name: '应用A',
|
||||
status: 'active',
|
||||
queuePriority: 'normal',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
messageRecords: [{ status: 'delivered' }, { status: 'undelivered' }],
|
||||
}]),
|
||||
@@ -16,6 +17,7 @@ function createPrismaMock() {
|
||||
tenantId: 'tenant-1',
|
||||
name: '应用A',
|
||||
status: 'active',
|
||||
queuePriority: 'normal',
|
||||
secretHash: 'secret-hash',
|
||||
tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' },
|
||||
}),
|
||||
@@ -147,6 +149,7 @@ describe('SmsConfigService', () => {
|
||||
expect.objectContaining({
|
||||
id: 'app-1',
|
||||
cmppStatus: 'connected',
|
||||
queuePriority: 'normal',
|
||||
sentToday: 2,
|
||||
deliveryRate: 50,
|
||||
cmppConnections: [expect.objectContaining({ connectionId: 'conn-a' })],
|
||||
@@ -167,6 +170,40 @@ describe('SmsConfigService', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('creates enterprise applications with persisted queue priority', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.createApplication({
|
||||
tenantId: 'tenant-1',
|
||||
name: '优先应用',
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: ['10.0.0.1/32'],
|
||||
})).resolves.toEqual(expect.objectContaining({ id: 'app-new' }));
|
||||
|
||||
expect(prisma.smsApplication.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
tenantId: 'tenant-1',
|
||||
name: '优先应用',
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
it('rejects invalid enterprise application queue priority', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
expect(() => service.createApplication({
|
||||
tenantId: 'tenant-1',
|
||||
name: '异常应用',
|
||||
queuePriority: 'urgent',
|
||||
})).toThrow('queuePriority must be normal or priority');
|
||||
|
||||
expect(prisma.smsApplication.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates enterprise application profile and allowlist through a transaction', async () => {
|
||||
const prisma = createPrismaMock();
|
||||
const tx = {
|
||||
@@ -181,7 +218,7 @@ describe('SmsConfigService', () => {
|
||||
prisma.$transaction.mockImplementationOnce((callback: (client: typeof tx) => unknown) => callback(tx));
|
||||
const service = new SmsConfigService(prisma as never);
|
||||
|
||||
await expect(service.updateApplication('app-1', { name: '新应用', customerUnitPrice: 300, ipAllowlist: ['10.0.0.1/32'] }))
|
||||
await expect(service.updateApplication('app-1', { name: '新应用', customerUnitPrice: 300, queuePriority: 'priority', ipAllowlist: ['10.0.0.1/32'] }))
|
||||
.resolves.toEqual(expect.objectContaining({ id: 'app-1', name: '新应用' }));
|
||||
|
||||
expect(tx.smsApplicationIpAllowlist.deleteMany).toHaveBeenCalledWith({ where: { applicationId: 'app-1' } });
|
||||
@@ -190,6 +227,7 @@ describe('SmsConfigService', () => {
|
||||
data: expect.objectContaining({
|
||||
name: '新应用',
|
||||
customerUnitPrice: 300,
|
||||
queuePriority: 'priority',
|
||||
ipAllowlist: { create: [{ ipCidr: '10.0.0.1/32' }] },
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface CreateSmsApplicationDto {
|
||||
callbackUrl?: string;
|
||||
dailyLimit?: number;
|
||||
customerUnitPrice?: number;
|
||||
queuePriority?: string;
|
||||
maxPhonesPerTask?: number;
|
||||
templateMismatchMode?: string;
|
||||
ipAllowlist?: string[];
|
||||
@@ -85,6 +86,9 @@ export interface ApplicationListQuery {
|
||||
includeConnections?: boolean;
|
||||
}
|
||||
|
||||
const APPLICATION_QUEUE_PRIORITIES = ['normal', 'priority'] as const;
|
||||
type ApplicationQueuePriority = typeof APPLICATION_QUEUE_PRIORITIES[number];
|
||||
|
||||
@Injectable()
|
||||
export class SmsConfigService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
@@ -148,6 +152,7 @@ export class SmsConfigService {
|
||||
|
||||
createApplication(data: CreateSmsApplicationDto) {
|
||||
const secret = randomBytes(24).toString('hex');
|
||||
const queuePriority = normalizeApplicationQueuePriority(data.queuePriority);
|
||||
return this.prisma.smsApplication.create({
|
||||
data: {
|
||||
tenantId: data.tenantId,
|
||||
@@ -157,6 +162,7 @@ export class SmsConfigService {
|
||||
secretHash: hashSecret(secret),
|
||||
dailyLimit: data.dailyLimit,
|
||||
customerUnitPrice: data.customerUnitPrice ?? 0,
|
||||
queuePriority,
|
||||
maxPhonesPerTask: data.maxPhonesPerTask ?? 1000000,
|
||||
templateMismatchMode: data.templateMismatchMode ?? 'reject',
|
||||
ipAllowlist: {
|
||||
@@ -172,6 +178,9 @@ export class SmsConfigService {
|
||||
if (!application) {
|
||||
throw new NotFoundException('Application not found');
|
||||
}
|
||||
const queuePriority = data.queuePriority === undefined
|
||||
? undefined
|
||||
: normalizeApplicationQueuePriority(data.queuePriority);
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
if (data.ipAllowlist) {
|
||||
@@ -185,6 +194,7 @@ export class SmsConfigService {
|
||||
callbackUrl: data.callbackUrl,
|
||||
dailyLimit: data.dailyLimit,
|
||||
customerUnitPrice: data.customerUnitPrice,
|
||||
queuePriority,
|
||||
maxPhonesPerTask: data.maxPhonesPerTask,
|
||||
templateMismatchMode: data.templateMismatchMode,
|
||||
status: data.status,
|
||||
@@ -738,6 +748,14 @@ function startOfToday() {
|
||||
return date;
|
||||
}
|
||||
|
||||
function normalizeApplicationQueuePriority(value?: string): ApplicationQueuePriority {
|
||||
const queuePriority = value ?? 'normal';
|
||||
if (!APPLICATION_QUEUE_PRIORITIES.includes(queuePriority as ApplicationQueuePriority)) {
|
||||
throw new BadRequestException('queuePriority must be normal or priority');
|
||||
}
|
||||
return queuePriority as ApplicationQueuePriority;
|
||||
}
|
||||
|
||||
function normalizeApplicationCmppStatus(connections: Array<{ status: string; currentConnections: number }>, applicationStatus: string) {
|
||||
if (applicationStatus !== 'active') {
|
||||
return 'inactive';
|
||||
|
||||
Reference in New Issue
Block a user