Каждый раз сталкиваясь с какой-то нестандартной задачей я радуюсь, радуюсь возможности снова погрузиться с головой в это чудесное состояние творчества, работы мысли. В последнее время такие задачи появляются часто и это здорово.
Обозначенные в заголовке были реализованы и работают, а значит пришло время поделиться с сообществом своими решениями.
Расскажу немного подробнее о каждой.
1. Организовать список номеров телефонов VIP-клиентов.
Звонки от VIP-клиентов должны попадать на первое место в очереди Asterisk, для скорейшей обработки именно их обращения. Так же нужно иметь возможность удобно добавлять и удалять контрагентов из этого списка.
2. Связать звонок клиента с конкретным оператором очереди на заданное время.
Настроить Asterisk так, чтобы в его «памяти», на какое-то заданное время, оставалась информация о том, какой из операторов очереди принял вызов. Позвонил человек с номера 8913*75*5*0 и попадает к оператору очереди Алёна и нужно сделать так, чтобы в течение, например суток, входящие звонки с этого номера принимала только Алёна и никто другой.
Но это еще не все, если клиент не хочет общаться с Аленой, то он может нажать клавишу * на своем телефоне и в следующий раз попадет уже к другому оператору.
С вступлением на этом заканчиваю, немного Python, MySQL и хитрого dialplan ждут вас под катом.
Список VIP-клиентов.
Приступив к реализации, я первым делом начал писать web-интерфейс для работы со списком контрагентов, но со временем понял, что это будет куда дольше чем найти что-то готовое. Действительно, спустя пол часа, у меня уже был развернут очень удобный вариант телефонной книги, написаной на php в связке с MySQL — как раз то, что нужно.
Большое спасибо разработчикам PHP Address Book за их труд.
Описывать установку не стану — все тривиально и очень подробно расписано в мануале, вложенном в архив проекта.
Интерфейс очень удобный и понятный
После внесения нужных данных я отправился писать Python-скрипт, который будет принимать от Asterisk CALLERID, обрабатывать его, делать запросы в БД и выставлять нужный приоритет позвонившему исходя из результатов.
Код у меня получился вот такой:
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import MySQLdb,sys,re
def WhatKindOfNumber(WKONnumber):
if re.match(r'^[78]3843(\d{6})$', WKONnumber):
# print "Если 11 цифр и начинается с [78]3843([78] + код города Новокузнецк): "
return WKONnumber[5:11],"our-town"
if re.match(r'^[3]843{(\d{6})$', WKONnumber):
# print "Если 10 цифр и начинается с 3843(код города Новокузнецк): "
return WKONnumber[4:10],"our-town"
if re.match(r'^[3](\d{6})$', WKONnumber):
# print "Если 7 цифр и начинается с 3(город Новокузнецк, только 7 цифр): "
return WKONnumber[1:7],"our-town"
if re.match(r'^(\d{6})$', WKONnumber):
# print "Если 6 цифр: "
return WKONnumber,"our-town"
if re.match(r'^\+(\d{11})$', WKONnumber):
# print "Если 11 цифр и начинается с +7: "
return WKONnumber[2:12],"mobile"
if re.match(r'^[78]9(\d{9})$', WKONnumber):
# print "Если 11 цифр и начинается с 89 или 79: "
return WKONnumber[1:11],"mobile"
if re.match(r'^[9](\d{9})$', WKONnumber):
# print "Если 10 цифр и начинается с 9: "
return WKONnumber,"mobile"
if re.match(r'^[78][^9](\d{9})$', WKONnumber):
# print "Если 11 цифр и начинается с 7 или 8, но не сотовый: "
return WKONnumber[1:11],"another-town"
if re.match(r'^[^9](\d{9})$', WKONnumber):
# print "Если 10 цифр и не сотовый: "
return WKONnumber,"another-town"
return WKONnumber,"default"
def agi_command(cmd):
print cmd
sys.stdout.flush()
return sys.stdin.readline().strip()
def mysqlconnect(sql):
db=MySQLdb.connect(host="127.0.0.1",port=3306,user="asterisk_user",passwd="password",db="asterisk")
cursor = db.cursor()
cursor.execute(sql)
sql = """SELECT FOUND_ROWS(); """
cursor.execute(sql)
row = cursor.fetchone()
db.close()
return row[0]
def main():
number, typeofnumber = WhatKindOfNumber(sys.argv[1])
if typeofnumber == "our-town" or typeofnumber == "default":
sql = """select SQL_CALC_FOUND_ROWS * from addressbook where mobile= '""" + number + """' or home= '""" + number + """' or work= '""" + number + """' or fax='""" + number + """' limit 1;"""
if typeofnumber == "mobile" or typeofnumber == "another-town":
sql = """select SQL_CALC_FOUND_ROWS * from addressbook where mobile like '%""" + number + """' or home like '%""" + number + """' or work like '%""" + number + """' or fax like '%""" + number + """' or mobile like '""" + number + """' or home like '""" + number + """' or work like '""" + number + """' or fax like '""" + number + """' limit 1;"""
result = mysqlconnect(sql)
if result == 0:
response = agi_command("EXEC Set QUEUE_PRIO=5")
if result > 0:
response = agi_command("EXEC Set QUEUE_PRIO=10")
sys.exit(0)
if __name__ == "__main__":
main()
В функции WhatKindOfNumber я обрабатываю полученный номер телефона, при необходимости привожу его к нужному мне виду и определяю его тип. Далее, в зависимости от типа, запрашиваю данные в БД и выставляю в Asterisk'е значение приоритета — 5 если номера нет и 10 если он есть.
БОльшее значение QUEUE_PRIO — бОльший приоритет.
Дело за малым, добавить строчку с вызовом AGI перед Queue.
Например так(диалпланы предпочитаю на ael — не обессудьте):
200601 => {
&recording(${CALLERID(num)},${EXTEN});
Answer();
AGI(vip_or_not.py,${CALLERID(num)});
Queue(first_TD,tT,,,20);
Hangup();
}
С этой задачей все, идем далее.
Связка клиент <-> оператор(менеджер).
Примем как данность, что взаимосвязь Asterisk и MySQL уже организована(если нет, то можете подсмотреть как это сделать тут).
Идем в mysql и создаем табличку, в которой у нас будут храниться записи о привязках клиентов к операторам.
mysql>use asterisk;
mysql> CREATE TABLE `numbers_remember` ( `id` int(9) unsigned NOT NULL auto_increment, `number` varchar(80) NOT NULL default 'NULL', `date` varchar(80), `agent` varchar(120) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `ix_phone` (`number`)) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=UTF8;
mysql> grant all on asterisk.* to 'asterisk_user'@'localhost' identified by 'password';
mysql> flush privileges;
Теперь внесем в func_odbc.conf запросы, которые будут выполняться из диалплана.
[GET_DATA]
dsn=asterisk
readsql=SELECT agent, date, number FROM asterisk.numbers_remember WHERE number='${ARG1}'
[SET_DATA]
dsn=asterisk
writesql=INSERT INTO asterisk.numbers_remember (number,date,agent) VALUES ('${SQL_ESC(${VAL1})}','${SQL_ESC(${VAL2})}', '${SQL_ESC(${VAL3})}')
[UPDATE_TIME]
dsn=asterisk
writesql=UPDATE asterisk.numbers_remember SET date='${SQL_ESC(${VAL1})}' WHERE number='${SQL_ESC(${VAL2})}'
[DELETE_DATA]
dsn=asterisk
writesql=DELETE FROM asterisk.numbers_remember WHERE number='${SQL_ESC(${VAL1})}' AND date='${SQL_ESC(${VAL2})}'
непосредственно сам диалплан:
globals {
TIMEOUT_OF_NUMBER=86400; // таймаут удержания номера в базе в секундах
};
1333 => {
Set(__DYNAMIC_FEATURES=delete_number_by_client);
//&recording(${CALLERID(number)},${EXTEN});
//Set(DB(clients/number)=${CALLERID(num)});
Set(__CALLFROMNUM=${CALLERID(num)});
Set(ARRAY(AGENT,DATE,NUMBER)=${ODBC_GET_DATA(${CALLERID(num)})});
if("${NUMBER}"!="") {
NoOp(== IF THE NUMBER ISN'T EQUAL "NULL" ==);
Set(DATERESULT=${MATH(${EPOCH}-${DATE},i)});
if(${DATERESULT}<${TIMEOUT_OF_NUMBER}) {
NoOp(== IF ${DATERESULT} < ${TIMEOUT_OF_NUMBER} ==);
Set(_NUM_TO_DEL=${CALLERID(NUM)});
&recording(${CALLFROMNUM},${EXTEN});
Dial(SIP/${AGENT},20,g);
if("${DIALSTATUS}"!="ANSWER") {
&recording(${CALLFROMNUM},${EXTEN});
Queue(Novokuznetsk,cnF);
Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});
Set(ODBC_DELETE_DATA()=${NUMBER},${DATE});
Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});
};
Set(ODBC_UPDATE_TIME()=${EPOCH},${NUMBER});};
if(${DATERESULT}>${TIMEOUT_OF_NUMBER}) {
NoOp(== IF ${DATERESULT} > ${TIMEOUT_OF_NUMBER} ==);
Set(ODBC_DELETE_DATA()=${NUMBER},${DATE});
&recording(${CALLFROMNUM},${CALLFROMNUM});
Queue(Novokuznetsk,cnF);
Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});
Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});
};
} else {
NoOp(== IF THE NUMBER DOESN'T EXIST IN DB ==);
&recording(${CALLFROMNUM},${EXTEN});
Queue(Novokuznetsk,cnF);
Set(AGENT=${CUT(MEMBERINTERFACE,/,2)});
NoOp(${CALLFROMNUM},${EPOCH},${AGENT});
Set(ODBC_SET_DATA()=${CALLFROMNUM},${EPOCH},${AGENT});
};
HangUp();
};
Логика диалплана такая:
Определяем есть ли в базе номер телефона, с которого нам пришел вызов.
1. Если нет, то идем в последний else NoOp(== IF THE NUMBER DOESN'T EXIST IN DB ==); и отправляем звонок в очередь, после чего присваиваем переменной AGENT значение ответившего оператора. И инсертим в БД — НОМЕР, ДАТУ в UTC, АГЕНТА.
2. Если номер в БД есть, то проверяем время. Если дата в БД менее TIMEOUT_OF_NUMBER, то отправим звонок конкретному агенту и обновим время, если больше, то в очередь.
*для того, чтобы иметь возможность заполучить значение переменной MEMBERINTERFACE нужно в конфиге КАЖДОЙ очереди указать параметр setinterfacevar=yes
у меня выглядит так:
[general]
persistentmembers = yes
autofill = yes
updatecdr=yes
[StandardQueue](!)
setinterfacevar=yes
music=default
strategy=rrmemory
timeout = 12
retry = 1
timeoutpriority = conf
joinempty=yes
leavewhenempty=no
ringinuse=yes
[Novokuznetsk](StandardQueue)
...
[Kemerovo](StandardQueue)
...
[Mejdurechensk](StandardQueue)
...
Регулярно удаляем устаревающие записи из БД.
Я написал вот такой скрипт на bash:
#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TIMEOUT_OF_NUMBER=`grep TIMEOUT_OF_NUMBER= /etc/asterisk/extensions.ael| sed s/[^0-9]//g`
CURRENT_DATE=`date +%s`
THRESHOLD_DATE=$(($CURRENT_DATE-$TIMEOUT_OF_NUMBER))
mysql -e "delete from numbers_remember where date<$THRESHOLD_DATE;" -uroot -p123 asterisk
Запускается по крону, хоть раз в минуту — зависит от значения TIMEOUT_OF_NUMBER
Для возможности удаления привязки позвонившим клиентом нужно добавить в features.conf вот такую строчку
delete_number_by_client => *,peer,Macro,delnum
и тогда, если в момент разговора клиент нажмет *, привязка удалится из таблицы.
За основу этого решения я взял статью — habrahabr.ru/post/204048,
но там автор скромно умолчал многие нюансы.
Заключение.
У меня осталось приятное послевкусие от успешно реализованных задач, Asterisk — это целый мир и порой от возможностей, в нем открывающихся, голова идет кругом. Это потрясающее ощущение, когда долго над чем-то работаешь, сумеешь победить, а потом еще и поделишься с другими людьми — будешь кому-то полезным.
На этом я заканчиваю, любите свой труд, удачи вам и интересных, сложных задач!
Комментарии (11)
ragequit
04.11.2015 09:24-3Обеспечим VIP-клиентам первое место в очереди звонков
С позиции предоставления сервиса ААА-класса все клиенты, которые вынуждены обращаться по телефону, а не имеют прямых контактов с руководством/уже работают с вами, должны иметь единый статус. Т.е. все клиенты — VIP-клиенты. Иначе это местячковый шараж-монтаж.
а так же свяжем клиента с конкретным оператором на заданное время
Персонал call-центра/саппорта должен обладать достаточной квалификацией для того, чтобы «подхватывать» работу своих коллег без ущерба коммуникации, либо структура этих организаций и их работа должна быть отлажена таким образом, чтобы «ведение» клиента было само собой разумеющимся на любых этапах общения. Велосипед тут выдумывать не стоит, приемлемо банальное ведение хистори и «Здравствуйте, спасибо за ваш звонок. Вы уже обращались к нам? Да, конечно, сейчас мы переключим Вас на специалиста, который уже работал с Вами, или же, если хотите, предоставим другого».
Все.FessAectan
04.11.2015 12:07+1Реализация списка важных клиентов была предоставлена для службы такси и если руководство посчитало, что им нужно сделать так, то пусть будет так.
ИТ — это инструмент, каждый применяет его так, как считает нужным.
Касаемо второго, о связи клиент-менеджер(оператор).
Задача решалась для гильдии перевозчиков нашей области, т.е. в очередь на этом астериске добавлены другие АТС со своими механизмами и операторами обрабатывающими заказы, все перевозчики используют один номер — 1333 и звонки распределяются между ними.
Zagrebelion
04.11.2015 09:24+1у вас какие-то удивительно запутанные регекспы. Например,
if re.match(r'^[8]{1,1}[9]{1,1}(\d{9,9})$', WKONnumber) or re.match(r'^[7]{1,1}[9]{1,1}(\d{9,9})$', WKONnumber):
можно записать так
if re.match(r'^89(\d{9})$', WKONnumber):Zagrebelion
04.11.2015 09:40+3то есть, if re.match(r'^[78]9(\d{9})$', WKONnumber):
FessAectan
04.11.2015 12:11с регекспами был первый опыт,
спасибо за исправление — внесу в скриптMaximChistov
04.11.2015 13:21а с + номер точно не будет? он что по первму регекспу, что по поправленному, не пройдет проверку(просто добавьте "+?" сразу после ^)
FessAectan
04.11.2015 13:54проверку номер с плюсом проходит
./vip_or_not.py +79130751580 Если 11 цифр и начинается с +7: 9130751580 mobile EXEC Set QUEUE_PRIO=10
FessAectan
Если у кого-то нет аккаунта на Хабре, но будут вопросы — в профиле есть контакты.
fleaump
mysql schedule
и база сама следит за чистоплотностью.