feat(meter_reminder): add monthly utility meter reading reminders with toggle option in profile menu
This commit is contained in:
parent
1008ecaff9
commit
c8d01adec5
7 changed files with 155 additions and 12 deletions
BIN
data/service_images/meter_reminder.jpg
Normal file
BIN
data/service_images/meter_reminder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 725 KiB |
29
database/migrate_add_meter_reminders.py
Normal file
29
database/migrate_add_meter_reminders.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""
|
||||
Миграция v1.16: добавить колонку meter_reminders_enabled в таблицу users
|
||||
"""
|
||||
import sqlite3
|
||||
import asyncio
|
||||
|
||||
DB_PATH = 'database/domovoy.db'
|
||||
|
||||
async def migrate():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем, есть ли уже колонка meter_reminders_enabled
|
||||
cursor.execute("PRAGMA table_info(users)")
|
||||
columns = [col[1] for col in cursor.fetchall()]
|
||||
|
||||
if 'meter_reminders_enabled' in columns:
|
||||
print("ℹ️ Колонка meter_reminders_enabled уже существует")
|
||||
else:
|
||||
print("➕ Добавляем колонку meter_reminders_enabled в таблицу users...")
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN meter_reminders_enabled BOOLEAN DEFAULT 1")
|
||||
print("✅ Колонка добавлена")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✅ Миграция v1.16 завершена!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(migrate())
|
||||
|
|
@ -53,6 +53,7 @@ class User(Base):
|
|||
staircase = Column(Integer, nullable=True) # Подъезд
|
||||
floor = Column(Integer, nullable=True) # Этаж
|
||||
has_ai_access = Column(Boolean, default=False) # Доступ к ИИ-консультанту
|
||||
meter_reminders_enabled = Column(Boolean, default=True) # Напоминания о показаниях счетчиков
|
||||
|
||||
# Для анти-спама
|
||||
spam_warning_count = Column(Integer, default=0) # Количество предупреждений за спам
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ async def cb_profile(event):
|
|||
f"Подъезд: {user.staircase or '—'} | Этаж: {user.floor or '—'}\n"
|
||||
f"Рейтинг: {user.rating} 🏆")
|
||||
|
||||
kb = get_profile_keyboard()
|
||||
kb = get_profile_keyboard(user.meter_reminders_enabled if user else True)
|
||||
|
||||
if isinstance(event, Message):
|
||||
await event.answer(text, reply_markup=kb, parse_mode='HTML')
|
||||
|
|
@ -213,6 +213,39 @@ async def cb_profile(event):
|
|||
await event.answer()
|
||||
await event.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
|
||||
|
||||
@router.callback_query(F.data == "toggle_meter_reminders")
|
||||
async def cb_toggle_meter_reminders(callback: CallbackQuery):
|
||||
user_id = callback.from_user.id
|
||||
from utils.ui_styles import get_level_badge, get_user_level_key
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
user = await session.get(User, user_id)
|
||||
if not user:
|
||||
await callback.answer("Пользователь не найден", show_alert=True)
|
||||
return
|
||||
|
||||
new_val = not user.meter_reminders_enabled
|
||||
user.meter_reminders_enabled = new_val
|
||||
await session.commit()
|
||||
|
||||
await callback.answer(
|
||||
f"Напоминания о счетчиках {'включены' if new_val else 'отключены'}!"
|
||||
)
|
||||
|
||||
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
|
||||
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
|
||||
badge = get_level_badge(get_user_level_key(user_id, user.is_admin, user.verified, is_ig))
|
||||
|
||||
text = (f"👤 <b>ТВОЙ ПРОФИЛЬ</b>\n\n"
|
||||
f"ID: <code>{user_id}</code>\n"
|
||||
f"Статус: {badge}\n"
|
||||
f"Квартира: {user.apartment or 'Не указана'}\n"
|
||||
f"Подъезд: {user.staircase or '—'} | Этаж: {user.floor or '—'}\n"
|
||||
f"Рейтинг: {user.rating} 🏆")
|
||||
|
||||
kb = get_profile_keyboard(new_val)
|
||||
await callback.message.edit_text(text, reply_markup=kb, parse_mode='HTML')
|
||||
|
||||
@router.callback_query(F.data == "edit_location")
|
||||
async def edit_location_start(callback: CallbackQuery):
|
||||
await callback.message.edit_text("Введите ваш подъезд и этаж через пробел\n<i>Например: '3 5' (3 подъезд, 5 этаж)</i>", parse_mode='HTML')
|
||||
|
|
@ -225,10 +258,15 @@ async def process_location(message: Message):
|
|||
async with AsyncSessionLocal() as session:
|
||||
await session.execute(update(User).where(User.user_id == message.from_user.id).values(staircase=staircase, floor=floor))
|
||||
await session.commit()
|
||||
await message.answer(f"✅ Данные обновлены: Подъезд {staircase}, Этаж {floor}", reply_markup=get_profile_keyboard(), parse_mode='HTML')
|
||||
|
||||
user = await session.get(User, message.from_user.id)
|
||||
meter_enabled = user.meter_reminders_enabled if user else True
|
||||
|
||||
await message.answer(f"✅ Данные обновлены: Подъезд {staircase}, Этаж {floor}", reply_markup=get_profile_keyboard(meter_enabled), parse_mode='HTML')
|
||||
except Exception as e:
|
||||
await message.answer("❌ Ошибка формата. Введите два числа через пробел.")
|
||||
|
||||
|
||||
@router.message(Command('help'))
|
||||
@router.callback_query(F.data == 'help')
|
||||
async def cb_help(event):
|
||||
|
|
|
|||
|
|
@ -402,9 +402,15 @@ def get_poll_view_keyboard(poll_id: int, is_admin: bool = False) -> InlineKeyboa
|
|||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_profile_keyboard() -> InlineKeyboardMarkup:
|
||||
def get_profile_keyboard(meter_reminders_enabled: bool = True) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура профиля"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
# Кнопка переключения напоминаний о счетчиках
|
||||
reminders_text = "🔔 Напоминания: ВКЛ" if meter_reminders_enabled else "🔕 Напоминания: ВЫКЛ"
|
||||
builder.row(InlineKeyboardButton(text=reminders_text, callback_data="toggle_meter_reminders"))
|
||||
|
||||
builder.row(InlineKeyboardButton(text='🔄 Обновить', callback_data='my_profile'))
|
||||
builder.row(InlineKeyboardButton(text='🔙 Назад', callback_data='main_menu'))
|
||||
return builder.as_markup()
|
||||
|
||||
|
|
|
|||
56
services/meter_reminder.py
Normal file
56
services/meter_reminder.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import logging
|
||||
import asyncio
|
||||
from aiogram import Bot
|
||||
from aiogram.types import FSInputFile
|
||||
from database.db import AsyncSessionLocal
|
||||
from database.models import User
|
||||
from sqlalchemy import select
|
||||
|
||||
logger = logging.getLogger('domovoy.meter_reminder')
|
||||
|
||||
METER_REMINDER_IMAGE = 'data/service_images/meter_reminder.jpg'
|
||||
REMINDER_TEXT = (
|
||||
"⏰ <b>Не забудьте отправить показания счетчиков!</b>\n\n"
|
||||
"Уважаемые жильцы, пришло время подать показания приборов учета воды и электричества. "
|
||||
"Пожалуйста, передайте данные до 25-го числа включительно.\n\n"
|
||||
"<i>Вы можете отключить или включить эти напоминания в своем профиле в любой момент (команда /profile).</i>"
|
||||
)
|
||||
|
||||
async def send_meter_reminders(bot: Bot):
|
||||
"""Рассылка напоминаний о счетчиках верифицированным жильцам"""
|
||||
logger.info("🚀 Запуск ежемесячной рассылки напоминаний о счетчиках")
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(User).where(
|
||||
User.verified == True,
|
||||
User.is_banned == False,
|
||||
User.meter_reminders_enabled == True
|
||||
)
|
||||
users = (await session.execute(stmt)).scalars().all()
|
||||
|
||||
logger.info(f"Найдено {len(users)} пользователей для отправки напоминания")
|
||||
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
photo = FSInputFile(METER_REMINDER_IMAGE)
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
await bot.send_photo(
|
||||
chat_id=user.user_id,
|
||||
photo=photo,
|
||||
caption=REMINDER_TEXT,
|
||||
parse_mode='HTML'
|
||||
)
|
||||
success_count += 1
|
||||
logger.info(f"✅ Напоминание отправлено пользователю {user.user_id} (кв. {user.apartment})")
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
logger.error(f"❌ Ошибка отправки пользователю {user.user_id}: {e}")
|
||||
|
||||
# Небольшая задержка, чтобы не превысить лимиты Telegram API
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
logger.info(f"📊 Рассылка завершена. Успешно: {success_count}, Ошибок: {fail_count}")
|
||||
return success_count, fail_count
|
||||
|
|
@ -89,6 +89,16 @@ class Scheduler:
|
|||
replace_existing=True
|
||||
)
|
||||
|
||||
# Ежемесячная рассылка напоминаний о счетчиках (23 числа в 10:00)
|
||||
self.scheduler.add_job(
|
||||
self.monthly_meter_reminders,
|
||||
CronTrigger.from_crontab('0 10 23 * *'),
|
||||
id='monthly_meter_reminders',
|
||||
name='Напоминания о показаниях счетчиков',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
|
||||
# Ежечасная проверка напоминаний о событиях (каждый час)
|
||||
self.scheduler.add_job(
|
||||
self.hourly_event_reminders,
|
||||
|
|
@ -273,6 +283,15 @@ class Scheduler:
|
|||
except Exception as e:
|
||||
logger.error(f'Ошибка напоминаний о платежах: {e}')
|
||||
|
||||
async def monthly_meter_reminders(self):
|
||||
"""Ежемесячная рассылка напоминаний о показаниях счетчиков"""
|
||||
try:
|
||||
from services.meter_reminder import send_meter_reminders
|
||||
await send_meter_reminders(self.bot)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка рассылки напоминаний о счетчиках: {e}')
|
||||
|
||||
|
||||
async def hourly_event_reminders(self):
|
||||
"""Ежечасная проверка напоминаний о событиях"""
|
||||
try:
|
||||
|
|
@ -397,8 +416,10 @@ class Scheduler:
|
|||
async with http_session.post(url, json=payload) as r:
|
||||
return (await r.json())['result']['message_id'] if r.status == 200 else None
|
||||
|
||||
msg_id = None
|
||||
# 1. Отправка в основной чат
|
||||
msg_id = await send_content(ADMIN_CHAT_ID, thread_id=post.topic_id)
|
||||
if post.recipients in ['chat_only', 'all_and_chat']:
|
||||
msg_id = await send_content(ADMIN_CHAT_ID, thread_id=post.topic_id)
|
||||
|
||||
# 2. Если нужно, рассылка жильцам в личку
|
||||
if post.recipients in ['all_verified', 'all_and_chat']:
|
||||
|
|
@ -415,14 +436,6 @@ class Scheduler:
|
|||
post.sent_at = datetime.utcnow()
|
||||
post.message_id = msg_id
|
||||
await session.commit()
|
||||
logger.info(f"✅ Пост {post.id} отправлен")
|
||||
|
||||
# Обновляем статус поста
|
||||
post.status = 'sent'
|
||||
post.sent_at = datetime.utcnow()
|
||||
post.message_id = message_id
|
||||
await session.commit()
|
||||
|
||||
logger.info(f"✅ Пост отправлен: id={post.id}, тема={post.topic_name}")
|
||||
|
||||
# Уведомляем админа
|
||||
|
|
|
|||
Loading…
Reference in a new issue