Индивидуальный дневной лимит на исходящие звонки (ограничение платных направлений)

в 5:32, , рубрики: asterisk, asteriskcdrdb, freebpx, mysql, python

В этой статье хочу рассказать, как мы решили не типовую задачу на FreePBX. Под определением «не типовую» я имею в виду, что ее нельзя решить стандартными средствами, без дополнительных инструментов.

Предыстория

Есть группа сотрудников, которая занимается обзвоном клиентов. Дабы экономить на исходящих звонках, для разных направлений используются разные номера телефонов. Это спокойно решается с помощью шаблонов (масок) номеров в Outbound Routes. Но часть направлений, например, звонки на мобильные, остается платным. Чтобы в конце месяца счет компании за телефонные услуги не перевалил XXX$, необходимо жестко контролировать и, при необходимости, ограничивать соответствующие направления звонков.

Задача

Установить индивидуальный дневной лимит для группы менедежеров. Запретить исходящие звонки на определенные направления при исчерпании лимита. При достижении пороговых значение: >50%, >90% и >100% отправлять соответствующее уведомление на email сотрудника. Если сотрудник в течении дня полностью не исчерпал свой дневной лимит, остаток должен перейти на следующий день.

Приступаем к выполнению

Для начала нужно определить на какие номера мы ограничиваем дозвон. В нашем случае это мобильные операторы Казахстана. Находим соответствующую статью в Википедии и стараемся сформировать шаблоны (маски) номеров. Так как FreePBX не имеет возможности использовать полноценные регулярные выражения, 23 возможных префикса нам удалось упаковать в 3 шаблона:

  • 870[5780-2]XXXXXXX
  • 877[15-8]XXXXXXX
  • 8747XXXXXXX

Создаем соответствующие записи в Outbound Routes. На данном примере открываем направления для внутреннего номера 2055:

Индивидуальный дневной лимит на исходящие звонки (ограничение платных направлений) - 1

Делаем это для того, чтобы соответствующие правила создались в конфигурационном файле:

/etc/asterisk/extensions_additional.conf 

Так как при редактировании и применении настроек во FreePBX, система каждый раз переписывает конфигурационные файлы, мы находим нужные нам блоки и перемещаем в файл:

/etc/asterisk/extensions_custom.conf

в который FreePBX не лезет.

Блок следующего вида:

Outbound Routes

exten => _877[15-8]XXXXXXX,1,Macro(user-callerid,LIMIT,EXTERNAL,)
exten => _877[15-8]XXXXXXX/2055,1,Macro(user-callerid,LIMIT,EXTERNAL,)
exten => _877[15-8]XXXXXXX/2055,n,ExecIf($[ "${CALLEE_ACCOUNCODE}" != "" ] ?Set(CDR(accountcode)=${CALLEE_ACCOUNCODE}))
exten => _877[15-8]XXXXXXX/2055,n,Set(MOHCLASS=${IF($["${MOHCLASS}"=""]?default:${MOHCLASS})})
exten => _877[15-8]XXXXXXX/2055,n,ExecIf($["${KEEPCID}"!="TRUE" & ${LEN(${TRUNKCIDOVERRIDE})}=0]?Set(TRUNKCIDOVERRIDE=<7123456789>))
exten => _877[15-8]XXXXXXX/2055,n,Set(_NODEST=)
exten => _877[15-8]XXXXXXX/2055,n,Gosub(sub-record-check,s,1(out,${EXTEN},))
exten => _877[15-8]XXXXXXX/2055,n,Macro(dialout-trunk,10,${EXTEN},,off)
exten => _877[15-8]XXXXXXX/2055,n,Macro(outisbusy,)

Если вы дружите с синтаксисом конфигов Asterisk'а, можно пропустить два предыдущих шага и сформировать нужные вам блоки самостоятельно.

Теперь можно удалить созданные ранее Outbound Routes, нужные нам разрешающие правила теперь содержатся в extensions_custom.conf. Таким образом, мы разрешили сотрудникам звонить по этим направлениям. Дальше больше.

Так как лимит является индивидуальным, и нам нужно рассылать уведомления на почту, необходимо где-то хранить всю эту информацию. Лучшим выбором будет использование базы данных. Тут у нас было два варианта:

  • использовать существующую базу данных asterisk, и добавить необходимые нам поля в таблицу users;
  • создать свою базу данных с таблицами нужной структуры.

Выбор пал на вариант№2, и получилось примерно следующее:

SQL Create table

CREATE TABLE `users` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'ФИО пользователя',
  `extension` varchar(5) CHARACTER SET utf8 DEFAULT '000' COMMENT 'Внутренний номер абонента',
  `mobile_limit_flag` int(11) DEFAULT '0' COMMENT 'Флаг для учета текущего лимита',
  `mobile_limit` int(11) DEFAULT '0' COMMENT 'Текущий лимит',
  `base_mobile_limit` int(11) DEFAULT NULL COMMENT 'Базовый лимит',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

Описание основных параметров:

  • base_mobile_limit хранит индивидуальный лимит абонента (в секундах), устанавливается единовременно;
  • mobile_limit содержит текущий лимит на текущий день, с учетом не израсходованных минут;
  • mobile_limit_flag определяет какой порог исчерпания лимита преодолел пользователь (0 — <50%, 1 — >50 и <90%, 2 — >90% и <100%, 3 — >100%);

Создадим Васю Пупкина, с уже известным нам внутренним номером 2055.

Индивидуальный дневной лимит на исходящие звонки (ограничение платных направлений) - 2

Приступаем к формированию основной системы, логика следующая:

  • по расписанию (по крону) скрипт проверяет сколько наговорил каждый абонент по нужным нам направлениям;
  • если абонент перешел порог 0,1 или 2, параметр mobile_limit_flag меняется на соответствующий и отправляется сообщение на email;
  • если абонент оказался на пороге 3 (лимит полностью исчерпан), отправляется соответствующее уведомление на email, в конфигурационном файле комментируется соответствующий блок, выполняется dialplan reload.

Для хранения позиций блоков соответствующих внутренних номеров, сформируем XML файл следующего вида:

XML

<?xml version="1.0" encoding="UTF-8" ?>
<bocks>
	<!-- BLOCKS START -->
	<block number="2055">
		<element first="4" last="11"/>
		<element first="117" last="124"/>
		<element first="230" last="237"/>
	</block>
	<block number="2066">
		<element first="14" last="21"/>
		<element first="127" last="134"/>
		<element first="240" last="247"/>
	</block>
	<block number="2077">
		<element first="24" last="31"/>
		<element first="137" last="144"/>
		<element first="250" last="257"/>
	</block>
<bocks>

Так как мы создали три маски для выхода на мобильные номера, для каждого внутреннего номера будет по три разрешающих блока. В XML мы указываем номера строк начала и конца каждого из этих блоков. Комментируем их с помощью следующего кода:

Функция комментирования блока

def commentBlocks(numb):
	import xml.etree.cElementTree as ET
	tree = ET.ElementTree(file='conf.xml')
	root = tree.getroot()
	f = open(r'extensions_custom.conf')
	lines = f.readlines()
	f.close()
	for elem in tree.iterfind('block[@number="'+numb+'"]/element'):
	    lines[int(elem.get('first'))-2] = ";--n"
	    lines[int(elem.get('last'))] = "--;n"
	f = open(r'extensions_custom.conf','w')
	f.writelines(lines)
	f.close()

И собственно основные мозги:

Main script

Дергается по расписанию, например, каждые 5 минут. Бонусом великолепный SQL запрос, и божественный код.

#мои функции
import send_email
import flags

print ('###########START_MOBILE_LIMIT############')

import pymysql
mainconn = pymysql.connect(host='10.10.2.1', user='user', passwd='password', db='asteriskcdrdb', charset='utf8')
maincur = mainconn.cursor()
maincur.execute("""SELECT SUM(billsec) AS sec, src 
	FROM cdr WHERE disposition = 'ANSWERED' 
	AND (dst LIKE '8700%' OR dst LIKE '8701%' 
	OR dst LIKE '8702%' OR dst LIKE '8705%' OR dst LIKE '8707%' 
	OR dst LIKE '8708%' OR dst LIKE '8747%' OR dst LIKE '8771%' 
	OR dst LIKE '8775%' OR dst LIKE '8776%' OR dst LIKE '8777%' 
	OR dst LIKE '8778%') AND DATE(calldate) = DATE(CURDATE()) 
	AND src in (2055,2066,2077)
	GROUP BY src;""")

row = maincur.fetchone()

print ('ROW COUNT: ' + str(self.maincur.rowcount))

while row is not None:

	#row[1] - внутренний номер
	#row[0] - исчерпанный лимит в секундах

	#изменяем текущий лимит
	flags.UpdateUserCurrentLimit(str(row[1]), str(row[0]))

	per = row[0] * 100 / flags.checkUserLimit(row[1])
	flag = flags.checkFlag(row[1])
	#вытаскиваем почтовый ящик абонента, ищем его по внутреннему номеру
	manager_mail = send_email.getEmail(row[1])

	#показываем % израсходонного трафика
	print (row[1] + ' (' + str(round(per,0)) + '%): ' + str(row[0]))

	#проверяем порог
	if per >= 50 and per < 90:
		message = 'Nomer ' + row[1] + ', limit ischerpan na ' + str(round(per, 0)) + '%'
		if flag == 0:
			print ('go email to ' + send_email.getEmail(row[1]))
			send_email.send_message(manager_mail, message)
			flags.changeFlag(row[1], 1)
			flags.insertLog(row[1], per)
		print (message)
	elif per > 90 and per < 100:
		message = 'Nomer ' + row[1] + ', limit ischerpan na ' + str(round(per, 0)) + '%'
		if flag == 1:
			print ('go email to ' + send_email.getEmail(row[1]))
			send_email.send_message(manager_mail, message)
			flags.changeFlag(row[1], 2)
			flags.insertLog(row[1], per)
		print (message)
	elif per >= 100:
		message = 'Nomer ' + row[1] + ', limit polnostiu ischerpan'
		if flag != 3:
			print ('go email to ' + send_email.getEmail(row[1]))
			send_email.send_message(manager_mail, message)
			flags.changeFlag(row[1], 3)
			flags.insertLog(row[1], per)
			#комментируем соответствующие блоки в конфиге
			flags.commentBlocks(str(row[1]))
			import subprocess
			#дергаем диалплан чтобы применить настройки
			subprocess.call(['./dialplan_reload.sh'])
		print (message)

	row = maincur.fetchone()

maincur.close()
mainconn.close()

print ('############END_MOBILE_LIMIT#############')

В конце рабочего дня мы добавляем неиспользуемый лимит:

AddUnusedLimit

def AddUnusedLimit(ext):
	conn = pymysql.connect(host='10.10.2.2', user='user', passwd='password', db='crm', charset='utf8')
	cur = conn.cursor()

	cur.execute ("""
	   UPDATE users
	   SET mobile_limit=base_mobile_limit+mobile_limit WHERE extension=%s
	""", (ext))

	conn.commit()
	print('changed', cur.rowcount)
	cur.close()
	conn.close()

Сбрасываем mobile_limit_flag на дефолтное значение 0 и раскоменчиваем все блоки:

uncommentBlocks

def uncommentBlocks():
	import xml.etree.cElementTree as ET
	tree = ET.ElementTree(file='conf.xml')
	root = tree.getroot()
	for elem in tree.iterfind('block/element'):
		first = int(elem.get('first'))
		last = int(elem.get('last'))
		lines[first-2] = "n"
		lines[last] = "n"
f = open(r'/etc/asterisk/extensions_custom.conf')
lines = f.readlines()
f.close()

############################################

cur.execute ("""
	   UPDATE users
	   SET mobile_limit_flag=%s
	""", (0))

Для решения возможных спорных ситуаций, пишем в лог данные об изменении порога лимита:

LOG

#функция логирования израсходованного лимита
def insertLog(ext, per):
	import pymysql
	conn = pymysql.connect(host='10.10.2.1', user='user', passwd='password', db='crm', charset='utf8')
	cur = conn.cursor()

	cur.execute ("""
	   INSERT INTO mobile_limit
	   (extension, percent)
	   VALUES (%s, %s)
	""", (ext, per))

	conn.commit()
	print('insert', cur.rowcount)
	cur.close()
	conn.close()

Вот такое кривое решение поставленной задачи. В версии скрипта 2.0 мы будем динамически формировать разрешающие блоки, что позволит более гибко использовать систему, при каких-либо изменениях.

Автор: Evgenius0307

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js