feat: optimize routing and operations views

This commit is contained in:
hectorzhao
2026-07-29 22:32:28 +08:00
parent 500f43f673
commit c0a4317a7e
21 changed files with 718 additions and 86 deletions
+29 -31
View File
@@ -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 }> }) {