feat: add number library routing and cdr location support

This commit is contained in:
hectorzhao
2026-06-24 11:39:32 +08:00
parent 5fa1bd35e9
commit 7057fd3c42
63 changed files with 6361 additions and 566 deletions
+128 -3
View File
@@ -24,6 +24,11 @@ export interface ConfigManifest {
policyCount: number;
vendorGatewayCount: number;
lineGroupCount: number;
cityCount: number;
phoneSegmentCount: number;
areaCodeCount: number;
carrierPrefixRuleCount: number;
blockedRegionCount: number;
checksum: string;
}
@@ -74,6 +79,13 @@ type ConfigSnapshot = {
forbiddenPeriods: Array<{ weekdayMask: number; startTime: string; endTime: string }>;
codecs: Array<{ codec: string; priority: number }>;
prefixRules: Array<{ direction: string; matchPrefix: string; replacePrefix: string; priority: number }>;
blockedRegions: Array<{
regionScope: string;
provinceCode: string | null;
provinceName: string | null;
cityCode: string | null;
cityName: string | null;
}>;
}>;
lineGroups: Array<{
id: string;
@@ -88,12 +100,43 @@ type ConfigSnapshot = {
status: string;
}>;
}>;
cities: Array<{
code: string;
provinceCode: string;
provinceName: string;
cityCode: string;
cityName: string;
cityLevel: string;
status: string;
}>;
phoneSegments: Array<{
segment7: string;
cityCode: string;
provinceCode: string;
provinceName: string;
cityName: string;
carrier: string;
numberType: 'MOBILE';
}>;
areaCodes: Array<{
areaCode: string;
cityCode: string;
provinceCode: string;
provinceName: string;
cityName: string;
numberType: 'LANDLINE';
}>;
carrierPrefixRules: Array<{
prefix: string;
carrier: string;
priority: number;
}>;
};
export async function publishPendingConfig(prisma: PrismaClient, redis: RedisClient, now = new Date()): Promise<PublishConfigResult> {
const pending = await prisma.outboxEvent.findMany({
where: {
aggregateType: { in: ['customer_gateway_config', 'vendor_gateway_config', 'line_group_config'] },
aggregateType: { in: ['customer_gateway_config', 'vendor_gateway_config', 'line_group_config', 'number_library_config'] },
status: 'PENDING',
availableAt: { lte: now }
},
@@ -174,7 +217,7 @@ export async function rollbackActiveConfig(redis: RedisClient): Promise<{ rolled
}
async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
const [customers, gateways, policies, vendorGateways, lineGroups] = await prisma.$transaction([
const [customers, gateways, policies, vendorGateways, lineGroups, cities, phoneSegments, areaCodes, carrierPrefixRules] = await prisma.$transaction([
prisma.customer.findMany({
where: { deletedAt: null },
orderBy: [{ id: 'asc' }]
@@ -193,7 +236,8 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
include: {
forbiddenPeriods: { orderBy: [{ weekdayMask: 'asc' }, { startTime: 'asc' }] },
codecs: { orderBy: [{ priority: 'asc' }] },
prefixRules: { orderBy: [{ direction: 'asc' }, { priority: 'asc' }] }
prefixRules: { orderBy: [{ direction: 'asc' }, { priority: 'asc' }] },
blockedRegions: { orderBy: [{ regionScope: 'asc' }, { provinceCode: 'asc' }, { cityCode: 'asc' }] }
}
}),
prisma.landingLineGroup.findMany({
@@ -202,6 +246,24 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
include: {
items: { orderBy: [{ priority: 'asc' }] }
}
}),
prisma.geoCity.findMany({
where: { deletedAt: null, status: 'ENABLED' },
orderBy: [{ provinceCode: 'asc' }, { cityCode: 'asc' }]
}),
prisma.phoneNumberSegment.findMany({
where: { deletedAt: null },
orderBy: [{ segment7: 'asc' }],
include: { city: { select: { provinceCode: true } } }
}),
prisma.phoneAreaCode.findMany({
where: { deletedAt: null },
orderBy: [{ areaCode: 'asc' }],
include: { city: { select: { provinceCode: true } } }
}),
prisma.carrierPrefixRule.findMany({
where: { deletedAt: null },
orderBy: [{ prefix: 'desc' }, { priority: 'asc' }]
})
]);
@@ -263,6 +325,13 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
matchPrefix: rule.matchPrefix,
replacePrefix: rule.replacePrefix,
priority: rule.priority
})),
blockedRegions: gateway.blockedRegions.map((region) => ({
regionScope: region.regionScope,
provinceCode: region.provinceCode,
provinceName: region.provinceName,
cityCode: region.cityCode,
cityName: region.cityName
}))
})),
lineGroups: lineGroups.map((group) => ({
@@ -277,6 +346,37 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
concurrencyCap: item.concurrencyCap,
status: item.status
}))
})),
cities: cities.map((city) => ({
code: city.code,
provinceCode: city.provinceCode,
provinceName: city.provinceName,
cityCode: city.cityCode,
cityName: city.cityName,
cityLevel: city.cityLevel,
status: city.status
})),
phoneSegments: phoneSegments.map((segment) => ({
segment7: segment.segment7,
cityCode: segment.cityCode,
provinceCode: segment.city.provinceCode,
provinceName: segment.provinceName,
cityName: segment.cityName,
carrier: segment.carrier,
numberType: 'MOBILE'
})),
areaCodes: areaCodes.map((areaCode) => ({
areaCode: areaCode.areaCode,
cityCode: areaCode.cityCode,
provinceCode: areaCode.city.provinceCode,
provinceName: areaCode.provinceName,
cityName: areaCode.cityName,
numberType: 'LANDLINE'
})),
carrierPrefixRules: carrierPrefixRules.map((rule) => ({
prefix: rule.prefix,
carrier: rule.carrier,
priority: rule.priority
}))
};
}
@@ -298,6 +398,11 @@ async function writeSnapshot(
policyCount: snapshot.policies.length,
vendorGatewayCount: snapshot.vendorGateways.length,
lineGroupCount: snapshot.lineGroups.length,
cityCount: snapshot.cities.length,
phoneSegmentCount: snapshot.phoneSegments.length,
areaCodeCount: snapshot.areaCodes.length,
carrierPrefixRuleCount: snapshot.carrierPrefixRules.length,
blockedRegionCount: snapshot.vendorGateways.reduce((sum, gateway) => sum + gateway.blockedRegions.length, 0),
checksum
};
@@ -321,6 +426,14 @@ async function writeSnapshot(
}
for (const gateway of snapshot.vendorGateways) {
multi.set(`${prefix}:vendor_gateway:${gateway.id}`, JSON.stringify(gateway));
for (const region of gateway.blockedRegions) {
if (region.regionScope === 'CITY' && region.cityCode) {
multi.sadd(`${prefix}:vendor_gateway:${gateway.id}:blocked_city_codes`, region.cityCode);
}
if (region.regionScope === 'PROVINCE' && region.provinceCode) {
multi.sadd(`${prefix}:vendor_gateway:${gateway.id}:blocked_province_codes`, region.provinceCode);
}
}
}
for (const lineGroup of snapshot.lineGroups) {
multi.set(`${prefix}:line_group:${lineGroup.id}`, JSON.stringify(lineGroup));
@@ -328,6 +441,18 @@ async function writeSnapshot(
multi.rpush(`${prefix}:line_group:${lineGroup.id}:items`, JSON.stringify(item));
}
}
for (const city of snapshot.cities) {
multi.set(`${prefix}:geo_city:${city.cityCode}`, JSON.stringify(city));
}
for (const segment of snapshot.phoneSegments) {
multi.set(`${prefix}:phone_segment:${segment.segment7}`, JSON.stringify(segment));
}
for (const areaCode of snapshot.areaCodes) {
multi.set(`${prefix}:area_code:${areaCode.areaCode}`, JSON.stringify(areaCode));
}
for (const rule of snapshot.carrierPrefixRules) {
multi.set(`${prefix}:carrier_prefix:${rule.prefix}`, JSON.stringify(rule));
}
if (previousVersion) {
multi.set(CONFIG_PREVIOUS_VERSION_KEY, previousVersion);