Лирическое вступление или “Кто виноват?”

Периодически пытаюсь снять деньги через банкомат “Сбербанка” у метро Ломоносовская и с завидной регулярностью (читай: всегда) сталкиваюсь с отсутствием в банкоматах мелких купюр. Остаются только пятитысячные, которыми ни в маршрутке не расплатиться, ни хлеба не купить.

Сначала я пытался решать эту проблему методами социальной инженерии - звонил на горячую линию Сбербанка, нажимал какие попало цифры и высказывал претензию оператору - денег в банкомате нет, ничего нет, населена роботами. В ответ получал длинные пространные рассуждения на тему того, что ближайший ко мне банкомат находится на ул.Бабушкина д.4 (на минуточку, почти в 3 километрах от метро Ломоносовская), а я могу составить претензию и её обязательно-обязательно рассмотрят в установленный внутренними правилами срок (7 дней) и обязательно-обязательно примут решение (ага, щаз!), о чем меня уведомят. Прикола ради я даже оставил две таких претензии. Не знаю, рассмотрели ли их, но денег в банкоматах как не было - так и нет.

Потом я перестал решать проблему методами социальной инженерии и просто стал ходить в “штатное” отделение Сбербанка напротив. Но это не всегда удобно, в итоге банкоматы Сбербанка в метро - это единственные банкоматы в округе, в которых никогда нет денег. Это печально. Нет, я все понимаю, они стоят в метро и мимо них каждый день проходит куча людей и все хотят денег. Но почему инкассаторы их заправляют когда попало?

Но это все лирика. Переходим к более интересным вопросам: Как сделать мир лучше?

“Что делать?” или Как сделать мир лучше?

И вот я задумался - если бы я работал во внутренних структурах Сбербанка - как бы я решал эту проблему?

В дальнейших размышлениях я предположу, что проблема не решена совсем и всю логику работу и инфраструктуру надо делать с нуля.

Что у нас есть?

  • Банкоматы, расставленные по всему городу в превеликом множестве мест.
  • Банкоматы эти имеют подключение по сети к банку для взаимодействия как минимум в рамках платёжных протоколов (но я думаю, возможности их информационного обмена с банком намного шире).
  • Множество людей, которые нерегулярно и в разное время снимают с банкоматов деньги.
  • Периодически приезжающие инкассаторы.

Какую дополнительную информацию банкомат должен отсылать банку?

  • Наличие купюр всех номиналов (не обязательно даже точное количество).
  • Точное время выполнения очередной операции.

1. Статистика использования банкоматов в час

На основании частоты выполнения операций по снятию денег мы можем оценить востребованность банкомата, а собрав статистику за какой-то срок - вывести величину OpH (operations per hour). Разумеется, средний OpH будет зависеть от дня недели, праздников и местоположения банкомата. Но эту информацию уже можно где-то хранить и визуализировать во внутренних системах - мы получим прелюбопытнейшую карту “истечения в реал наличности”.

Что нам для этого нужно? Отзывчивый сервер, способный обрабатывать сотни коротких запросов в секунду. Скорее всего это nodeJS и реляционная БД с таблицей вида

1
2
3
4
5
6
7
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, так и получить статистику по динамике снятия средств (рублей/час) и статистику посещаемости по часам, дням недели и так далее.

А для вывода информации на карту нам понадобится еще одна таблица:

1
2
3
4
5
6
7
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 придется усложнить - вводим дополнительные поля по купюрам:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
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 аналогичной структуры:

1
2
3
4
5
6
7
8
9
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 делается запрос в духе:

1
2
3
4
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 более отзывчивы в плане “сотни запросов в секунду”. На самом деле, количество запросов я бы оценил как

1
2 * (количество банкоматов в регионе) / 60

На таком уровне я NodeJS не знаю, но если такая задача встанет - будет хороший повод доучиться :) Пока что такой задачи не стоит.

Как-то так.

Comments