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
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
const DEFAULT_CARRIER_RULE_CACHE_TTL_MS = 30_000;
interface CompiledCarrierRule {
carrier: string;
pattern: RegExp;
}
@Injectable()
export class PhoneRoutingLookupService {
private carrierRuleCache?: { expiresAt: number; rules: CompiledCarrierRule[] };
private carrierRuleLoad?: { generation: number; promise: Promise<CompiledCarrierRule[]> };
private carrierRuleGeneration = 0;
constructor(private readonly prisma: PrismaService) {}
async identifyCarrier(phoneNumber: string) {
const rules = await this.getCarrierRules();
return rules.find((rule) => rule.pattern.test(phoneNumber))?.carrier;
}
async identifyProvince(phoneNumber: string) {
const prefixes = phonePrefixes(phoneNumber);
if (prefixes.length === 0) return null;
const segments = await this.prisma.phoneSegment.findMany({
where: { prefix: { in: prefixes } },
select: { prefix: true, province: true },
});
const provinceByPrefix = new Map(segments.map((segment) => [segment.prefix, segment.province]));
for (const prefix of prefixes) {
const province = provinceByPrefix.get(prefix);
if (province) return province;
}
return null;
}
invalidateCarrierRules() {
this.carrierRuleGeneration += 1;
this.carrierRuleCache = undefined;
}
private async getCarrierRules() {
const now = Date.now();
if (this.carrierRuleCache && this.carrierRuleCache.expiresAt > now) {
return this.carrierRuleCache.rules;
}
const generation = this.carrierRuleGeneration;
if (this.carrierRuleLoad?.generation === generation) return this.carrierRuleLoad.promise;
const promise = this.loadCarrierRules(generation);
this.carrierRuleLoad = { generation, promise };
try {
return await promise;
} finally {
if (this.carrierRuleLoad?.promise === promise) {
this.carrierRuleLoad = undefined;
}
}
}
private async loadCarrierRules(generation: number) {
const rows = await this.prisma.phoneCarrierRule.findMany({
where: { status: 'active' },
orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }],
select: { carrier: true, pattern: true },
});
const rules = rows.flatMap((row) => {
try {
return [{ carrier: row.carrier, pattern: new RegExp(row.pattern) }];
} catch {
return [];
}
});
if (generation === this.carrierRuleGeneration) {
this.carrierRuleCache = {
expiresAt: Date.now() + carrierRuleCacheTtlMs(),
rules,
};
}
return rules;
}
}
function phonePrefixes(phoneNumber: string) {
const maxLength = Math.min(7, phoneNumber.length);
return Array.from({ length: Math.max(0, maxLength - 2) }, (_, index) =>
phoneNumber.slice(0, maxLength - index));
}
function carrierRuleCacheTtlMs() {
const configured = Number(process.env.PHONE_CARRIER_RULE_CACHE_TTL_MS);
return Number.isFinite(configured) && configured > 0
? Math.floor(configured)
: DEFAULT_CARRIER_RULE_CACHE_TTL_MS;
}