feat: optimize routing and operations views
This commit is contained in:
@@ -8,6 +8,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { BillingService } from '../billing/billing.service';
|
||||
import { isIpAllowed } from '../common/ip-allowlist';
|
||||
import { moneyToNumber } from '../common/money';
|
||||
import { PhoneRoutingLookupService } from '../dictionaries/phone-routing-lookup.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { RiskReviewService } from '../risk-review/risk-review.service';
|
||||
import { OpenApiService } from '../open-api/open-api.service';
|
||||
@@ -315,13 +316,17 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
private upstreamReceiptInboxInitialTimer?: ReturnType<typeof setTimeout>;
|
||||
private upstreamReceiptInboxIntervalTimer?: ReturnType<typeof setInterval>;
|
||||
private upstreamReceiptInboxScanRunning = false;
|
||||
private readonly phoneRouting: PhoneRoutingLookupService;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly billing: BillingService,
|
||||
private readonly riskReview: RiskReviewService,
|
||||
@Optional() @Inject(forwardRef(() => OpenApiService)) private readonly openApi?: OpenApiService,
|
||||
) {}
|
||||
@Optional() phoneRouting?: PhoneRoutingLookupService,
|
||||
) {
|
||||
this.phoneRouting = phoneRouting ?? new PhoneRoutingLookupService(prisma);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
if (process.env.API_ENABLE_SEND_WORKER === 'true') {
|
||||
@@ -3600,6 +3605,8 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
queuedAt?: Date;
|
||||
clientSrcId?: string | null;
|
||||
applicationExtension?: string | null;
|
||||
carrier?: string | null;
|
||||
province?: string | null;
|
||||
},
|
||||
reason: string,
|
||||
sourceSubmitRecordId?: string,
|
||||
@@ -3659,7 +3666,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
})}`);
|
||||
return null;
|
||||
}
|
||||
const route = await this.findApplicationRoute(message.tenantId, message.applicationId ?? undefined, await this.identifyCarrier(message.phoneNumber));
|
||||
const retryCarrier = message.carrier
|
||||
? normalizeCarrier(message.carrier)
|
||||
: await this.identifyCarrier(message.phoneNumber);
|
||||
const route = await this.findApplicationRoute(message.tenantId, message.applicationId ?? undefined, retryCarrier);
|
||||
const retryTimeLimitMinutes = Math.min(route.group.retryTimeLimitMinutes ?? route.group.retryTimeLimitHours * 60, 72 * 60);
|
||||
if (!route.group.retryEnabled || ageMinutes >= retryTimeLimitMinutes) {
|
||||
this.logger.warn(`sms_retry_route_skipped ${JSON.stringify({
|
||||
@@ -3673,7 +3683,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const routed = await this.selectChannelForMessage(message, {
|
||||
const routed = await this.selectChannelForMessage({ ...message, carrier: retryCarrier }, {
|
||||
forceNational: true,
|
||||
excludeChannelIds: attemptedChannelIds,
|
||||
});
|
||||
@@ -3708,18 +3718,25 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private async selectChannelForMessage(
|
||||
message: { id: string; tenantId: string; applicationId?: string | null; templateId?: string | null; signatureId?: string | null; phoneNumber: string; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null },
|
||||
message: { id: string; tenantId: string; applicationId?: string | null; templateId?: string | null; signatureId?: string | null; phoneNumber: string; carrier?: string | null; province?: string | null; template?: { signature?: { id?: string | null } | null } | null; signature?: { id?: string | null } | null },
|
||||
options: { forceNational?: boolean; excludeChannelIds?: string[] } = {},
|
||||
): Promise<RoutedChannel> {
|
||||
if (!message.applicationId) {
|
||||
throw new BadRequestException('短信应用未配置,无法选择通道组');
|
||||
}
|
||||
const carrier = await this.identifyCarrier(message.phoneNumber);
|
||||
const province = await this.identifyProvince(message.phoneNumber);
|
||||
await this.prisma.smsMessageRecord.update({
|
||||
where: { id: message.id },
|
||||
data: { carrier, province },
|
||||
});
|
||||
const hasPersistedRouting = Boolean(message.carrier);
|
||||
const [carrier, province] = hasPersistedRouting
|
||||
? [normalizeCarrier(message.carrier), message.province ?? null]
|
||||
: await Promise.all([
|
||||
this.identifyCarrier(message.phoneNumber),
|
||||
this.identifyProvince(message.phoneNumber),
|
||||
]);
|
||||
if (!hasPersistedRouting) {
|
||||
await this.prisma.smsMessageRecord.update({
|
||||
where: { id: message.id },
|
||||
data: { carrier, province },
|
||||
});
|
||||
}
|
||||
const route = await this.findApplicationRoute(message.tenantId, message.applicationId, carrier);
|
||||
const excluded = new Set(options.excludeChannelIds ?? []);
|
||||
const signatureId = await this.resolveMessageSignatureId(message);
|
||||
@@ -3777,30 +3794,11 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
private async identifyCarrier(phoneNumber: string) {
|
||||
const rules = await this.prisma.phoneCarrierRule.findMany({
|
||||
where: { status: 'active' },
|
||||
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
for (const rule of rules) {
|
||||
try {
|
||||
if (new RegExp(rule.pattern).test(phoneNumber)) {
|
||||
return normalizeCarrier(rule.carrier);
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return 'mobile';
|
||||
return normalizeCarrier(await this.phoneRouting.identifyCarrier(phoneNumber));
|
||||
}
|
||||
|
||||
private async identifyProvince(phoneNumber: string) {
|
||||
for (let length = Math.min(7, phoneNumber.length); length >= 3; length -= 1) {
|
||||
const segment = await this.prisma.phoneSegment.findUnique({ where: { prefix: phoneNumber.slice(0, length) } });
|
||||
if (segment?.province) {
|
||||
return segment.province;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return this.phoneRouting.identifyProvince(phoneNumber);
|
||||
}
|
||||
|
||||
private isChannelSendAvailable(channel: { status: string; connectionStates?: Array<{ status: string; currentConnections: number; desiredConnections: number }> }) {
|
||||
|
||||
Reference in New Issue
Block a user