feat: complete phase2 baseline cdr quality rbac
This commit is contained in:
@@ -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` }),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user