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
@@ -59,9 +59,38 @@ function prismaFixture(): PrismaClient {
{ id: 'cus_1', status: 'ENABLED', balance: new Prisma.Decimal('10'), creditLimit: new Prisma.Decimal('0'), minBalance: new Prisma.Decimal('0') }
]
},
businessPrefix: {
findMany: async () => [
{ id: 'bp_1', prefix: '671', name: '业务671', priority: 10, status: 'ENABLED' }
]
},
customerGateway: {
findMany: async () => [
{ id: 'cgw_1', customerId: 'cus_1', authMode: 'IP', sourceIp: '100.93.185.30', sipUsername: null, sipDomain: null, sipHa1: null, status: 'ENABLED' }
{
id: 'cgw_1',
customerId: 'cus_1',
authMode: 'IP',
sourceIp: '100.93.185.30',
ips: [
{ sourceIp: '100.93.185.30' },
{ sourceIp: '100.93.185.31' }
],
sipUsername: null,
sipDomain: null,
sipHa1: null,
lineGroupId: 'lg_1',
billingCycleSec: 6,
cycleRate: new Prisma.Decimal('0.008'),
callerMatchMode: 'PREFIXES',
callerPrefixes: [{ prefix: '021' }],
calleeMatchMode: 'BUSINESS_PREFIXES',
businessPrefixes: [
{
businessPrefix: { id: 'bp_1', prefix: '671', name: '业务671', priority: 10, status: 'ENABLED' }
}
],
status: 'ENABLED'
}
]
},
customerGatewayPolicy: {
@@ -95,10 +124,15 @@ function prismaFixture(): PrismaClient {
concurrencyLimit: 30,
billingCycleSec: 6,
cycleRate: new Prisma.Decimal('0.012'),
landingCalleePrefix: '86',
status: 'ENABLED',
forbiddenPeriods: [],
codecs: [],
prefixRules: [],
callerRewritePool: [
{ caller: '02160010001', weight: 70, status: 'ENABLED' },
{ caller: '02160010002', weight: 30, status: 'ENABLED' }
],
blockedRegions: [{ regionScope: 'CITY', provinceCode: '340000', provinceName: '安徽省', cityCode: '340100', cityName: '合肥市' }]
}
]
@@ -141,6 +175,12 @@ describe('S42 config publisher number library snapshot', () => {
expect(result.published).toBe(true);
expect(result.manifest).toMatchObject({
schemaVersion: 2,
businessPrefixCount: 1,
customerGatewayIpCount: 2,
customerGatewayBusinessPrefixCount: 1,
customerGatewayCallerPrefixCount: 1,
callerRewriteCount: 2,
cityCount: 1,
phoneSegmentCount: 1,
areaCodeCount: 1,
@@ -149,6 +189,14 @@ describe('S42 config publisher number library snapshot', () => {
});
expect(redis.writes).toEqual(
expect.arrayContaining([
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:business_prefix:bp_1` }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:business_prefix_value:671`, value: 'bp_1' }),
expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:business_prefixes` }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:auth:ip:100.93.185.30`, value: 'cgw_1' }),
expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:auth:ip:100.93.185.31:gateways`, value: 'cgw_1' }),
expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:customer_gateway:cgw_1:caller_prefixes`, value: '021' }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:customer_gateway:cgw_1:line_group`, value: 'lg_1' }),
expect.objectContaining({ command: 'rpush', key: `cfg:v:${version}:vendor_gateway:vgw_1:caller_rewrite_pool` }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:phone_segment:1380013` }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:area_code:0551` }),
expect.objectContaining({ command: 'set', key: `cfg:v:${version}:carrier_prefix:138` }),
+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);