Індивідуальний денний ліміт на вихідні дзвінки (обмеження платних напрямків)

У цій статті хочу розповісти, як ми вирішили не типову задачу на FreePBX. Під визначенням «не типову» я маю на увазі, що її не можна вирішити стандартними засобами, без додаткових інструментів.

Передісторія
Є група працівників, яка займається обзвоном клієнтів. Щоб економити на вихідних дзвінках, що для різних напрямків використовуються різні номери телефонів. Це спокійно вирішується за допомогою шаблонів (масок) номерів в Outbound Routes. Але частина напрямів, наприклад, дзвінки на мобільні, залишається платним. Щоб у кінці місяця рахунок компанії за телефонні послуги не перевалив XXX$, необхідно жорстко контролювати і, при необхідності, обмежувати відповідні напрямки.

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

Приступаємо до виконання
Для початку потрібно визначити на які номери ми обмежуємо дозвон. У нашому випадку це мобільні оператори Казахстану. Знаходимо відповідну статтю в Википедии і намагаємося сформувати шаблони (маски) номерів. Так як FreePBX не має можливості використовувати повноцінні регулярні вирази, 23 можливих префікса нам вдалося упакувати в 3 шаблону:
  • 870[5780-2]XXXXXXX
  • 877[15-8]XXXXXXX
  • 8747XXXXXXX
Створюємо відповідні записи в Outbound Routes. На даному прикладі відкриваємо напрямки для внутрішнього номери 2055:



Робимо це для того, щоб відповідні правила створилися в конфігураційному файлі
/etc/asterisk/extensions_additional.conf 

Так як при редагуванні та застосування налаштувань під FreePBX, система кожен раз переписує конфігураційні файли, ми знаходимо потрібні нам блоки і переміщаємо в файл
/etc/asterisk/extensions_custom.conf
в який FreePBX не лізе.

Блок наступного виду:

Outbound Routes
розширеннями => _877[15-8]XXXXXXX,1,Macro(user-callerid,LIMIT,EXTERNAL,)
розширеннями => _877[15-8]XXXXXXX/2055,1,Macro(user-callerid,LIMIT,EXTERNAL,)
розширеннями => _877[15-8]XXXXXXX/2055,n,ExecIf($[ "${CALLEE_ACCOUNCODE}" != "" ] ?Set(CDR(accountcode)=${CALLEE_ACCOUNCODE}))
розширеннями => _877[15-8]XXXXXXX/2055,n,Set(MOHCLASS=${IF($["${MOHCLASS}"=""]?default:${MOHCLASS})})
розширеннями => _877[15-8]XXXXXXX/2055,n,ExecIf($["${KEEPCID}"!="TRUE" & ${LEN(${TRUNKCIDOVERRIDE})}=0]?Set(TRUNKCIDOVERRIDE=<7123456789>))
розширеннями => _877[15-8]XXXXXXX/2055,n,Set(_NODEST=)
розширеннями => _877[15-8]XXXXXXX/2055,n Gosub(sub-record-check,s,1(out,${РОЗШИРЕННЯМИ},))
розширеннями => _877[15-8]XXXXXXX/2055,n,Макрос(dialout-trunk,10,${РОЗШИРЕННЯМИ},,off)
розширеннями => _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) DEFAULT CHARACTER SET utf8 '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.



Приступаємо до формування основної системи, логіка наступна:
  • за розкладом (крону) скрипт перевіряє скільки наговорив кожен абонент за потрібне нам напрямами;
  • якщо абонент перейшов поріг 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 ми будемо динамічно формувати дозволяють блоки, що дозволить більш гнучко використовувати систему, при будь-яких змінах.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.