Лирическое вступление или "Кто виноват?"
Периодически пытаюсь снять деньги через банкомат "Сбербанка" у метро Ломоносовская и с завидной регулярностью (читай: всегда) сталкиваюсь с отсутствием в банкоматах мелких купюр. Остаются только пятитысячные, которыми ни в маршрутке не расплатиться, ни хлеба не купить.
Сначала я пытался решать эту проблему методами социальной инженерии - звонил на горячую линию Сбербанка, нажимал какие попало цифры и высказывал претензию оператору - денег в банкомате нет, ничего нет, населена роботами. В ответ получал длинные пространные рассуждения на тему того, что ближайший ко мне банкомат находится на ул.Бабушкина д.4 (на минуточку, почти в 3 километрах от метро Ломоносовская), а я могу составить претензию и её обязательно-обязательно рассмотрят в установленный внутренними правилами срок (7 дней) и обязательно-обязательно примут решение (ага, щаз!), о чем меня уведомят. Прикола ради я даже оставил две таких претензии. Не знаю, рассмотрели ли их, но денег в банкоматах как не было - так и нет.
Потом я перестал решать проблему методами социальной инженерии и просто стал ходить в "штатное" отделение Сбербанка напротив. Но это не всегда удобно, в итоге банкоматы Сбербанка в метро - это единственные банкоматы в округе, в которых никогда нет денег. Это печально. Нет, я все понимаю, они стоят в метро и мимо них каждый день проходит куча людей и все хотят денег. Но почему инкассаторы их заправляют когда попало?
Но это все лирика. Переходим к более интересным вопросам: Как сделать мир лучше?
"Что делать?" или Как сделать мир лучше?
И вот я задумался - если бы я работал во внутренних структурах Сбербанка - как бы я решал эту проблему?
В дальнейших размышлениях я предположу, что проблема не решена совсем и всю логику работу и инфраструктуру надо делать с нуля.
Что у нас есть?
- Банкоматы, расставленные по всему городу в превеликом множестве мест.
- Банкоматы эти имеют подключение по сети к банку для взаимодействия как минимум в рамках платёжных протоколов (но я думаю, возможности их информационного обмена с банком намного шире).
- Множество людей, которые нерегулярно и в разное время снимают с банкоматов деньги.
- Периодически приезжающие инкассаторы.
Какую дополнительную информацию банкомат должен отсылать банку?
- Наличие купюр всех номиналов (не обязательно даже точное количество).
- Точное время выполнения очередной операции.
1. Статистика использования банкоматов в час
На основании частоты выполнения операций по снятию денег мы можем оценить востребованность банкомата, а собрав статистику за какой-то срок - вывести величину OpH (operations per hour). Разумеется, средний OpH будет зависеть от дня недели, праздников и местоположения банкомата. Но эту информацию уже можно где-то хранить и визуализировать во внутренних системах - мы получим прелюбопытнейшую карту "истечения в реал наличности".
Что нам для этого нужно? Отзывчивый сервер, способный обрабатывать сотни коротких запросов в секунду. Скорее всего это nodeJS и реляционная БД с таблицей вида
CREATE TABLE `stat_withdrawal` (
`id_event` bigint(20) NOT NULL AUTO_INCREMENT,
`event_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Временная метка события',
`id_device` int(11) DEFAULT NULL COMMENT 'Идентификатор банкомата',
`withdrawal` int(11) DEFAULT NULL COMMENT 'Сумма снятия в рублях',
PRIMARY KEY (`id_event`)
) DEFAULT CHARSET=latin1
оптимизированной на вставку. Отмечу, тут нет информации о клиенте, то есть информация обезличена. И запрос на вставку делается только по факту снятия клиентом наличности.
На основании этой информации мы можем построить как OpH, так и получить статистику по динамике снятия средств (рублей/час) и статистику посещаемости по часам, дням недели и так далее.
А для вывода информации на карту нам понадобится еще одна таблица:
CREATE TABLE `devices_info` (
`id_device` int(11) NOT NULL AUTO_INCREMENT,
`lat` decimal(8,6) DEFAULT NULL,
`lon` decimal(8,6) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8
В выборе типа поля широты/долготы я опираюсь на данные по точности хранения координат со stackoverflow.
Дальнейшее очевидно - yandex.api или leaflet с гео-слоем и отрисованными поверх маркерами по указанным координатам. Код приводить не буду, он элементарно ищется в документации.
2. Учёт наличия средств
Здесь таблицу таблицу stat_withdrawal
придется усложнить - вводим дополнительные поля по купюрам:
CREATE TABLE `stat_withdrawal` (
`id_event` bigint(20) NOT NULL AUTO_INCREMENT,
`event_ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Временная метка события',
`id_device` int(11) DEFAULT NULL COMMENT 'Идентификатор банкомата',
`withdrawal` int(11) DEFAULT NULL COMMENT 'Сумма снятия в рублях',
`w_5k` smallint(6) DEFAULT NULL COMMENT 'выданных купюр по 5000р',
`w_1k` smallint(6) DEFAULT NULL COMMENT 'выданных купюр по 1000р',
`w_500` smallint(6) DEFAULT NULL COMMENT 'выданных купюр по 500р',
`w_100` smallint(6) DEFAULT NULL COMMENT 'выданных купюр по 100р',
PRIMARY KEY (`id_event`)
) DEFAULT CHARSET=latin1
Эти четыре поля могут показаться ненужной денормализацией, но... в данном случае эта денормализация оправдана.
Я предполагаю, что банкоматы твари умные и выдаваемую сумму стараются разбить на купюры так, чтобы выдать наименьшее их количество. Алгоритмизируется это элементарно и останавливаться на этом я не буду.
Итак, что происходит?
Когда инкассаторы заряжают в банкомат деньги, он рапортует наверх: "Устройство №555 получило 245000 рублей купюрами следующих номиналов: 10 купюр по 5000, 45 купюр по 1000, 100 купюр по 500, 1000 купюр по 1000", или на языке SQL:
insert into `stat_withdrawal`
(`event_ts`, `id_device`, `withdrawal`, `w_5k`, `w_1k`, `w_500`, `w_100`)
values
('...', 555, 245000, 10, 45, 100, 1000);
Тут есть один подводный камень - я не знаю, как вставляют деньги в банкомат и о чем он рапортует - о том, сколько денег в нем стало или сколько денег в него добавили.
Вместе с этим запросом обновляется и актуальная информация в таблице device_balance
аналогичной структуры:
CREATE TABLE `device_balance` (
`id_device` int(11) DEFAULT NULL COMMENT 'Идентификатор банкомата',
`balance` int(11) DEFAULT NULL COMMENT 'Количество рублей в устройстве',
`w_5k` smallint(6) DEFAULT NULL COMMENT 'купюрами по 5000р',
`w_1k` smallint(6) DEFAULT NULL COMMENT 'купюрами по 1000р',
`w_500` smallint(6) DEFAULT NULL COMMENT 'купюрами по 500р',
`w_100` smallint(6) DEFAULT NULL COMMENT 'купюрами по 100р',
PRIMARY KEY (`id_device`)
) DEFAULT CHARSET=latin1
Эта таблица оптимизирована на обновление.
Когда клиент снимает из банкомата деньги, в таблицу stat_withdrawal
делается запрос в духе:
insert into `stat_withdrawal`
(`event_ts`, `id_device`, `withdrawal`, `w_5k`, `w_1k`, `w_500`, `w_100`)
values
('...', 555, -17700, -3, -2, -1, -2);
И соответственно обновляется информация в таблице device_balance
.
3. Но что нам с этим всем делать?
После первичного сбора статистики (возможно придётся еще немного усложнить структуру базы, добавив флаг "операция снятия успешна / отказано, нет денег") мы можем выяснить проблемные места и каждому устройству в таблице devices_info
добавить два поля:
estimated_solvency
- она означает среднее время в минутах, в течение которого банкомату хватает денег на удовлетворение запросов клиентов.money_delivery_time
- это то время, которое нужно инкассаторам на поездку к устройству и заправку его деньгами (оптимальное время+надбавка за пробки)
Далее на базу навешиваются триггеры, которые по истечении доли срока estimated_solvency
у каждого устройства ставят его в таблицу "это устройство требуется покормить деньгами".
Точное значение делителя estimated_solvency
выясняется экспериментально и для банкоматов в метро может быть 0.5
, а в какой-нибудь тьмутаракани вообще 0.1
. Выставлять это значение надо на основе соотношения estimated_solvency
и money_delivery_time
, так, чтобы банкомат простаивал не более 10% от времени estimated_solvency
(а лучше вообще не простаивал пустой, но мы ведь живем не в идеальном мире).
По приезду инкассаторов банкомат рапортует "наверх" - "меня кормят деньгами" и сервер статистики удаляет его из очереди "жаждущих подкормки". Разумеется, время заправки деньгами мы тоже можем записывать.
Резюмирую
Это концепт системы поддержания банкоматов в платежеспособном состоянии. Разумеется, в процессе разработки выяснится множество тонких мест, которые я пока не в силах углядеть. Но чисто технически эти механизмы довольно нетребовательны по ресурсам, тем более что база хранит только факты приёма/выдачи денег. Поэтому запросы могут сваливаться в очередь и обрабатываться не вотпрямщас, а с задержками.
По программной части: это все элементарно реализуется на PHP, но, насколько я слышал, сервера на NodeJS более отзывчивы в плане "сотни запросов в секунду". На самом деле, количество запросов я бы оценил как
2 * (количество банкоматов в регионе) / 60
На таком уровне я NodeJS не знаю, но если такая задача встанет - будет хороший повод доучиться :) Пока что такой задачи не стоит.
Как-то так.
P.S. В 2022 году на хабре написали интересную статью про банкоматы: https://habr.com/ru/company/gazprombank/blog/654235/ . В общем, все мои размышления в этой статье - это попытка "в аналитику".