Asterisk. Разгружаем секретаря/диспетчера/первую линию тех. поддержки

    Надеюсь, что этой статьей получится превратить статьи по конфигурации Asterisk в целый цикл статей, начало которому я, для себя положил около 2-х лет назад в этой статье.

    Я стараюсь описывать свою разработку (а возможно кому-то и приподниму завесу тайны) нетривиальных, или просто интересных, по моему мнению, диалпланов.

    Я сразу оговорюсь, что не буду описывать настройку модулей, конфигурационных файлов подключения к базе данных и тому подобного, так как это не цель статьи, тем более все это есть в wiki и в книгах по Asterisk. Так же я сразу скажу что делаю все это на голом Asterisk без freePBX так как считаю реализацию этого веб-интерфейса неправильной, нелогичной и ущербной.

    Начну я с одного из сценариев в организациях — разгрузки секретаря. Эту же логику будет возможно применить и к Call центрам, и диспетчерам такси (немного изменив Dialplan).

    Сценарий:
    -Звонок на секретаря.
    -Сектретарь поднимает трубку -просят соединить с отделом продаж.
    -Секретарь переводит звонок на группу менеджеров.
    -Менеджер разговаривает с клиентом.
    -При повторном звонке в течении 24 часов клиент попадает сразу к принявшему его впервые менеджеру.

    Кому интересно прошу под кат.

    Отступление:
    Предугадывая вопрос Зачем? в комментариях:
    Как правило у клиентов с менеджерами достаточно тесное общение, но не всегда удобно давать свой личный номер телефона- он может забыться или потеряться. Менеджер может забыть и не дать его ну или еще что-то приключится с ним и клиент останется без прямой связи. У секретаря так же есть ряд обязанностей и работать телефонистом полный рабочий день ему/ей накладно. Даже при наличии внутреннего номера у каждого менежера всегда проще запомнить 6-10 цифр телеонного номера вместо 13-15.


    Задача:
    — Избавить секретаря от перевода одного и того же клиента на менеджеров в течение определенного промежутка времени.
    — Привязать клиента к отпределенному менеджеру

    Логика реализации:
    После того как менеджер взял трубку Asterisk «запомниает» номер клиента и при повторном звонке в ближайшие 24 часа соединяет клиента именно с тем менеджером который с ним общался.

    Реализация.

    Asterisk должен как то узнавать клиента который ему звонит, а так же должен запоминать менеджера, который поднял трубку. Эта информация будет храниться в базе данных и будет доступна астериску по запросу.

    Создаем базу данных или заводим таблицу в уже имеющейся (Я использовал mysql).

    Cозданная таблица имеет 4 поля:
    id Уникальное значение строки
    number Номер клиента
    date Дата в UTC
    agent Номер агента принявшего вызов

    Все нужные обращения к таблице я уложил в 4 запроса:

    GET_DATA Как видно из названия: получаем данные о позвонимшем клиенте ориентируясь на его номер
    SELECT agent, date, number FROM dbname.clients WHERE number=Номер клиента.

    SET_DATA Записываем данные в базу
    INSERT INTO dbname.clients (number,date,agent) VALUES ('Номер клиента','Дата в UTC','Номер агента принявшего вызов')

    UPDATE_TIME Обновляем время при повторном звонке
    UPDATE dbname.clients SET date=Текущая дата WHERE number=Номер клиента

    DELETE_DATA Удаляем клиента из базы
    DELETE FROM dbname.clients WHERE number=Номер клиента AND date=Дата в UTC

    Теперь, кода инструмент для работы с базой готов осталось написать dialplan который будет обрабатывать это соединение.

    Шаг первый- опознаем клиента:
    Все входящие звонки изначально приходят к секретарю (контекст from-external). Каждый такой звонок- потенциальный клиент, поэтому мы будем проверять — а не зонил ли он нам уже?
    Для этого сначала запросим в таблице значения номера агента, даты и номера звонящего с помощью запроса GET_DATA

    [from-external]
    exten=>_X.,1,Set(ARRAY(AGENT,DATE,NUMBER)=${ODBC_GET_DATA(${CALLERID(num)})")


    Если в базе такого номера нет то переменная number останется пустой- это и будет нашим основным параметром сравнения для того чтобы понять в какую сторону пойдет диалплан далее:

    exten=>_X.,n,GotoIf($[${NUMBER}!=""]?comparedate:dialexten)

    Если переменная number не пуста, значит клиент уже звонил нам, следующим шагом нужно узнать как давно он нам звонил. Для этого нужно сравнить дату в таблице с датой текущей, для этого идем по ветке comparedate, вычитанем из текущей даты значение переменной date полученное на самом первом шаге и сохраняем все это в переменной DATERESULT:

    exten=>_X.,n(comparedate),Set(DATERESULT=${MATH(${EPOCH}-${DATE},i)})

    Затем сравниваем ее с магическим числом 86400 (не буду раскрывать секрет этого числа, знающие сами поймут, а незнающие посмотрят в википедии, что такое Unix Time Stamp, расширят свой кругозор и вспомнят математику), и исходя из того какой результат получили уходим по веткам dialagent и deleteagent. Здесь важно обратить внимание на синтаксис — без кавычек и с проблеом между знаком "<" и значениями, если написать по-другому GotoIf отработает некорректно:

    exten=>_X.,n,GotoIf($[${DATERESULT} < 86400]?dialagent:deleteagent)

    Метка dialagent отвечает за вызов менеджера к которому прикреплен клиент. После вызова мы обновляем значение времени, так как сотрудничество продолжается:

    exten=>_X.,n(dialagent),Dial(SIP/${AGENT},,Ttg)

    exten=>_X.,n,Set(ODBC_UPDATE_TIME()=${EPOCH},${NUMBER})

    exten=>_X.,n,Hangup()


    Метка deleteagent переводит диалплан в ветку удаления вызова секретаря с предварительным удалением из базы данных пользователя, происходит это когда пользователь не звонил в компанию более 24 часов.

    exten=>_X.,n(deleteagent),Set(ODBC_DELETE_DATA()=${NUMBER},${DATE})
    exten=>_X.,n(dialexten),Dial(SIP/${EXTEN},,Ttg)


    Примечание:
    Я не стал писать удаление записи из базы по истечению времени через диалплан asterisk потому что в данной реализации это костыль и гораздо правильнее было бы запускать раз в сутки по cron скрипт хотя бы на том же php и чистить базу от посроченных записей


    На этом определение статуса звонящего пользователя закончивается и происходит перевод пользователя в очередь.
    Важно при переводе использовать blindtransfer, чтобы CALLERID звонящего клиента высветился у менеджера. Если же ну очень надо использовать extended transfer то стоит перед переводом установить CALLERID секретаря в значение CALLERID клиента, чтобы в таблицу в поле number попал именно номер клиента, а не секретаря. Это первое решение коорое мне пришло в голову. если у кого то возникнут еще идеи — буду рад услышать.

    И так, при переводе по blindtransfer на номер очереди нужно учесть 1 момент:
    Нам понадобится номер менеджера, поднявшего трубку. Здесь я отойду от правила не углубляться в конфиги, потому, что чтобы получить это значение в конфигурационном файле очереди необходимо выствить значение setinterfacevar=yes. Эта настройка позволит видеть значение переменной MEMBERINTERFACE, которая в себе как раз и хранит номер члена очереди, поднявшего трубку.
    Чтобы ее обработать можно пойти 2-мя путями:

    1. Использовать макрос в вызове очереди:

    exten=>500,1,Queue(Queue1,t,,,,,queue-answer)

    [macro-queue-answer]

    exten => s,1,Set(AGENT=${CUT(MEMBERINTERFACE,/,2)})


    2. Использовать hangup экстеншн

    exten=>500,1,Queue(Queue1,t)

    exten => h,1,Set(AGENT=${CUT(MEMBERINTERFACE,/,2)})


    Проблема в том, что по оканчанию вызова, чтобы занести менеджера в базу данных необходимо получить его CALLERID. Способ 2 не пробрасывает CALLERID менеджера в макорс, а передача параметра в макрос при вызове его из очереди невозможна — как я не пытался, пробросить его у меня не получилось. Поэтому способ был обматерен и проклят, и я остановился на способе 1.

    С этим способом так же есть нюанс- если я буду отлавилвать событие hangup в контексте from-external, то любой hangup (будь то менеджер или секретарь) попадут под обработку, а этого не нужно. Поэтому при переводе звонка в очередь, сама очередь переводится в новый контекст, в котором и обрабатывается:

    exten=>500,1,Answer()

    exten=>500,n,Goto(queue-answering,s,1)

    [queue-answering]

    exten=>s,1,Queue(Queue1,t)

    exten => h,1,Set(AGENT=${CUT(MEMBERINTERFACE,/,2)})


    После того как получен удобочитаемый номер менеджера, осталось записать данные в базу:

    exten => h,n,Set(ODBC_SET_DATA()="${CALLERID(num)}","${EPOCH}","${AGENT}")

    На этом диалплан закончен. его можно улучшить и усовершенствовать добавив IVR при обработке вызова уже звонившего клиента и предлагая ему разные варианты совершения вызова. Так же можно добавить анонс звонка для менеджера, когда он берет трубку, чтобы он знал что, это звонок от нового клиента, или просто от клиента.

    В общем и целом данный диалплан это базис, на основе которого можно развернуть очень удобное и недорогое для производительности asterisk решение (не прибегая к AGI например), которое используют как фирмы такси, так и некоторые call-центры.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 27
    • 0
      В целом интересно, но я бы сделал не 86400 а 129600, потому что на следующий день клиент может позвонить позже чем он звонил первый раз и уже попадет на секретаря.
      • 0
        Ну вот… Всю тайную тайну раскрыли...))
        Теоретически я бы и 3 суток поставил, но заказ был на сутки: хозяин-барин.
        • 0
          Еще нужно иметь ввиду, что разные клиенты могут звонить через SIP и у них может определяться один и тот же номер.
          • 0
            Я думал об этом, но такие случаи не предугадаешь. Точка входа- телефонный номер, вообще можно и по имени (CALLERID(name)) фильтовать, и по IP… Ну в общем все зависит от степени погружения, если можно это так назвать.
            • 0
              Согласен, но можно внести в исключения известные номера SIP провайдеров.
      • 0
        Еще таким же образом, можно сделать в обратном порядке, когда из офиса звонят клиенту и если он перезванивает, то попадает не на секретаря, а на того кто ему звонил последний раз!
        • 0
          Отличная фишка. Очень ее не хватает, когда долго ждешь в очереди, попадаешь на неизвестный ext, долго ему втолковываешь проблему, связь рвется, а ты даже не знаешь с кем разговаривал. И опять все по новой, другому сотруднику. Статья натолкнула на мысль — а не сделать ли анонсирование номера ext перед соединением из очереди или группы.
          • +1
            Вот уже за это плюсанул!
            Так же я сразу скажу что делаю все это на голом Asterisk без freePBX так как считаю реализацию этого веб-интерфейса неправильной, нелогичной и ущербной.

            А потом продолжил читать. Задумка интересная, правда на небольшой нагрузке я возможно пользовался бы astdb вместо внешней БД.
            • 0
              мне возможностей AstDB для реализации не хватило. Не могу представить как запизнуть 3/4 значения с 2 поля в key/value, да и потом все больше сталкиваюсь с тем что системы растут очень быстро, и вместо того чтобы потом переделывать прихожу к тому, что лучше сразу «запилить» систему с запасом.
            • +1
              Два сценария:
              1. клиент через секретаря попадает на менеджера, в процессе общения, менеджер переводит клиента на бухгалтера, звонок срывается. Куда теперь попадёт клиент при попытке перезвонить?
              2. клиент совершил свой «первый» звонок в понедельник в 16.30. На следующий день менеджер взял больничный (автофорварды и прочие плюшки по каким-то причинам отключены). Куда теперь попадёт клиент при попытке перезвонить?
              • 0
                1. Сценарий не предполагает перевода клиента на бухгалтера, потому что Зачем? переводить клиента на внутреннюю стуктурную единицу, ну и потом если уж так сильно надо можно же и поменять ход диалплана и записывать в базу например сразу после дозвона менеджеру.

                2. В данном диалплане он попадет все равно на своего менеджера, потому что телефон не болеет в отличие от менеджера, как раз для этого можно кастомизировать диалплан поставив скажем перед вызовом менеджера небольшой IVR с предложением позвонить своему менеджеру нажав 1 или набрать секретаря нажав 2. Ведь ничего не мешает это сделать.
                • +1
                  1. Зачем? — Это, наверное, риторический вопрос, но требования бизнеса могут быть очень разные :) Если говорить про контору типа books.ru, это, наверное, не потребуется. Если говорить про SMB, то я не сильно задумываясь могу предложить вариант из дружественной мне конторы (поставка оборудования), где для того чтобы сделать клиенту действительно хорошо есть спецы по «направлениям». Один отвечает за компьютеры и комплектуху, второй за сервера и их комплектуху, третий за оборудование аудиомонтажа, четвёртый за оборудование видеомонтажа, пятый занимается исключительно логистикой и доставкой товара клиенту. Ни один, даже самый прекрасный менеджер, не сможет проконсультировать по всем этим видам оборудования, подсказать особенности и недостатки каждого, а так же подсказать, в зависимости от целей клиента, что лучше ему подойдёт.

                  2. Ну и в таком случае клиент попав первый раз «на своего» менеджера у которого телефон бренчит на столе и ни кому до этого нет дела, т.к. все и так говорят. Будет вынужден звонить еще раз. Скажу честно, если заказ на стадии формирования и т.п., я бы на второй раз начал звонить в соседнюю фирму.

                  • 0
                    1. Собсвтенно ответ на решение проблемы выше изложен
                    2. Ни что не мешает настроить последовательную переадресацию скажем снова на группу менеджеров при продолжительности звонка в N секунд. Тогда даже если менеджер не способен ответить- при первом же звонке дольше N секунд звонок будет перведен на группу менеджеров, а дальше как хотите- можете перезаписать агента, можете оставить тем же и использовать переадресацию. Если в ближайшее время появится свободная минутка-постараюсь реализовать этот момент, ЗАмечание то правильное и интересное.
                    В любом случае сложного тут ничего нет. Диалплан легко поддается кастомизации и расширению — благо там все прозрачно. Как говорится- было бы желание)
                    • 0
                      Вот за это я Астер и обожаю! Выложил кто-то хорошую идею насчёт какой-то фишки, а потом сидишь и думаешь, что «у меня в чистом виде работать не будет, но вот если добавить… и поменять ..., то выйдет конфетка!»

                      А в «железячных» решениях такой кастом почти невозможен…
                  • 0
                    Спасибо кстати за вопросы, первый был неожиданным, но второй я хотел услышать.
                    • 0
                      Не за что, это на самом деле даже по сути и не вопросы, а примеры ситуаций, которые вы сами могли забыть учесть и из-за которых потом могла начать болеть голова. Так сказать, попытка помощи от коллективного-бессознательного :)
                  • +1
                    Есть еще вариант как можно было реализовать данный функционал. Включить статистику звонков(cdr), и при каждом входящем звонке проверять одним запросом по базе статистики был ли от этого номера вызов на внутренний номер, сортируя с конца и в интервале нужной даты! Запрос более объемный, зато не нужно дополнительных таблиц и достаточно легко отловить transfer(в базе cdr буква t).
                    • 0
                      В малонагруженной системе — да, но если это call-центр? Где звонков до 100 000 в день, это достаточно серьезная нагрузка на database и сам запрос будет долгим.
                      • 0
                        Согласен что запрос будешь выполнятся дольше, тут как раз интересно было бы прогнать на реальной базе. Если меньше секунды, то думаю не будет заметно.
                        • 0
                          Да, было бы здорово протестировать под нагрузкой. Буду ждать результатов- у самого call центра под рукой нет)
                      • 0
                        Идея блеск!
                        Тоже самое что и в статье, но одним запросом к одной таблице БД, в которую астериск еще и сам пишет! Гуру, поделитесь
                        • 0
                          Постараюсь реализовать это на неделе и протестировать, отпишу вам!
                    • +1
                      В Asterisk есть функция, называется ODBC_ANTIGF. Пожалуй, озвученный тут вопрос, рассматривает один из немногих случаев, когда ее можно применить.

                      -= Info about function 'ODBC_ANTIGF' =-
                      [Synopsis]
                      Check if a specified callerid is contained in the ex-gf database

                      Ну это так, каламбур, а вообще, написано по делу.

                      По поводу вариантов определения номера ответившего, придумал еще один. Если используется логирование событий queue_log в БД, то значит уже «есть» механизм определения ответившего оператора. В БД можно настроить trigger, который будет следить за insert'ами в таблицу queue_log, искать совпадения по event in ('COMPLETEAGENT', 'COMPLETECALLER'), и класть значение agent в таблицу с номерами. На нагруженных системах это сократит количество внешних обращений к базе, что тоже к лучшему.
                      • 0
                        Я храню в redis, добавление обновления записей lua, только использую freeswitch а не asterisk.
                        • 0
                          а как это сделать на freepbx distro?
                          • 0
                            Написать свой диалплан)

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.