feat: optimize routing and operations views
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { DictionariesModule } from '../dictionaries/dictionaries.module';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { RiskReviewModule } from '../risk-review/risk-review.module';
|
||||
import { SmsConfigModule } from '../sms-config/sms-config.module';
|
||||
@@ -10,7 +11,7 @@ import { GatewayEventsController } from './gateway-events.controller';
|
||||
import { SendChainService } from './send-chain.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, BillingModule, forwardRef(() => RiskReviewModule), SmsConfigModule, forwardRef(() => OpenApiModule)],
|
||||
imports: [PrismaModule, BillingModule, DictionariesModule, forwardRef(() => RiskReviewModule), SmsConfigModule, forwardRef(() => OpenApiModule)],
|
||||
controllers: [AdminSendChainController, ClientSendChainController, GatewayEventsController],
|
||||
providers: [SendChainService],
|
||||
exports: [SendChainService],
|
||||
|
||||
@@ -143,7 +143,7 @@ function createPrismaMock() {
|
||||
findMany: jest.fn().mockResolvedValue([{ carrier: 'mobile', pattern: '^13[4-9]', priority: 1, status: 'active' }]),
|
||||
},
|
||||
phoneSegment: {
|
||||
findUnique: jest.fn().mockResolvedValue({ prefix: '1380000', province: '山东', city: '济南' }),
|
||||
findMany: jest.fn().mockResolvedValue([{ prefix: '1380000', province: '山东', city: '济南' }]),
|
||||
},
|
||||
smsChannel: {
|
||||
findFirst: jest.fn().mockResolvedValue(channel),
|
||||
@@ -1873,6 +1873,26 @@ describe('SendChainService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reuses persisted carrier and province without querying routing dictionaries again', async () => {
|
||||
const { service, prisma } = createService();
|
||||
service['identifyCarrier'] = jest.fn();
|
||||
service['identifyProvince'] = jest.fn();
|
||||
|
||||
await expect(service['selectChannelForMessage']({
|
||||
id: 'record-1',
|
||||
tenantId: 'tenant-1',
|
||||
applicationId: 'app-1',
|
||||
signatureId: 'sig-1',
|
||||
phoneNumber: '13800000001',
|
||||
carrier: 'mobile',
|
||||
province: '山东',
|
||||
})).resolves.toEqual(expect.objectContaining({ carrier: 'mobile', province: '山东' }));
|
||||
|
||||
expect(service['identifyCarrier']).not.toHaveBeenCalled();
|
||||
expect(service['identifyProvince']).not.toHaveBeenCalled();
|
||||
expect(prisma.smsMessageRecord.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates submit result status, charges billing, and task progress', async () => {
|
||||
const { service, prisma, billing } = createService();
|
||||
|
||||
|
||||
@@ -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