В первой части мы с тобой сделали скелет бота: он умеет парсить команду /add и выводит расходы в консоль. Но есть проблема: стоит перезапустить бота — и все твои суши с маршрутками пропадают без следа 🫠 Сегодня это исправим. Прикрутим к боту настоящую базу данных SQLite, добавим статистику и кнопки, а ещё сделаем супер‑полезную команду /undo.
🧠 Что будем колдовать сегодня
- База данных SQLite (встроена в Python, ничего устанавливать не нужно)
- Команда /stats — итоги за сегодня и категории трат
- Команда /undo — удалить последнюю запись (ой, не туда нажал!)
- Команда /total — общая сумма за всё время
- Инлайн‑клавиатуры — кнопки под сообщением
- Автоматический сброс «за вечер» в полночь с помощью JobQueue
🗃️ Шаг 1: Подключаем SQLite — никакой магии, просто работа
Открываем наш expense_bot.py. Сначала добавляем импорт встроенного модуля в самом верху:
python
import sqlite3
Теперь создадим функцию, которая при запуске бота создаст файл базы данных expenses.db и таблицу для наших расходов, если их ещё нет. Добавь этот код перед командой start:
python
def init_db():
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
amount REAL,
category TEXT,
date TEXT
)
''')
conn.commit()
conn.close()
Разберём по шагам:
- sqlite3.connect('expenses.db') — создаёт файл базы данных (или открывает существующий). Храниться он будет в той же папке, где лежит бот.
- CREATE TABLE IF NOT EXISTS expenses — создаём таблицу расходов с колонками: id (уникальный номер), user_id (чтобы разные люди не видели траты друг друга), amount (сумма), category (категория) и date (дата операции).
- conn.commit() и conn.close() — сохраняем изменения и закрываем соединение с БД.
После этого в самом низу, перед app.run_polling(), добавляем вызов этой функции:
python
init_db()
Теперь при каждом запуске бот будет проверять, есть ли база, и создавать её при необходимости.
📝 Шаг 2: Переписываем команду /add на сохранение в базу
Сейчас у нас в /add есть строки:
python
print(f"Добавлен расход: {amount} руб. на '{category}'")
await update.message.reply_text(f"✅ Записал: {amount} руб. — {category}\nЗа вечер потрачено: ??? (скоро подсчитаем)")
Заменяем их на полноценное сохранение в БД. Вот так будет выглядеть обновлённая версия add:
python
async def add(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not context.args:
await update.message.reply_text(
"Как писать:\n/add 150 такси\n/add 45 кофе"
)
return
try:
amount = float(context.args[0])
category = " ".join(context.args[1:]) if len(context.args) > 1 else "прочее"
user_id = update.effective_user.id
# Сохраняем в базу данных
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
INSERT INTO expenses (user_id, amount, category, date)
VALUES (?, ?, ?, datetime('now'))
''', (user_id, amount, category))
conn.commit()
conn.close()
# Считаем сумму трат за сегодня
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
SELECT SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
''', (user_id,))
today_total = cur.fetchone()[0] or 0
conn.close()
await update.message.reply_text(
f"✅ Записал: {amount} руб. — {category}\n"
f"💰 За сегодня потрачено: {today_total:.2f} руб."
)
except ValueError:
await update.message.reply_text("Сумма должна быть числом! Например: /add 99 кино")
Что поменялось? Во-первых, добавилась запись в БД через INSERT. Мы используем ? как placeholder — это безопасно и защищает от SQL‑инъекций. Во-вторых, после добавления новый запрос считает сумму всех трат текущего пользователя за сегодня (date(date) = date('now')). В‑третьих, мы подставляем user_id из update.effective_user.id — теперь разные люди не увидят чужие расходы.
📊 Шаг 3: Создаём команду /stats
Пришло время добавить реальную статистику, а не просто консольные «отладочные сообщения». Вставляем новый обработчик:
python
async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
# Расходы за сегодня
cur.execute('''
SELECT SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
''', (user_id,))
today_total = cur.fetchone()[0] or 0
# Расходы по категориям за сегодня
cur.execute('''
SELECT category, SUM(amount) FROM expenses
WHERE user_id = ? AND date(date) = date('now')
GROUP BY category
ORDER BY SUM(amount) DESC
''', (user_id,))
categories = cur.fetchall()
conn.close()
if not categories:
await update.message.reply_text("📭 Сегодня пока нет расходов. Добавь через /add!")
return
reply = f"📊 Статистика за сегодня:\n💰 Всего: {today_total:.2f} руб.\n\nПо категориям:\n"
for cat, total in categories:
reply += f"• {cat}: {total:.2f} руб.\n"
await update.message.reply_text(reply)
И, конечно, регистрируем новую команду в конце файла, где добавляются обработчики:
python
app.add_handler(CommandHandler("stats", stats))
Теперь пользователь может в любой момент вызвать /stats и увидеть, сколько уже успел потратить и на что именно.
🧶 Шаг 4: Команда /undo — отменяем последнюю операцию
Когда вечером понимаешь, что чипсы и пицца были лишними, можно быстро удалить последнюю запись. Создаём новую функцию:
python
async def undo(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
# Находим последнюю запись пользователя
cur.execute('''
SELECT id, amount, category FROM expenses
WHERE user_id = ?
ORDER BY id DESC LIMIT 1
''', (user_id,))
last = cur.fetchone()
if not last:
await update.message.reply_text("❌ Нет расходов, которые можно отменить.")
conn.close()
return
expense_id, amount, category = last
# Удаляем запись
cur.execute('DELETE FROM expenses WHERE id = ?', (expense_id,))
conn.commit()
conn.close()
await update.message.reply_text(f"🗑️ Отменил последний расход: {amount:.2f} руб. — {category}")
Регистрируем обработчик:
python
app.add_handler(CommandHandler("undo", undo))
Проверяем: /undo — и бот удалит самую свежую запись в базе.
📈 Шаг 5: Общая статистика /total
Иногда хочется посмотреть не только сегодняшнюю статистику, но и общую картину. Создаём команду /total:
python
async def total(update: Update, context: ContextTypes.DEFAULT_TYPE):
user_id = update.effective_user.id
conn = sqlite3.connect('expenses.db')
cur = conn.cursor()
cur.execute('''
SELECT SUM(amount) FROM expenses WHERE user_id = ?
''', (user_id,))
total_all = cur.fetchone()[0] or 0
cur.execute('''
SELECT COUNT(*) FROM expenses WHERE user_id = ?
''', (user_id,))
count_all = cur.fetchone()[0] or 0
conn.close()
await update.message.reply_text(
f"📈 Всего потрачено за всё время: {total_all:.2f} руб.\n"
f"📝 Количество записей: {count_all}"
)
Добавляем в регистрацию:
python
app.add_handler(CommandHandler("total", total))
🔘 Шаг 6: Инлайн‑клавиатура — красивые кнопки вместо слеша
Набор команд — это удобно, но представь: ты заходишь в бота, а тебе сразу показывается меню с кнопками. Добавим такую возможность.
Сначала создаём функцию, которая показывает меню, а затем — обработчик нажатия на кнопку.
python
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
async def menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
keyboard = [
[InlineKeyboardButton("➕ Добавить расход", callback_data="add")],
[InlineKeyboardButton("📊 Статистика за сегодня", callback_data="stats")],
[InlineKeyboardButton("📈 Полная статистика", callback_data="total")],
[InlineKeyboardButton("⏪ Отменить последний", callback_data="undo")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("Выбери действие:", reply_markup=reply_markup)
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
data = query.data
if data == "stats":
# Переиспользуем нашу функцию stats, но для callback-запроса
await stats(update, context)
elif data == "total":
await total(update, context)
elif data == "undo":
await undo(update, context)
elif data == "add":
await query.edit_message_text("Напиши: /add сумма категория\nНапример: /add 250 продукты")
Регистрируем обработчики:
python
app.add_handler(CommandHandler("menu", menu))
app.add_handler(CallbackQueryHandler(button_handler))
🕛 Шаг 7: Автоматический сброс — JobQueue
Помнишь, в начале статьи было обещание показать, как добавить автоматическое обнуление трат за вечер в полночь? Сейчас сделаем. Для этого используем встроенный планировщик задач.
python
async def reset_reminder(context: ContextTypes.DEFAULT_TYPE):
# Приветственное сообщение в выбранный чат
await context.bot.send_message(
chat_id=context.job.chat_id,
text="🌙 Полночь! Отчёт за вчера готов. Вызови /stats, чтобы посмотреть статистику!"
)
async def set_reset(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
# Планируем задачу на полночь каждый день
context.application.job_queue.run_daily(
reset_reminder,
time=datetime.time(hour=0, minute=0),
chat_id=chat_id
)
await update.message.reply_text("✅ Ежедневное напоминание на полночь установлено!")
Не забудь импортировать datetime в самом верху:
python
import datetime
Добавляем команду, которая запускает планировщик:
python
app.add_handler(CommandHandler("setreset", set_reset))
🧪 Шаг 8: Пробуем в деле
Запускаем бота заново:
bash
python expense_bot.py
Теперь у нас есть:
- /add 100 кофе — расход сохраняется в базу и выводит итог за сегодня
- /stats — показывает сегодняшнюю статистику по категориям
- /total — общая сумма за всё время
- /undo — отмена последней операции
- /menu — удобное меню с кнопками
- /setreset — автоматическое напоминание каждый вечер в полночь
https://./media/telegram-bot.jpg
📁 Финальная структура проекта
text
expense_bot.py # главный файл бота
expenses.db # база данных SQLite (создаётся автоматически)
💡 Что дальше?
База данных работает, статистика считается — бот уже реально полезен. Вот несколько идей для развития:
ИдеяСложностьЧто даётExcel-отчёт⭐⭐Экспорт всех трат в файл раз в месяцГрафики matplotlib⭐⭐⭐Визуализация: круговая диаграмма категорийВыбор даты для отчёта⭐⭐Отдельная статистика за любой день/неделюРедактирование категорий⭐⭐Возможность переименовать «прочее» в «развлечения»
🎯 Итоги второй части
Мы превратили консольный прототип в полноценного бота с настоящим хранилищем данных. Твои расходы больше не исчезнут после перезагрузки, появилась команда отмены и полезная статистика.
Что делать прямо сейчас
- Скопируй готовый код в expense_bot.py и запусти
- Добавь несколько трат через /add
- Попробуй /stats, /undo и удобное меню /menu
Подпишись на канал «Код доступа», чтобы не пропустить новые фишки: графики расходов, Excel-отчёты и голосовой ввод!
Если статья была полезной — поставь лайк и поделись с другом, который вечно не может вспомнить, куда улетела зарплата.
До встречи в третьей части! А я пошёл проверять, сколько сегодня потратил на кофе... снова 500 рублей 😅☕