feat: complete phase2 baseline cdr quality rbac

This commit is contained in:
hectorzhao
2026-06-24 18:40:21 +08:00
parent 7057fd3c42
commit a86de6545f
63 changed files with 7853 additions and 3678 deletions
+102 -5
View File
@@ -17,12 +17,18 @@ export interface PublishConfigResult {
}
export interface ConfigManifest {
schemaVersion: number;
version: string;
generatedAt: string;
customerCount: number;
businessPrefixCount: number;
gatewayCount: number;
customerGatewayIpCount: number;
customerGatewayBusinessPrefixCount: number;
customerGatewayCallerPrefixCount: number;
policyCount: number;
vendorGatewayCount: number;
callerRewriteCount: number;
lineGroupCount: number;
cityCount: number;
phoneSegmentCount: number;
@@ -40,14 +46,29 @@ type ConfigSnapshot = {
creditLimit: string;
minBalance: string;
}>;
businessPrefixes: Array<{
id: string;
prefix: string;
name: string;
priority: number;
status: string;
}>;
gateways: Array<{
id: string;
customerId: string;
authMode: string;
sourceIp: string | null;
sourceIps: string[];
sipUsername: string | null;
sipDomain: string | null;
sipHa1: string | null;
lineGroupId: string | null;
billingCycleSec: number;
cycleRate: string;
callerMatchMode: string;
callerPrefixes: string[];
calleeMatchMode: string;
businessPrefixes: Array<{ id: string; prefix: string; name: string; priority: number; status: string }>;
status: string;
}>;
policies: Array<{
@@ -75,10 +96,12 @@ type ConfigSnapshot = {
concurrencyLimit: number;
billingCycleSec: number;
cycleRate: string;
landingCalleePrefix: string | null;
status: string;
forbiddenPeriods: Array<{ weekdayMask: number; startTime: string; endTime: string }>;
codecs: Array<{ codec: string; priority: number }>;
prefixRules: Array<{ direction: string; matchPrefix: string; replacePrefix: string; priority: number }>;
callerRewritePool: Array<{ caller: string; weight: number; status: string }>;
blockedRegions: Array<{
regionScope: string;
provinceCode: string | null;
@@ -136,7 +159,7 @@ type ConfigSnapshot = {
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', 'number_library_config'] },
aggregateType: { in: ['business_prefix_config', 'customer_gateway_config', 'vendor_gateway_config', 'line_group_config', 'number_library_config'] },
status: 'PENDING',
availableAt: { lte: now }
},
@@ -217,14 +240,30 @@ export async function rollbackActiveConfig(redis: RedisClient): Promise<{ rolled
}
async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
const [customers, gateways, policies, vendorGateways, lineGroups, cities, phoneSegments, areaCodes, carrierPrefixRules] = await prisma.$transaction([
const [customers, businessPrefixes, gateways, policies, vendorGateways, lineGroups, cities, phoneSegments, areaCodes, carrierPrefixRules] = await prisma.$transaction([
prisma.customer.findMany({
where: { deletedAt: null },
orderBy: [{ id: 'asc' }]
}),
prisma.businessPrefix.findMany({
where: { deletedAt: null, status: 'ENABLED' },
orderBy: [{ priority: 'asc' }, { prefix: 'asc' }]
}),
prisma.customerGateway.findMany({
where: { deletedAt: null },
orderBy: [{ id: 'asc' }]
orderBy: [{ id: 'asc' }],
include: {
ips: { where: { deletedAt: null, status: 'ENABLED' }, orderBy: [{ createdAt: 'asc' }] },
callerPrefixes: { orderBy: [{ priority: 'asc' }, { prefix: 'asc' }] },
businessPrefixes: {
orderBy: [{ createdAt: 'asc' }],
include: {
businessPrefix: {
select: { id: true, prefix: true, name: true, priority: true, status: true }
}
}
}
}
}),
prisma.customerGatewayPolicy.findMany({
where: { deletedAt: null },
@@ -237,6 +276,7 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
forbiddenPeriods: { orderBy: [{ weekdayMask: 'asc' }, { startTime: 'asc' }] },
codecs: { orderBy: [{ priority: 'asc' }] },
prefixRules: { orderBy: [{ direction: 'asc' }, { priority: 'asc' }] },
callerRewritePool: { where: { deletedAt: null, status: 'ENABLED' }, orderBy: [{ weight: 'desc' }, { caller: 'asc' }] },
blockedRegions: { orderBy: [{ regionScope: 'asc' }, { provinceCode: 'asc' }, { cityCode: 'asc' }] }
}
}),
@@ -275,14 +315,35 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
creditLimit: customer.creditLimit.toFixed(6),
minBalance: customer.minBalance.toFixed(6)
})),
businessPrefixes: businessPrefixes.map((prefixItem) => ({
id: prefixItem.id,
prefix: prefixItem.prefix,
name: prefixItem.name,
priority: prefixItem.priority,
status: prefixItem.status
})),
gateways: gateways.map((gateway) => ({
id: gateway.id,
customerId: gateway.customerId,
authMode: gateway.authMode,
sourceIp: gateway.sourceIp,
sourceIps: gateway.ips.length ? gateway.ips.map((ip) => ip.sourceIp) : gateway.sourceIp ? [gateway.sourceIp] : [],
sipUsername: gateway.sipUsername,
sipDomain: gateway.sipDomain,
sipHa1: gateway.sipHa1,
lineGroupId: gateway.lineGroupId,
billingCycleSec: gateway.billingCycleSec,
cycleRate: gateway.cycleRate.toFixed(6),
callerMatchMode: gateway.callerMatchMode,
callerPrefixes: gateway.callerPrefixes.map((item) => item.prefix),
calleeMatchMode: gateway.calleeMatchMode,
businessPrefixes: gateway.businessPrefixes.map((item) => ({
id: item.businessPrefix.id,
prefix: item.businessPrefix.prefix,
name: item.businessPrefix.name,
priority: item.businessPrefix.priority,
status: item.businessPrefix.status
})),
status: gateway.status
})),
policies: policies.map((policy) => ({
@@ -310,6 +371,7 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
concurrencyLimit: gateway.concurrencyLimit,
billingCycleSec: gateway.billingCycleSec,
cycleRate: gateway.cycleRate.toFixed(6),
landingCalleePrefix: gateway.landingCalleePrefix,
status: gateway.status,
forbiddenPeriods: gateway.forbiddenPeriods.map((period) => ({
weekdayMask: period.weekdayMask,
@@ -326,6 +388,11 @@ async function loadSnapshot(prisma: PrismaClient): Promise<ConfigSnapshot> {
replacePrefix: rule.replacePrefix,
priority: rule.priority
})),
callerRewritePool: gateway.callerRewritePool.map((caller) => ({
caller: caller.caller,
weight: caller.weight,
status: caller.status
})),
blockedRegions: gateway.blockedRegions.map((region) => ({
regionScope: region.regionScope,
provinceCode: region.provinceCode,
@@ -391,12 +458,18 @@ async function writeSnapshot(
const prefix = configVersionPrefix(version);
const checksum = crypto.createHash('sha256').update(stableJson(snapshot)).digest('hex');
const manifest: ConfigManifest = {
schemaVersion: 2,
version,
generatedAt: now.toISOString(),
customerCount: snapshot.customers.length,
businessPrefixCount: snapshot.businessPrefixes.length,
gatewayCount: snapshot.gateways.length,
customerGatewayIpCount: snapshot.gateways.reduce((sum, gateway) => sum + gateway.sourceIps.length, 0),
customerGatewayBusinessPrefixCount: snapshot.gateways.reduce((sum, gateway) => sum + gateway.businessPrefixes.length, 0),
customerGatewayCallerPrefixCount: snapshot.gateways.reduce((sum, gateway) => sum + gateway.callerPrefixes.length, 0),
policyCount: snapshot.policies.length,
vendorGatewayCount: snapshot.vendorGateways.length,
callerRewriteCount: snapshot.vendorGateways.reduce((sum, gateway) => sum + gateway.callerRewritePool.length, 0),
lineGroupCount: snapshot.lineGroups.length,
cityCount: snapshot.cities.length,
phoneSegmentCount: snapshot.phoneSegments.length,
@@ -412,10 +485,31 @@ async function writeSnapshot(
for (const customer of snapshot.customers) {
multi.set(`${prefix}:customer:${customer.id}`, JSON.stringify(customer));
}
for (const businessPrefix of snapshot.businessPrefixes) {
multi.set(`${prefix}:business_prefix:${businessPrefix.id}`, JSON.stringify(businessPrefix));
multi.set(`${prefix}:business_prefix_value:${businessPrefix.prefix}`, businessPrefix.id);
multi.rpush(`${prefix}:business_prefixes`, JSON.stringify(businessPrefix));
}
for (const gateway of snapshot.gateways) {
multi.set(`${prefix}:customer_gateway:${gateway.id}`, JSON.stringify(gateway));
if ((gateway.authMode === 'IP' || gateway.authMode === 'MIXED') && gateway.sourceIp) {
multi.set(`${prefix}:auth:ip:${gateway.sourceIp}`, gateway.id);
if (gateway.callerMatchMode === 'PREFIXES') {
for (const callerPrefix of gateway.callerPrefixes) {
multi.rpush(`${prefix}:customer_gateway:${gateway.id}:caller_prefixes`, callerPrefix);
}
}
if (gateway.calleeMatchMode === 'BUSINESS_PREFIXES') {
for (const businessPrefix of gateway.businessPrefixes) {
multi.rpush(`${prefix}:customer_gateway:${gateway.id}:business_prefixes`, JSON.stringify(businessPrefix));
}
}
if (gateway.lineGroupId) {
multi.set(`${prefix}:customer_gateway:${gateway.id}:line_group`, gateway.lineGroupId);
}
if (gateway.authMode === 'IP' || gateway.authMode === 'MIXED') {
for (const sourceIp of gateway.sourceIps) {
multi.set(`${prefix}:auth:ip:${sourceIp}`, gateway.id);
multi.rpush(`${prefix}:auth:ip:${sourceIp}:gateways`, gateway.id);
}
}
if ((gateway.authMode === 'SIP_DIGEST' || gateway.authMode === 'MIXED') && gateway.sipUsername && gateway.sipDomain) {
multi.set(`${prefix}:auth:sip:${gateway.sipUsername}@${gateway.sipDomain}`, gateway.id);
@@ -426,6 +520,9 @@ async function writeSnapshot(
}
for (const gateway of snapshot.vendorGateways) {
multi.set(`${prefix}:vendor_gateway:${gateway.id}`, JSON.stringify(gateway));
for (const callerRewrite of gateway.callerRewritePool) {
multi.rpush(`${prefix}:vendor_gateway:${gateway.id}:caller_rewrite_pool`, JSON.stringify(callerRewrite));
}
for (const region of gateway.blockedRegions) {
if (region.regionScope === 'CITY' && region.cityCode) {
multi.sadd(`${prefix}:vendor_gateway:${gateway.id}:blocked_city_codes`, region.cityCode);