UPD. Прекрасный человек по имени Максим адаптировал мой код под FreePBX - его проект вы можете посмотреть по этой ссылке.
Давненько я не писал ничего про Звездочку, хотя идей подкопилось немало. Надеюсь этой статьей возродить рубрику про Астериск.
К написанию этой статьи меня подтолкнул вопрос одного иноземного знакомца, который поинтересовался на тему биллинга в Астериске. Конечно, есть множество систем биллинга - тот же AsteriskNOW, насколько мне известно, содержит зачатки биллинга или, можно сказать, индустриальный стандарт A2Billing. Но порочная страсть все сделать самому победила, и я набросал простой скрипт биллинга.
У данного скрипта следующие ограничения:
- это скрипт только для диалплана, т.е. не включает в себя создание пользователей и пополнение их баланса - только списание;
- так же сюда не включена авторизация пользователей, она должна быть произведена до вызова скрипта;
- все звонки округляются до минуты в большую сторону, нет возможности разной тарификации по времени суток;
- макрос не имеет защиты от параллельных звонков с одними учетными данными;
- обновление баланса происходит по окончании разговора.
Логика работы
Скрипт состоит из двух макросов - snubill-call и snubill-finish.
Первый вызывается перед звонком. В него передается идентификатор пользователя и набираемый номер. Макрос считает доступное количество минут, исходя из баланса пользователя, и устанавливает это значение в переменную канала SNBILL_MINUTES_COUNT.
После этого производится набор номера (см. ниже). По завершении разговора, вызывается макрос snubill-finish, который обновляет баланс пользователя.
Макрос использует следующую логику тарификации: существует таблица префиксов с ценами на звонок по этому префиксу. Например, пустой префикс - 10р/мин, префикс 8926 - 20р/мин, префикс 8926227 - 30р/мин.
Номер проверяется на соответствие префиксу максимальной длины. Т.е., в данном примере, при наборе номера 8-926-227-0000 - будет использована цена 30р/мин, для номера 8-926-111-0000 - 20р/мин, при наборе 8-903-111-0000 - 10р/мин.
Настройка базы данных
Для работы скрипта, первым делом необходимо настроить подключение к MySQL. В принципе, все запросы простейшие, так что вы можете переписать их под вашу любимую БД. Я использовал MySQL через ODBC. О настройке такой связки можете смело почитать тут.
Для работы скрипта у вас должно быть две таблицы.
Первая - пользователи и их балансы. Я создал такую таблицу:
(
`user_id` VARCHAR(255) NOT NULL,
`current_balance` FLOAT NOT NULL DEFAULT 0,
PRIMARY KEY (`user_id`)
)
Вторая таблица - префиксы номеров и цена за минуту.
`prefix` VARCHAR(20) NOT NULL,
`price` FLOAT NOT NULL DEFAULT 0,
PRIMARY KEY (`prefix`))
Во вторую таблицу лучше сразу вставить тариф по-умолчанию (в данном случае - 10р/мин):
Теперь создадим запросы в func_odbc.conf:
[SNUBILL_GET_PRICE]
dsn=asterisk
readsql=SELECT price FROM tariffs WHERE '${SQL_ESC(${ARG1})}' LIKE CONCAT(prefix,'%') ORDER BY CHAR_LENGTH(prefix) DESC LIMIT 0,1
; Получение баланса пользователя
[SNUBILL_GET_BALANCE]
dsn=asterisk
readsql=SELECT current_balance FROM users WHERE user_id='${SQL_ESC(${ARG1})}'
; Обновление баланса
[SNUBILL_UPDATE_BALANCE]
dsn=asterisk
writesql=UPDATE users SET current_balance=current_balance-${VAL1} WHERE user_id='${SQL_ESC(${ARG1})}'
После этого, через консоль Asterisk перегружаем func_odbc и проверяем, что функции появились:
CLI> odbc read
ODBC_SNUBILL_GET_BALANCE ODBC_SNUBILL_GET_PRICE
ODBC_SNUBILL_UPDATE_BALANCE
Код макросов
Я вынес макросы в отдельный файл, snubill.conf.
; (c) Василий Snussi Шоков, https://www.snussi.ru, snussi@snussi.ru
; Если при использовании макроса упомянете мой сайт - буду рад
;
; Документацию по макросу можно посмотреть по адресу https://www.snussi.ru/asterisk/snubill.html
; snubill-call - звонок на номер
;
; На вход макроса подается два аргумента - идентификатор пользователя и набираемый телефон
; В результате создается переменная канала SNUBILL_MINUTES_COUNT, которая содержит число целых минут,
; доступных пользователю для данного звонка
[macro-snubill-call]
exten => s,1,NoOp(SnuBill-call v0.1)
exten => s,n,NoOp(User: ${ARG1}, target number: ${ARG2})
; Перенесем в переменные канала пользователя
; чтобы потом использовать при работе snubill-finish
exten => s,n,Set(__SNUBILL_USER_ID=${ARG1})
; Проверим, что задан идентификатор пользователя
exten => s,n,GotoIf($['${SNUBILL_USER_ID}' = ''] ?:have_user)
exten => s,n,NoOp(Empty user id for this call)
exten => s,n,MacroExit()
; Установим количество доступных минут в 0
exten => s,n(have_user),Set(__SNUBILL_MINUTES_COUNT=0)
; Получим тариф на указанный телефон
exten => s,n,Set(__SNUBILL_PRICE=${ODBC_SNUBILL_GET_PRICE(${ARG2})})
; Возможно, номер не подошел ни под один из шаблонов. В этом случае, вернется прайс ''
; В этой ситуации вернемся из макроса с сообщением
exten => s,n,GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_price)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()
; Прайс есть. Получим теперь баланс пользователя
exten => s,n(have_price),Set(SNUBILL_BALANCE=${ODBC_SNUBILL_GET_BALANCE(${ARG1})})
; Возможно, такого пользователя нет. или еще что-то. В этом случае, вернется баланс ''
; В этой ситуации вернемся из макроса с сообщением
exten => s,n,GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_balance)
exten => s,n,NoOp(Empty balance for this user)
exten => s,n,MacroExit()
; Баланс есть.
; Разберемся с количеством минут.
; Возможны два варианта. Стоимость минуты больше нуля или 0 (и меньше)
; Если 0 и меньше, то считаем это безлимитным и возвращаем 3600 минут.
exten => s,n(have_balance),Set(__SNUBILL_MINUTES_COUNT=${IF($[${SNUBILL_PRICE}>0] ?${MATH( ${SNUBILL_BALANCE} / ${SNUBILL_PRICE} ,i)}:3600)})
exten => s,n,NoOp(Price per minute is ${SNUBILL_PRICE}, balance is ${SNUBILL_BALANCE}, minutes count is ${__SNUBILL_MINUTES_COUNT})
; Выходим
exten => s,n,MacroExit()
; snubill-finish
;
; Обновление баланса после звонка.
;
; Обновление использует переменную ANSWEREDTIME, устанавливаемую DIAL,
; а так же SNUBILL_USER_ID и SNUBILL_PRICE, установленные через snubill-call
[macro-snubill-finish]
exten => s,1,NoOp(SnuBill-finish v0.1)
; Проверим, что задан идентификатор пользователя
exten => s,n,GotoIf($['${SNUBILL_USER_ID}' = ''] ?:have_user)
exten => s,n,NoOp(Empty user id for this call)
exten => s,n,MacroExit()
; Проверим, что установлен прайс
exten => s,n(have_user),GotoIf($['${SNUBILL_PRICE}' = ''] ?:have_price)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()
; Наконец, проверим, что есть количество секунд ответа
exten => s,n(have_price),GotoIf($['${ANSWEREDTIME}' = ''] ?:have_answer)
exten => s,n,NoOp(Empty price for this call)
exten => s,n,MacroExit()
; Прайс есть. Получим теперь количество минут, которые проговорил пользователь
; с округлением в большую сторону
exten => s,n(have_answer),Set(SNUBILL_MINUTES=$[CEIL(${ANSWEREDTIME} / 60 )])
; Теперь можно умножить минуты на прайс и обновить баланс пользователя
exten => s,n,Set(ODBC_SNUBILL_UPDATE_BALANCE(${SNUBILL_USER_ID})=${MATH(${SNUBILL_MINUTES} * ${SNUBILL_PRICE},f)})
exten => s,n,MacroExit()
Использование в диалплане
Простой пример использования в диалплане.
Включаем сам макрос в диалплан
При вызове некоего экстеншна (в моем примере - TEST_EXT) вызываем макрос
; Установим тестовый идентификатор пользователя и номер
exten => TEST_EXT,n,Set(NUMBER_TO_DIAL=89261110000)
exten => TEST_EXT,n,Set(USER_ID=test)
; Вызываем макрос
exten => TEST_EXT,n,Macro(snubill-call,${USER_ID},${NUMBER_TO_DIAL})
exten => TEST_EXT,n,NoOp(${SNUBILL_MINUTES_COUNT})
; Если нет свободных минут, то переходим к завершению вызова (или к сообщению, что нет денег на счете)
exten => TEST_EXT,n,GotoIf($[${SNUBILL_MINUTES_COUNT}>0]?:the_end)
; Набираем номер. При этом, нужен флаг L, чтобы ограничить вызов
; Параметр L передается в миллисекундах, поэтому количество минут надо умножить на 60.000
exten => TEST_EXT,n,Dial(SIP/external_line_mgts/${NUMBER_TO_DIAL},,rL(${MATH(${SNUBILL_MINUTES_COUNT} * 60000,i)}) )
; Вешаем трубку
exten => TEST_EXT,n(the_end),Hangout()
В том же контексте, необходимо прописать для экстеншна h макрос, закрывающий соединение:
exten => h,n,Macro(snubill-finish)