Некоторое время назад я запустил Telegram-бота для мониторинга сайтов и обозначил в нём такой тариф:
Один сайт на мониторинге — бесплатно.
Каждый дополнительный — 2 ₽ в день.
Сколько строк кода получилось из двух строк описания я покажу ниже. По пути расскажу, как обретала свою логику биллинговая система. Возможно, это сэкономит вам немножко мыслетоплива в похожем проекте.
Этап 1: просто возможность оплатить
С самого начала бот затевался как коммерческий проект. Тем более, количество пользователей и добавленных ими сайтов создавало определённую нагрузку на хостинг. Рекорд — 56 сайтов у одного пользователя. Поэтому через пару месяцев после первой моей публикации о боте я решил ввести платный тариф.
Начал с того, что просто добавил удобную возможность оплаты, об этом подробно рассказал в другой статье. Разослал пользователям уведомления о том, что скоро мониторинг двух и более сайтов будет стоить денег, и сразу получил первые несколько оплат. А с ними — чувство эйфории.
Итог: оплата работает, бот доказал свою ценность рублём, можно идти дальше.
Этап 2: ограничения для тех, кто не оплатил
Ещё пару дней после анонсированного срока начала действия платного тарифа в боте ничего не поменялось: новых оплат не было, но и я ещё даже не продумал биллинг. Было ощущение, что выгоднее его вообще не делать ради нескольких платных пользователей.
Но решил, всё же, сделать MVP биллинга из трёх функций:
-
Предупреждение, а затем и приостановка мониторинга всех сайтов кроме одного у пользователей без оплат (если у них больше 1 сайта).
-
Автоматическое возобновление мониторинга при поступлении оплаты на любую сумму.
-
Невозможность добавить второй сайт на мониторинг, если нет ни одной оплаты.
То есть, пользователь мог даже истратить всю сумму пополнения и продолжать пользоваться ботом сколько угодно — списаний с баланса всё ещё нет. Но никто об этом, конечно, не знал.
Итог: получил ещё одну оплату.
Этап 3: списания с баланса по тарифу
Какое-то время размышлял о том, как лучше всего организовать эти самые списания по 2 рубля в день за каждый доп. сайт, и выбрал такой вариант:
-
В начале суток рассчитываю списания за предыдущие сутки.
-
Сумма списания не может превышать баланс (чтобы не уводить в минус).
-
Сохраняю сумму списания в базу с уникальным ключом клиент+дата (страховка от дублирования).
-
Днём проверяю балансы клиентов (поступления минус списания) и количество подписок у них.
-
Шлю уведомления тем, у кого баланса хватит меньше чем на 9 дней, если в последние два дня о балансе не уведомлял.
-
Приостанавливаю доп. сайты тем, у кого баланс 0, при условии что я их уже уведомлял о нулевом балансе минимум сутки назад.
-
При попытке добавить второй сайт на мониторинг проверяю уже не просто наличие поступлений, а баланс, то есть учитываю списания.
-
В пункте меню про тарифы добавляю баланс.
Получается, что при нулевом балансе мониторинг продолжает работать бесплатно некоторое время, но в данном случае это сознательный выбор, чтобы не тревожить пользователей ночью и, с другой стороны, не сделать биллинг ещё более сложным. Учитывая, что минимальную сумму пополнения определяю я, эксплуатировать эту особенность мне в убыток не получится.
Техническая реализация
Серверная часть бота запускается по расписанию каждые 5 минут и выполняет проверки для сайтов на мониторинге. Дополнительно, в зависимости от времени суток, выполняются действия по расчёту списаний, по уведомлению пользователей и приостановке мониторинга. Поэтому все функции и запросы к базе рассчитаны на то, что могут запускаться несколько раз в час, а потом долгое время не запускаться — кажется, это повышает надёжность.
Вот так выглядит запрос (MySQL) для расчёта списаний:
SELECT result.chat_id, CAST(result.amount_calculated AS SIGNED) AS amount_calculated, -- Сумма списания по тарифу, без учёта баланса CAST(GREATEST( 0, -- Подстраховка от списания отрицательной суммы из-за отрицательного баланса (по идее, невозможно из условия отбора) LEAST(result.amount_calculated, result.balance) -- Не списываем в минус ) AS SIGNED) AS amount_fact, -- Сколько фактически списать CAST(result.balance AS SIGNED) AS balance_before, -- Баланс до списания result.date_for_charge -- Дата, за которую делаем списание FROM ( SELECT chat_id, FLOOR(GREATEST( 0, (SUM(TIMESTAMPDIFF(MINUTE, start_date, end_date))/1440 - 1) * 200 )) AS amount_calculated, -- Количество сайто-дней в дату списания, 1 сайто-день бесплатно, каждый дополнительный — 200 копеек в сутки. payments_sum - COALESCE(charges_sum, 0) AS balance, first_date_for_charge AS date_for_charge FROM ( SELECT s.id, s.chat_id, s.created, s.deleted, GREATEST(s.created, p.first_date_for_charge) AS start_date, LEAST( COALESCE( s.deleted, -- Если подписка удалена, то до момента удаления, p.first_date_for_charge + INTERVAL 1 DAY), -- иначе до даты за датой списания. p.first_date_for_charge + INTERVAL 1 DAY) AS end_date, -- но не позже даты за датой списания. p.payments_sum, p.charges_sum, p.first_date_for_charge FROM subscriptions s JOIN ( SELECT ip.chat_id, ip.payments_sum, ch.charges_sum, ch.last_charge_date, s.first_subscription_date, CAST( GREATEST( s.first_subscription_date, COALESCE( ch.last_charge_date, :tariff_start_date ) + INTERVAL 1 DAY ) AS date ) AS first_date_for_charge -- Самая старая дата, за которую нужно сделать списание у пользователя FROM ( SELECT chat_id, SUM(amount) AS payments_sum FROM income_payments GROUP BY chat_id ) ip LEFT JOIN ( SELECT chat_id, SUM(amount_fact) AS charges_sum, MAX(date_for_charge) AS last_charge_date FROM charges GROUP BY chat_id ) ch ON ch.chat_id = ip.chat_id JOIN ( SELECT min(created) AS first_subscription_date, chat_id FROM subscriptions GROUP BY chat_id ) s ON s.chat_id = ip.chat_id WHERE ip.payments_sum - COALESCE(ch.charges_sum, 0) > 0 -- Если есть что списать AND ( ch.last_charge_date IS NULL -- Если не было списаний OR ch.last_charge_date < NOW() - INTERVAL 2 DAY -- Или последнее списание было больше 2 дней назад ) ) p ON p.chat_id = s.chat_id WHERE -- Ищем подписки, действовашие в дату списания (s.deleted IS NULL OR s.deleted > p.first_date_for_charge) AND (s.created < p.first_date_for_charge + INTERVAL 1 DAY) ) total GROUP BY chat_id, payments_sum, charges_sum, first_date_for_charge ) result ;
Пояснения:
-
chat_idв данном случае можно считать идентификатором клиента. -
subscriptions— таблица с подписками, 1 подписка — 1 сайт на мониторинге. -
charges— таблица со списаниями. -
income_payments— таблица с пополнениями баланса. -
:tariff_start_date— единственный передаваемый параметр, начало действия платного тарифа. -
Все суммы считаются в копейках как целые числа.
-
Приведение типов (
CAST) добавлено из-за того, что без него php после pdo эти числа получает в виде строк. -
Для каждого подписчика определяется самая ранняя дата, за которую нужно рассчитать списание — это необязательно вчерашний день. Сделано так для расчёта списаний за период с объявления платного тарифа до момента его имплементации в коде. Запрос выполняется несколько раз за сутки, постепенно нагоняя упущенные дни. В штатном режиме после первого же выполнения списаний в течение этих суток запрос уже не будет ничего возвращать.
-
Для каждого пользователя рассчитываем, сколько сайто-дней приходилось на расчётную дату, 1 сайт вычитаем, а остаток умножаем на суточный тариф. Получается, если половину суток было 2 сайта, а вторую половину — ни одного, то списания не будет.
-
Клиенты без оплат в выборку не попадают — с них списать всё равно нечего.
-
Если по итогу получилось нулевое списание, оно всё равно сохраняется, чтобы потом этот клиент с этой датой уже не попадал в выборку.
-
Запрос выглядит, возможно, громоздко, но быстро работает и выдаёт в готовом виде все необходимые данные для списаний. На стороне php остаётся только взять нужные поля и поместить в таблицу списаний, попутно залогировав.
Итог
Теперь биллинг можно считать законченным — есть и пополнения, и мотивация для пополнений, и списания согласно тарифу. Некоторое чувство велосипедостроения присутствует, но чувство творческого удовлетворения его перевешивает. Возможно, подобная задача могла бы быть решена как-то более эффективно — пишите, будет интересно обсудить.
Да и самого бота, конечно, тоже пробуйте, я не только над биллингом в нём потрудился!
ссылка на оригинал статьи https://habr.com/ru/articles/876524/
Добавить комментарий