Asterisk呼叫转移时多CDR日志重复生成的解决方案问询
Asterisk呼叫转移场景下避免重复生成通话日志的方案
问题背景
在Asterisk系统中,呼叫转移会触发5条CDR事件:1条来自队列,其余4条为呼入事件。正常无分机转移的呼叫场景中,系统仅生成1条通话日志(对应2条CDR:队列事件+1条呼入事件);但分机间转移时,4条呼入CDR会导致系统生成4条重复的通话日志,需要实现呼叫转移全生命周期仅生成1条通话日志。
现有CDR处理代码
this.ami.on('cdr', async (evt) => { if (evt.source == '//somerandomtestphonenumber//') { console.log(evt); } var destination: string = evt.destination.toString(); var destinationcontext: string = evt.destinationcontext.toString(); var disposition: string = evt.disposition.toString(); var channel: string = evt.channel.toString(); var destinationchannel: string = evt.destinationchannel.toString(); var source: string = evt.source.toString(); const landline: string = '+//someLandLinePhoneNumber'; var receiver: string = evt.destination.toString(); var startTime: Date = new Date(evt.starttime); var answerTime: Date = evt.answertime == '' ? null : new Date(evt.answertime); var endTime: Date = new Date(evt.endtime); var totalTime: number = parseFloat(evt.duration); var callTime: number = parseFloat(evt.billableseconds); var holdTime: number = totalTime - callTime; var lastapplication = evt.lastapplication.toString(); var duration: string = evt.duration.toString(); var billableseconds: string = evt.billableseconds.toString(); var uniqueId: string = evt.uniqueid.toString(); // Voicemail if (destination == 's' && evt.lastapplication == 'VoiceMail') { const contextFirstPart = destinationcontext.substring( destinationcontext.indexOf('-') + 1, destinationcontext.length, ); const queue = contextFirstPart.substring( contextFirstPart.indexOf('-') + 1, contextFirstPart.length, ); // Note : Event is handled in voicemail service this.eventEmitter.emit( 'voicemail-received', new VoicemailLogDto( queue, source, startTime, answerTime, endTime, duration, disposition, uniqueId, channel, ), ); } if ( !destinationcontext.includes('ivr') && !disposition.includes('CONGESTION') && !disposition.includes('FAILED') ) { var caller: string = source == landline ? evt.channel.split('/')[1].split('-')[0] : evt.source.toString(); const callLogEntry = new CreateCalllogDto( caller, receiver, startTime, answerTime, endTime, evt.endtime, holdTime, totalTime, callTime, disposition, 'unknown', uniqueId, ); // For Outbound if (source == landline && destinationcontext == 'from-internal') { callLogEntry.type = 'outbound'; if (process.env.NODE_ENV !== 'development') { await this.callLogService.create(callLogEntry); } if ( callLogEntry.disposition == 'NO ANSWER' || callLogEntry.disposition == 'BUSY' ) { this.realtimeGateway.emitData( 'outbound-missed', 'outbound-missed', [Role.ADMIN, Role.SUPERVISOR, Role.AGENT], ); } if (callLogEntry.disposition == 'ANSWERED') { this.realtimeGateway.emitData( 'outbound-success', 'outbound-success', [Role.ADMIN, Role.SUPERVISOR, Role.AGENT], ); } } // For inbound if (source != landline && destinationcontext == 'from-internal') { callLogEntry.type = 'inbound'; var result = await this.callLogService.create(callLogEntry); } // For internal if (destinationchannel.includes('from-internal-xfer')) { callLogEntry.type = 'internal'; await this.callLogService.create(callLogEntry); } // For queue if (source != landline && destinationcontext == 'ext-queues') { const queueLogEntry = new CreateQueueReceiveLogDto( caller, destinationchannel.substring( destinationchannel.indexOf('/') + 1, destinationchannel.indexOf('@'), ), receiver, 'ANSWERED', uniqueId, evt.endtime, ); if (process.env.NODE_ENV !== 'development') { await this.queueLogService.createQueueReceiveLog(queueLogEntry); } } } });
呼叫转移时的CDR事件日志
{ event: 'Cdr', privilege: 'cdr,all', accountcode: '', source: '/somerandomphonenumber/', destination: '4043', destinationcontext: 'from-internal', callerid: '"/somerandomphonenumber/" </somerandomphonenumber/>', channel: 'Local/4043@from-internal-0000032a;2', destinationchannel: 'SIP/4043-00000835', lastapplication: 'Dial', lastdata: 'SIP/4043,,trM(auto-blkvm)I', starttime: '2023-10-03 11:52:49', answertime: '2023-10-03 11:52:52', endtime: '2023-10-03 11:52:57', duration: '7', billableseconds: '4', disposition: 'ANSWERED', amaflags: 'DOCUMENTATION', uniqueid: '1696313269.25353', userfield: '' } { event: 'Cdr', privilege: 'cdr,all', accountcode: '', source: '/somerandomphonenumber/', destination: '4042', destinationcontext: 'from-internal', callerid: '"/somerandomphonenumber/" </somerandomphonenumber/>', channel: 'Local/4042@from-queue-00000327;2', destinationchannel: 'SIP/4042-0000082f', lastapplication: 'Dial', lastdata: 'SIP/4042,,trM(auto-blkvm)', starttime: '2023-10-03 11:52:30', answertime: '2023-10-03 11:52:36', endtime: '2023-10-03 11:52:49', duration: '18', billableseconds: '12', disposition: 'ANSWERED', amaflags: 'DOCUMENTATION', uniqueid: '1696313250.25287', userfield: '' } { event: 'Cdr', privilege: 'cdr,all', accountcode: '', source: '/somerandomphonenumber/', destination: '4042', destinationcontext: 'from-internal', callerid: '"/somerandomphonenumber/" </somerandomphonenumber/>', channel: 'Local/4042@from-queue-00000327;2', destinationchannel: 'Local/4043@from-internal-0000032a;1', lastapplication: 'Dial', lastdata: 'SIP/4042,,trM(auto-blkvm)', starttime: '2023-10-03 11:52:49', answertime: '2023-10-03 11:52:49', endtime: '2023-10-03 11:52:57', duration: '7', billableseconds: '7', disposition: 'ANSWERED', amaflags: 'DOCUMENTATION', uniqueid: '1696313250.25287', userfield: '' } { event: 'Cdr', privilege: 'cdr,all', accountcode: '', source: '/somerandomphonenumber/', destination: '4042', destinationcontext: 'from-internal', callerid: '"/somerandomphonenumber/" </somerandomphonenumber/>', channel: 'Local/4042@from-queue-00000327;2', destinationchannel: 'SIP/4043-00000835', lastapplication: 'Dial', lastdata: 'SIP/4042,,trM(auto-blkvm)', starttime: '2023-10-03 11:52:57', answertime: '2023-10-03 11:52:57', endtime: '2023-10-03 11:53:02', duration: '4', billableseconds: '4', disposition: 'ANSWERED', amaflags: 'DOCUMENTATION', uniqueid: '1696313250.25287', userfield: '' } { event: 'Cdr', privilege: 'cdr,all', accountcode: '', source: '/somerandomphonenumber/', destination: '602', destinationcontext: 'ext-queues', callerid: '"/somerandomphonenumber/" </somerandomphonenumber/>', channel: 'SIP/ntc_sip-0000082d', destinationchannel: 'Local/4042@from-queue-00000327;1', lastapplication: 'Queue', lastdata: '602,t,,,10,,,,,', starttime: '2023-10-03 11:52:27', answertime: '2023-10-03 11:52:27', endtime: '2023-10-03 11:53:02', duration: '34', billableseconds: '34', disposition: 'ANSWERED', amaflags: 'DOCUMENTATION', uniqueid: '1696313247.25273', userfield: '' }
可行解决方案
从提供的CDR日志可以看出,同一呼叫转移链的呼入CDR共享同一个uniqueid,而队列CDR拥有独立的uniqueid且包含整个呼叫的完整时长。基于这些特征,有以下两种核心解决思路:
思路1:基于uniqueid去重,仅处理一次
维护一个内存缓存记录已处理的呼叫uniqueid,当收到呼入CDR时,先检查该uniqueid是否已经处理过,跳过重复的CDR:
// 在类内部初始化缓存,用于记录已处理的呼叫ID private processedCallIds = new Map<string, boolean>(); this.ami.on('cdr', async (evt) => { // ... 原有变量定义逻辑 ... if ( !destinationcontext.includes('ivr') && !disposition.includes('CONGESTION') && !disposition.includes('FAILED') ) { // ... 原有caller、callLogEntry创建逻辑 ... // 修改呼入CDR处理逻辑,添加去重判断 if (source != landline && destinationcontext == 'from-internal') { // 若该uniqueid已处理过,直接跳过 if (this.processedCallIds.has(uniqueId)) { return; } // 标记为已处理 this.processedCallIds.set(uniqueId, true); callLogEntry.type = 'inbound'; await this.callLogService.create(callLogEntry); } // ... 原有其他类型呼叫处理逻辑 ... } });
思路2:仅处理整个呼叫链的最终事件
队列CDR的endtime是整个呼叫的最终结束时间,且包含完整的通话时长,可以直接基于队列CDR生成主通话日志,同时忽略转移过程中的中间呼入CDR:
this.ami.on('cdr', async (evt) => { // ... 原有变量定义逻辑 ... if ( !destinationcontext.includes('ivr') && !disposition.includes('CONGESTION') && !disposition.includes('FAILED') ) { // ... 原有caller、callLogEntry创建逻辑 ... // 仅保留队列CDR的处理逻辑,呼入CDR不再单独生成日志 if (source != landline && destinationcontext == 'ext-queues') { // 基于队列CDR生成完整的通话日志 const fullCallLogEntry = new CreateCalllogDto( caller, // 可以从呼叫链中提取最终接听的分机号,或保留队列号 receiver, startTime, answerTime, endTime, evt.endtime, holdTime, totalTime, callTime, disposition, 'inbound', uniqueId, ); await this.callLogService.create(fullCallLogEntry); // 原有队列日志逻辑保留 const queueLogEntry = new CreateQueueReceiveLogDto( caller, destinationchannel.substring( destinationchannel.indexOf('/') + 1, destinationchannel.indexOf('@'), ), receiver, 'ANSWERED', uniqueId, evt.endtime, ); if (process.env.NODE_ENV !== 'development') { await this.queueLogService.createQueueReceiveLog(queueLogEntry); } } // 注释或删除原有呼入CDR的处理逻辑 /* if (source != landline && destinationcontext == 'from-internal') { callLogEntry.type = 'inbound'; var result = await this.callLogService.create(callLogEntry); } */ // ... 原有其他类型呼叫处理逻辑 ... } });
额外优化:识别转移中间CDR直接跳过
通过CDR的destinationchannel字段可以识别转移的中间链路(比如包含Local/xxx@from-internal格式的记录),直接跳过这类中间CDR:
// 在处理呼入CDR前先判断是否为转移中间事件 if (source != landline && destinationcontext == 'from-internal') { // 如果是转移中间链路的CDR,直接跳过 if (destinationchannel.startsWith('Local/')) { return; } // 正常生成日志 callLogEntry.type = 'inbound'; await this.callLogService.create(callLogEntry); }
内容的提问来源于stack exchange,提问作者arpandhakal
相关产品推荐
相关产品推荐

