Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

Напевно, у багатьох з вас, як і в мене, була ідея зробити щось унікальне. У цій статті я опишу технічні проблеми та рішення, з якими довелося зіткнутися під час розробки АТС. Можливо, це комусь допоможе зважитися на свою ідею, а комусь пройти протоптаною доріжкою, адже я теж користувався досвідом першопрохідників.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

Ідея та ключові вимоги

А почалося все банально з любові до Зірочка (framework для побудови комунікаційних додатків), автоматизації телефонії та установок Безкоштовна АТС (Веб інтерфейс для Зірочка). Якщо потреби компанії були без особливостей і вкладалися у можливості Безкоштовна АТС - все супер. Вся установка проходила за добу, компанія отримувала налаштовану АТС, зручний інтерфейс та коротке навчання плюс супровід за бажанням.

Але найцікавіші завдання були нестандартними, і тоді було не так казково. Зірочка може багато що, але щоб зберегти в робочому вигляді веб-інтерфейс, доводилося витратити в рази більше часу. Так невелика дрібниця могла зайняти часу набагато більше, ніж установка решти АТС. І справа не в тому, що писати веб-інтерфейс довго, а скоріше справа в особливостях архітектури Безкоштовна АТС. Підходи та методи архітектури Безкоштовна АТС закладалася в часи php4, а в той момент вже був php5.6, на якому все можна було зробити простіше і зручніше.

Останньою краплею стали графічні діалплани як схеми. Коли спробував подібне побудувати для Безкоштовна АТС, зрозумів, що доведеться суттєво його переписати і простіше вже побудувати щось нове.

Ключовими вимогами стали:

  • просте налаштування, інтуїтивно доступне навіть адміністратору-початківцю. Тим самим компаніям не потрібне обслуговування АТС на нашій стороні,
  • легке доопрацювання, щоб завдання вирішувалися за адекватний час,
  • зручність інтеграції з АТС. У Безкоштовна АТС не було API зміни налаштувань, тобто. не можна, наприклад, створювати групи або голосові меню зі сторонньої програми, тільки API самого Зірочка,
  • opensource – для програмістів це дуже важливо для доопрацювань під клієнта.

Ідея швидшої розробки полягала в тому, щоб весь функціонал складався з модулів у вигляді об'єктів. Усі об'єкти мали мати спільний батьківський клас, отже назви всіх основних функцій вже відомі і отже вже є реалізації за умовчанням. Об'єкти дозволять різко скоротити кількість аргументів у вигляді асоціативних масивів з рядковими ключами, дізнатися які в Безкоштовна АТС можна було, дослідивши всю функцію та вкладені функції. У разі об'єктів банальне автодоповнення покаже всі властивості, та й загалом у багато разів спростить життя. Плюс успадкування та перевизначення вже закриває безліч проблем із доопрацюваннями.

Наступне, що уповільнювало час доопрацювання і чого варто було уникнути — дублювання. Якщо є відповідальний модуль за додзвон до співробітника, то всі інші модулі, яким потрібно відправити дзвінок співробітнику, повинні використовувати саме його, а не створювати свої власні копії. Так, якщо потрібно щось змінити, то міняти доведеться тільки в одному місці і пошук «як це працює» проводити одного місця, а не здійснювати пошук по всьому проекту.

Перша версія та перші помилки

Перший прототип був готовий уже за рік. Вся АТС, як і планувалося, була модульною, і модулі могли не лише додавати новий функціонал для обробки дзвінків, а й змінювати сам веб-інтерфейс.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php
Так, ідея побудови діалплану у вигляді такої схеми не моя, але вона дуже зручна і я зробив те саме для Зірочка.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

За допомогою написання модуля програмісти вже могли:

  • створювати для обробки дзвінка власний функціонал, який можна було розмістити на схемі, а також меню елементів зліва,
  • створювати власні сторінки для веб-інтерфейсу та додавати свої шаблони на існуючі сторінки (якщо розробник сторінки це передбачив),
  • додавати свої налаштування на вкладку основних налаштувань або створювати власну вкладку з налаштуваннями,
  • програміст може успадковуватись від існуючого модуля, змінити частину функціоналу та зареєструвати під новим ім'ям або замінити оригінальний модуль.

Наприклад, так можна створити своє голосове меню:

......
class CPBX_MYIVR extends CPBX_IVR
{
 function __construct()
 {
 parent::__construct();
 $this->_module = "myivr";
 }
}
.....
$myIvrModule = new CPBX_MYIVR();
CPBXEngine::getInstance()->registerModule($myIvrModule,__DIR__); //Зарегистрировать новый модуль
CPBXEngine::getInstance()->registerModuleExtension($myIvrModule,'ivr',__DIR__); //Подменить существующий модуль

Перші складні впровадження принесли першу гордість та перші розчарування. Тішило те, що воно працювало, що я вже зміг відтворити основні можливості Безкоштовна АТС. Тішило, що людям ідея схеми припала до душі. Було ще багато варіантів спростити розробку, але й у той час частина завдань вже робилася простіше.

Зневірою стало API для зміни конфігурації АТС — вийшло зовсім не те, що хотілося. Я взяв той самий принцип, що й Безкоштовна АТС, після натискання кнопки Apply перетворюється вся конфігурація і перезапускаються модулі.

Виглядає це так:

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php
*Діалплан - правило (алгоритм), яким обробляється дзвінок.

Але за такого варіанта неможливо написати нормальне API для зміни налаштувань АТС. По-перше, операція застосування змін до Зірочка занадто довга та ресурсомістка.
По-друге, не можна викликати дві функції одночасно, т.к. обидві створюватимуть конфігурацію.
По-третє, застосовує всі налаштування, в тому числі зроблені адміністратором.

У цій версії, як і в Askozia, можна було генерувати конфігурацію лише змінених модулів і перезапускати лише необхідні модулі, але це все напівзаходи. Потрібно було змінювати підхід.

Друга версія Ніс витяг хвіст ув'яз

Ідеєю для вирішення проблеми стало не перетворювати конфігурацію та діалплан для Зірочка, а зберігати інформацію в базі та читати з бази прямо під час обробки дзвінка. Зірочка вже вмів читати зміни з бази, достатньо змінити значення в базі і наступний дзвінок вже буде оброблятися з урахуванням змін, а для читання параметрів діалплана добре підійшла функція REALTIME_HASH.

У результаті не знадобилося навіть перезапускати Зірочка при зміні налаштувань і всі настройки стали застосовуватися відразу до Зірочка.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

Єдині зміни діалплану – це додавання внутрішніх номерів та підказки. Але це були маленькі точкові зміни

exten=>101,1,GoSub(‘sub-callusers’,s,1(1)); - точечное изменение, добавляется/изменяется через ami

; sub-callusers – универсальная функция генерится при установке модуля.
[sub-callusers]
exten =>s,1,Noop()
exten =>s,n,Set(LOCAL(TOUSERID)=${ARG1})
exten =>s,n,ClearHash(TOUSERPARAM)
exten =>s,n,Set(HASH(TOUSERPARAM)=${REALTIME_HASH(rl_users,id,${LOCAL(TOUSERID)})})
exten =>s,n,GotoIf($["${HASH(TOUSERPARAM,id)}"=""]?return)
...

Додати або поміняти рядок у діалплані легко можна через Амі (інтерфейс управління Зірочка) та перезавантаження всього діалплану не потрібно.

Так було вирішено проблему з API конфігурації. Можна було навіть безпосередньо зайти в базу і додати нову групу або поміняти, наприклад, час додзвону в полі "dialtime" у групи і наступний дзвінок вже триватиме вказаний час (Це не рекомендація до дії, тому що для деяких операцій API потрібні Амі виклики).

Перші складні впровадження знову принесли першу гордість та розчарування. Тішило те, що це працює. База даних стала критично важливою ланкою, зросла залежність від диска, ризиків більше, але все працювало стабільно та без проблем. А головне тепер все, що можна було зробити через веб-інтерфейс, можна було зробити і через API і при цьому використовувалися ті самі методи. Додатково веб-інтерфейс позбавився кнопки «застосувати налаштування до АТС», про яку адміністратори часто забували.

Розчаруванням стало ускладнення розробки. Ще з першої версії мова php генерує діалплан мовою Зірочка і виглядає це абсолютно нечитано, плюс сама мова Зірочка для написання діалплану вкрай примітивний.

Як це виглядало:

$usersInitSection = $dialplan->createExtSection('usersinit-sub','s');
$usersInitSection
 ->add('',new Dialplanext_gotoif('$["${G_USERINIT}"="1"]','exit'))
 ->add('',new Dialplanext_set('G_USERINIT','1'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnAnswerSub','usersconnected-sub'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnPredoDialSub','usersinitondial-sub'))
 ->add('',new Dialplanext_set('LOCAL(TECH)','${CUT(CHANNEL(name),/,1)}'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="SIP"]','sipdev'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="PJSIP"]','pjsipdev'))

У другій версії діалплан став універсальним, у нього було закладено всі можливі варіанти обробки залежно від параметрів та його розмір значно зріс. Все це сильно уповільнювало час розробки, і сама думка, що в черговий раз потрібно втручатися в діалплан, наводила смуток.

Третя версія

Ідеєю для вирішення проблеми стало не генерувати Зірочка діалплан з php, а використовувати FastAGI і всі правила обробки писати вже на php. FastAGI дозволяє Зірочкадля обробки дзвінка, підключитися до сокету. Отримувати звідти команди та надсилати результати. Таким чином, логіка діалплану знаходиться вже за кордонами. Зірочка і може бути написана будь-якою мовою, в моєму випадку на php.

Тут було багато спроб і помилок. Головною проблемою було, що я вже мав багато класів/файлів. На створення об'єктів, ініціалізацію та взаємну реєстрацію між ними йшло близько 1,5 секунд, і ця затримка на кожен дзвінок не те, що можна ігнорувати.

Ініціалізація мала бути лише 1 раз і тому пошуки рішення почалися з написання сервісу на php з використанням Pthreads. Через тиждень експериментів цей варіант було відкладено через тонкощі роботи цього розширення. Від асинхронного програмування на php після місяця тестів теж довелося відмовитися, потрібно було щось просте, знайоме будь-якому новачкові php, та й багато розширень для php синхронні.

Рішенням став свій багатопотоковий сервіс на 'сі', який компілювався з PHPLIB. Він підвантажує всі php файли АТС, чекає коли всі модулі ініціалізуються, додадуть коллбек один до одного і коли все готово - кешує. При запиті FastAGI створюється потік, у ньому відтворюється копія з кешу всіх класів та даних і запит передається у функцію php.

При такому рішенні час від відправки дзвінка до нашого сервісу до першої команди Зірочка скоротилося з 1,5с до 0,05с і цей час слабко залежить від розміру проекту.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

В результаті, час на розробку діалплану скоротився істотно, і я можу це оцінити, оскільки мені довелося переписати весь діалплан всіх модулів на php. По-перше, у php вже повинні бути написані методи для отримання об'єкта з бази, вони були потрібні для відображення у веб-інтерфейсі, а по-друге, і це головне – нарешті з'явилася можливість зручної роботи з рядками з числами з масивами базою даних плюс безліч розширень PHP.

Для обробки діалплану у класі модуля потрібно реалізувати функцію dialplanDynamicCall та аргумент pbxCallRequest буде містити об'єкт для взаємодії з Зірочка.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

У додатку з'явилася можливість налагоджувати діалплан (у php є xdebug і для нашого сервісу воно працює), можна рухатися кроками переглядаючи значення змінних.

Дані за дзвінками

Для будь-якої аналітики та звітів потрібні правильно зібрані дані і цей блок АТС теж проходив багато спроб і помилок з першої по третю версію. Найчастіше дані про дзвінки – це табличка. Один дзвінок = один запис: хтось дзвонив, хтось відповів, скільки проговорили. У цікавіших варіантах є ще додаткова табличка, кого зі співробітників АТС викликала під час дзвінка. Але все це закриває лише частину потреб.

Початковими вимогами стали:

  • зберігати як кому дзвонила АТС, а й хто відповів, т.к. існують перехоплення і при аналізі дзвінків це потрібно буде враховувати,
  • час до з'єднання із співробітником. У Безкоштовна АТС та деяких інших АТС, дзвінок вважається відповідальним, як тільки АТС підніме слухавку. Але для голосового меню вже потрібно підняти трубку, таким чином усі дзвінки стають відповідальними і час очікування на відповідь стає 0-1 секунду. Тому вирішено було зберігати не тільки час до відповіді, але час до з'єднання з ключовими модулями (модуль сам встановлює це прапор. Зараз це «Співробітник», «Зовнішня лінія»),
  • для складнішого діалплану, коли дзвінок гуляє між різними групами, потрібна була можливість кожен елемент досліджувати окремо.

Найкращим варіантом виявився варіант, коли модулі АТС самі про себе відправляють інформацію за дзвінками та в результаті зберігати інформацію у вигляді дерева.

Виглядає це наступним чином:

Для початку загальна інформація про дзвінок (як у всіх нічого особливого).

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

  1. Надійшов дзвінок по зовнішній лінії.Для тесту» о 05:55:52 з номера 89295671458 на номер 89999999999, у результаті на нього відповів співробітник «Секретар2» з номером 104. Клієнт чекав 60 секунд і розмовляв 36 секунд.
  2. Співробітник «Секретар2» здійснює дзвінок на номер 112 і на нього відповідає співробітник «Менеджер1» через 8 секунд. Розмовляють 14 секунд.
  3. Клієнта переводять на Співробітника «менеджер1» де вони продовжують розмовляти ще 13 секунд

Але це вершина айсберга, за кожним записом можна отримати докладне проходження дзвінка АТС.

Історія одного проекту або як я 7 років створював АТС на базі Asterisk та Php

Вся інформація подається у вигляді вкладеності викликів:

  1. Надійшов дзвінок по зовнішній лінії.Для тесту» о 05:55:52 з номера 89295671458 за номером 89999999999.
  2. О 05:55:53 зовнішня лінія надсилає дзвінок на Вхідну схему «тест»
  3. Під час обробки дзвінка за схемою викликається модуль «виклик менеджера», у якому дзвінок перебуває 16 секунд. Це розроблений під клієнтом модуль.
  4. Модуль «виклик менеджера» відправляє дзвінок на відповідального за номер (клієнта) співробітника «Менеджер1» та очікує відповіді 5 секунд. Менеджер не відповів.
  5. Модуль «виклик менеджера» надсилає дзвінок на групу «Менеджери КОРП». Це інші менеджери такого ж напрямку (сидять в одній кімнаті) і чекає на відповідь 11 секунд.
  6. Група «Менеджери КОРП» викликає співробітників «Менеджер1, Менеджер2, Менеджер3» одночасно по 11 секунд. Відповіді немає.
  7. Виклик менеджера завершується. І схема дзвінок відправляє на модульВибір маршруту з 1с». Теж написаний під клієнтом модуль. Тут дзвінок оброблявся 0 секунд.
  8. Схема надсилає дзвінок на голосове менюОсн із донабором». Клієнт у ньому чекав 31 секунду, до набору не було.
  9. Схема надсилає дзвінок на Групу «Секретарі», де клієнт чекав 12 секунд.
  10. У групі викликається одночасно 2 співробітники «Секретар1» та «Секретар2» та через 12 секунд відповідає співробітник «Секретар2». Відповідь на дзвінок дублюється в батьківські дзвінки. Виходить і у групі відповів «Секретар2», при виклику схеми відповів «Секретар2» і на дзвінок по зовнішній лінії відповів «Секретар2».

Саме збереження інформації про кожну операцію та їх вкладеність дозволить просто зробити звіти. Звіт по голосовому меню допоможе з'ясувати, наскільки воно допомагає чи заважає. Збудувати звіт про пропущені співробітниками дзвінки з урахуванням, що дзвінок перехопили і значить не вважається пропущеним, і з урахуванням, що це був груповий дзвінок, і хтось інший узяв раніше, а значить, теж дзвінок не пропущений.

Таке зберігання інформації дозволить взяти кожну групу окремо та визначити наскільки вона ефективно працює, побудувати графік відповідних та пропущених групи за годинами. Можна також перевірити, наскільки вгадує з'єднання з відповідальним менеджером, аналізуючи переклади після з'єднання з менеджером.

У тому числі можна проводити досить нетипові дослідження, наприклад, як часто номери, яких немає в базі, набирають правильний додатковий або який відсоток вихідних дзвінків є переадресацією на мобільний.

Що в підсумку?

Для обслуговування АТС не потрібно спеціаліст, з цим справляється звичайнісінький адміністратор – перевірено на практиці.

Для доробок не потрібні спеціалісти з серйозною кваліфікацією достатньо знань php, т.к. вже написані модулі і для sip протоколу, і для черги, і для виклику співробітника та інші. Є клас обгортка для Зірочка. Програміст розробки модуля може (і по-доброму повинен) викликати вже готові модулі. І знання Зірочка зовсім не потрібні, якщо клієнт просить додати сторінку з якимось новим звітом. Але практика показує, що сторонні програмісти хоч і справляються, але без документації та нормального покриття коментарями почуваються невпевнено, тому є ще куди рухатися.

Модулі можуть:

  • створювати нові можливості з обробки дзвінка,
  • додавати нові блоки у веб-інтерфейс,
  • успадкуватися від будь-якого з існуючих модулів, перевизначити функції та підмінити його або просто бути злегка зміненою копією,
  • додавати свої налаштування до шаблону налаштувань інших модулів та багато іншого.

Налаштування АТС через API. Як описано вище, всі налаштування зберігаються в базі та читаються в момент виклику, тому через API можна змінювати всі налаштування АТС. При виклику API не перетворюється конфігурація і не перезапускаються модулі, отже, не важливо, наскільки багато у вас налаштувань і співробітників. API запити виконуються швидко та не блокують один одного.

АТС зберігає всі ключові операції з дзвінками з тривалістю (очікування/розмови), вкладеннями та термінами АТС (співробітник, група, зовнішня лінія, а не канал, номер). Це дозволяє будувати різні звіти під конкретних клієнтів і більшість роботи – зробити зручний інтерфейс.

Що буде далі, покаже час. Є ще багато нюансів, які варто переробити, є ще багато планів, але від створення третьої версії пройшов уже рік і вже можна сказати, що ідея працює. Основний мінус 3-ї версії – це апаратні ресурси, але за зручність розробки зазвичай завжди так і доводиться платити.

Джерело: habr.com

Додати коментар або відгук