Как я реализовал публикации постов в Telegram-боте

Нужно было прорубить окно между ботом и общим каналом обсуждения. Самой простой фичей стало — позволить пользователям бота публиковать свои блюда в телеграм-канал.

Публикация приёма пищи из бота Scalory
Публикация приёма пищи из бота Scalory

Вроде всё просто: уже есть таблица meals, формируем текст из её данных и отправляем в канал.

TABLE meals ( id, user_id, image_key, name, calories, protein, fat, carbs )

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

Значит, публикации требуют отдельную таблицу. Хорошо, не вопрос — создаём meal_publications и будем добавлять туда запись после публикации блюда из meals:

TABLE meal_publications ( id, meal_id );

I проблема

В боте уже реализовано, что любой пользователь может изменить своё блюдо в meals — например, указать новое количество калорий. Но тогда опубликованный пост будет показывать данные, не соответствующие реальности, ведь значения в meals изменились. И если кто-то потом сохранит себе это блюдо из поста, он получит уже изменённую версию, а не ту, что была опубликована.

Список приёмов пищи в боте Scalory
Список приёмов пищи в боте Scalory

Значит, нужно хранить не meal_id, а копию данных из meals на момент публикации:

TABLE meal_publications ( id, user_id, image_key, name, calories, protein, fat, carbs );

II проблема

Опыт разработчика заставляет заранее заботиться о возможных проблемах. И одна из них — спам. Что если пользователь будет публиковать одно и то же блюдо несколько раз подряд? Я сразу решил: один и тот же человек не должен публиковать одно и то же блюдо, пока он его не изменит.

Казалось бы, просто: добавляем поле is_published в таблицу meals и переключаем его из true в false после редактирования.

TABLE meals ( id, ..., is_published );

Решено? На первый взгляд — да. Но идеализм архитектора меня остановил. Поле is_published придётся менять вручную, а это значит — появится лишнее правило, которое либо нужно автоматизировать триггером БД, либо документировать для других разработчиков. Мне хотелось решения, которое само будет поддерживать целостность данных, без костылей.

Идеальное решение

Перед публикацией блюда (то есть перед добавлением копии из meals в meal_publications) я считаю хэш.

Это слепок всех значимых и изменяемых пользователем полей meals, на основе которых формируется ключ.

Например:

meals_2121_fields = { user_id=1, channel_id=100000000, image_key="folder/file.ext", name="Борщ", calories=300, protein=12, fat=13, carbs=24 } hash = encode(meals_2121_fields) # hash -> 12g1g3g31233g123g

После генерации хэша я сравниваю его с уже существующими записями в meal_publications. Если такой хэш уже есть — значит, это повторная публикация того же блюда, и она запрещается. Если нет — публикуем и сохраняем snapshot_hash:

TABLE meal_publications ( id, ..., snapshot_hash );

Такое решение мне нравится тем, что сама база данных теперь требует наличие snapshot_hash и следит за его уникальностью. Больше не нужно никаких искусственных флагов is_published и ручных проверок. Система сама защищает себя от дубликатов и сохраняет чистую архитектуру.

Пост приёма пищи в канале
Пост приёма пищи в канале

На этом всё. До встречи. Пишите, как бы вы реализовали подобную механику

Начать дискуссию