diff --git a/data/service_images/meter_reminder.jpg b/data/service_images/meter_reminder.jpg
new file mode 100644
index 0000000..47c6f57
Binary files /dev/null and b/data/service_images/meter_reminder.jpg differ
diff --git a/database/migrate_add_meter_reminders.py b/database/migrate_add_meter_reminders.py
new file mode 100644
index 0000000..5042a02
--- /dev/null
+++ b/database/migrate_add_meter_reminders.py
@@ -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())
diff --git a/database/models.py b/database/models.py
index 4677172..7ec1929 100644
--- a/database/models.py
+++ b/database/models.py
@@ -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) # Количество предупреждений за спам
diff --git a/handlers/users.py b/handlers/users.py
index 3987c84..cbb6212 100644
--- a/handlers/users.py
+++ b/handlers/users.py
@@ -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"👤 ТВОЙ ПРОФИЛЬ\n\n"
+ f"ID: {user_id}\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Например: '3 5' (3 подъезд, 5 этаж)", 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):
diff --git a/keyboards/inline.py b/keyboards/inline.py
index e7e10b0..25392e2 100644
--- a/keyboards/inline.py
+++ b/keyboards/inline.py
@@ -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()
+
diff --git a/services/meter_reminder.py b/services/meter_reminder.py
new file mode 100644
index 0000000..64bac7c
--- /dev/null
+++ b/services/meter_reminder.py
@@ -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 = (
+ "⏰ Не забудьте отправить показания счетчиков!\n\n"
+ "Уважаемые жильцы, пришло время подать показания приборов учета воды и электричества. "
+ "Пожалуйста, передайте данные до 25-го числа включительно.\n\n"
+ "Вы можете отключить или включить эти напоминания в своем профиле в любой момент (команда /profile)."
+)
+
+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
diff --git a/services/scheduler.py b/services/scheduler.py
index 6ed8db0..052ab3a 100644
--- a/services/scheduler.py
+++ b/services/scheduler.py
@@ -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}")
# Уведомляем админа