feat: add application queue priority routing

This commit is contained in:
hectorzhao
2026-07-07 14:49:10 +08:00
parent 72f2c010ce
commit 4c8f7dd73f
17 changed files with 392 additions and 46 deletions
+35 -2
View File
@@ -80,6 +80,8 @@ interface SendJob {
messageRecordId: string;
}
type QueuePriority = 'normal' | 'priority';
type RoutedChannel = {
channel: {
id: string;
@@ -101,6 +103,10 @@ type RoutedChannel = {
const SEND_QUEUE = 'sms.send.queue';
const GATEWAY_SUBMIT_QUEUE = 'gateway.submit.queue';
const BULLMQ_PRIORITY: Record<QueuePriority, number> = {
priority: 1,
normal: 100,
};
@Injectable()
export class SendChainService implements OnModuleInit, OnModuleDestroy {
@@ -133,6 +139,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const schedule = parseSchedule(data);
await this.validateSendResources(data.tenantId, data.applicationId, data.templateId);
const unitPrice = await this.resolveUnitPrice(data.tenantId, data.applicationId);
const queuePriority = await this.resolveQueuePriority(data.tenantId, data.applicationId);
const risk = await this.riskReview.evaluateTask({
tenantId: data.tenantId,
applicationId: data.applicationId,
@@ -223,6 +230,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
billingUnits: billing.billingUnitsPerMessage,
unitPrice: billing.unitPrice,
amountCents: billing.billingUnitsPerMessage * billing.unitPrice,
queuePriority,
status: batchStatus === 'ready' ? 'queued' : batchStatus === 'scheduled' ? 'scheduled' : batchStatus,
errorMessage: risk.status === 'rejected' ? risk.reason ?? undefined : undefined,
})),
@@ -365,12 +373,17 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
}
const messages = await this.prisma.smsMessageRecord.findMany({
where: { batchTaskId: taskId, status: 'queued' },
select: { id: true },
select: { id: true, queuePriority: true },
take: 100000,
});
const queue = this.getSendQueue();
for (const message of messages) {
await queue.add('send-message', { messageRecordId: message.id }, { jobId: message.id, attempts: 3 });
const queuePriority = normalizeQueuePriority(message.queuePriority);
await queue.add('send-message', { messageRecordId: message.id }, {
jobId: message.id,
attempts: 3,
priority: BULLMQ_PRIORITY[queuePriority],
});
}
await this.prisma.smsBatchTask.update({ where: { id: taskId }, data: { status: 'queued' } });
return { taskId, enqueued: messages.length };
@@ -653,6 +666,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
phoneNumber: string;
content: string;
billingUnits: number;
queuePriority?: string | null;
template?: { signature?: { id?: string | null; name?: string | null } | null } | null;
},
routed: RoutedChannel,
@@ -701,6 +715,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
applicationId: message.applicationId ?? 'unknown',
taskId: message.batchTaskId,
submitId,
queuePriority: normalizeQueuePriority(message.queuePriority),
phoneNumber: message.phoneNumber,
content: message.content,
signature: message.template?.signature?.name ?? 'SMS',
@@ -882,6 +897,20 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
return application.customerUnitPrice ?? 0;
}
private async resolveQueuePriority(tenantId: string, applicationId?: string): Promise<QueuePriority> {
if (!applicationId) {
return 'normal';
}
const application = await this.prisma.smsApplication.findUnique({
where: { id: applicationId },
select: { tenantId: true, queuePriority: true },
});
if (!application || application.tenantId !== tenantId) {
return 'normal';
}
return normalizeQueuePriority(application.queuePriority);
}
private async validateSendResources(tenantId: string, applicationId?: string, templateId?: string) {
const tenant = await this.prisma.tenant.findUnique({ where: { id: tenantId } });
if (!tenant || tenant.status !== 'active') {
@@ -1202,6 +1231,10 @@ function normalizeCarrier(carrier?: string | null) {
return value || 'mobile';
}
function normalizeQueuePriority(queuePriority?: string | null): QueuePriority {
return queuePriority === 'priority' ? 'priority' : 'normal';
}
function isCarrierCompatible(channelCarrier: string | null | undefined, targetCarrier: string) {
const normalized = normalizeCarrier(channelCarrier);
return normalized === 'all' || normalized === targetCarrier;