domovoy_bot/handlers/payments.py
Admin cef2ac3714 v1.19 ИГ рассылка + объявления 14 дней + настройки
🔥 ОБЪЯВЛЕНИЯ:
• Срок действия: 7 → 14 дней

🎯 ИНИЦИАТИВНАЯ ГРУППА:
• Добавлена кнопка 'Рассылка ИГ' в меню
• Команда /ig_broadcast для членов ИГ
• Поддержка текста + фото + файлов
• Настройка лимита в веб-панели

⚙️ ВЕБ-ПАНЕЛЬ:
• Новая страница /settings
• Настройка лимита рассылки ИГ (секунды)
• Кнопки статистики фильтруют пользователей
• Индикатор активного фильтра

🎨 UI:
• Исправлен <font> → emoji (Telegram не поддерживает)
• Цветные заголовки через emoji

📁 ФАЙЛЫ:
• handlers/ads.py: 14 дней
• handlers/initiative_group.py: рассылка ИГ
• web/app.py: API настроек
• web/templates/settings.html: страница настроек
• utils/ui_styles.py: без <font> тегов
• database: settings table

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 17:01:30 +00:00

394 lines
15 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Напоминания о платежах ЖКХ
Только для администраторов
"""
import logging
from datetime import datetime, timedelta
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command, StateFilter
from aiogram.fsm.state import State, StatesGroup
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database.db import AsyncSessionLocal
from database.models import PaymentReminder, User
from utils.decorators import admin_only
from keyboards.inline import get_payment_type_menu
import config
import json
logger = logging.getLogger(__name__)
router = Router()
# =============================================================================
# FSM для создания напоминания
# =============================================================================
class PaymentState(StatesGroup):
waiting_for_type = State()
waiting_for_title = State()
waiting_for_amount = State()
waiting_for_due_date = State()
waiting_for_description = State()
# =============================================================================
# СОЗДАНИЕ НАПОМИНАНИЯ
# =============================================================================
@router.message(Command('payment_add'))
@admin_only
async def cmd_payment_add(message: Message, state: FSMContext):
"""
Добавить напоминание о платеже
Использование: /payment_add
"""
# Загружаем типы платежей
try:
with open('data/payment_links.json', 'r', encoding='utf-8') as f:
payment_types = json.load(f).get('payment_types', {})
except Exception:
payment_types = {}
await message.answer(
'💳 <b>Добавление напоминания о платеже</b>\n\n'
'Выберите тип платежа:\n\n'
'1⃣ ⚡ Электроэнергия\n'
'2⃣ 💧 Вода\n'
'3⃣ 🔥 Отопление\n'
'4⃣ 🔶 Газ\n'
'5⃣ 🏠 Содержание жилья\n'
'6⃣ 🗑️ Вывоз мусора\n'
'7⃣ 📡 Интернет\n'
'8⃣ 📋 Другое\n\n'
'Или отправьте номер типа (1-8):',
reply_markup=get_payment_type_menu(),
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_type)
@router.callback_query(F.data.startswith('payment_type_'))
async def callback_payment_type(callback: CallbackQuery, state: FSMContext):
"""Выбор типа платежа"""
type_map = {
'payment_type_electricity': ('electricity', '⚡ Электроэнергия'),
'payment_type_water': ('water', '💧 Вода'),
'payment_type_heating': ('heating', '🔥 Отопление'),
'payment_type_gas': ('gas', '🔶 Газ'),
'payment_type_maintenance': ('maintenance', '🏠 Содержание жилья'),
'payment_type_trash': ('trash', '🗑️ Вывоз мусора'),
'payment_type_internet': ('internet', '📡 Интернет'),
'payment_type_other': ('other', '📋 Другое'),
}
type_key, type_name = type_map.get(callback.data, ('other', '📋 Другое'))
await state.update_data(payment_type=type_key, type_name=type_name)
await callback.message.answer(
f'✅ Тип: {type_name}\n\n'
'Теперь отправьте название платежа:\n'
'<i>Например: "Квартплата за февраль"</i>',
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_title)
await callback.answer()
@router.message(PaymentState.waiting_for_type)
async def process_payment_type(message: Message, state: FSMContext):
"""Выбор типа платежа текстом"""
type_map = {
'1': ('electricity', '⚡ Электроэнергия'),
'2': ('water', '💧 Вода'),
'3': ('heating', '🔥 Отопление'),
'4': ('gas', '🔶 Газ'),
'5': ('maintenance', '🏠 Содержание жилья'),
'6': ('trash', '🗑️ Вывоз мусора'),
'7': ('internet', '📡 Интернет'),
'8': ('other', '📋 Другое'),
}
text = message.text.strip()
if text in type_map:
type_key, type_name = type_map[text]
await state.update_data(payment_type=type_key, type_name=type_name)
await message.answer(
f'✅ Тип: {type_name}\n\n'
'Теперь отправьте название платежа:\n'
'<i>Например: "Квартплата за февраль"</i>',
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_title)
else:
await message.answer('❌ Неверный тип. Выберите из меню или отправьте номер 1-8:')
@router.message(PaymentState.waiting_for_title)
async def process_payment_title(message: Message, state: FSMContext):
"""Получили название платежа"""
title = message.text
await state.update_data(title=title)
await message.answer(
f'✅ Название: {title}\n\n'
'Укажите сумму платежа (в рублях) или отправьте "0" если переменная:\n'
'<i>Например: 3500</i>',
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_amount)
@router.message(PaymentState.waiting_for_amount)
async def process_payment_amount(message: Message, state: FSMContext):
"""Получили сумму"""
text = message.text.strip()
try:
amount = int(text)
if amount < 0:
raise ValueError
except ValueError:
await message.answer('❌ Неверный формат суммы. Отправьте число:')
return
await state.update_data(amount=amount if amount > 0 else None)
await message.answer(
f'✅ Сумма: {amount if amount > 0 else "Переменная"}\n\n'
'Укажите дату оплаты (число месяца):\n'
'<i>Например: 10 (ежемесячно до 10 числа)</i>',
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_due_date)
@router.message(PaymentState.waiting_for_due_date)
async def process_payment_due_date(message: Message, state: FSMContext):
"""Получили дату оплаты"""
text = message.text.strip()
try:
due_date = int(text)
if due_date < 1 or due_date > 31:
raise ValueError
except ValueError:
await message.answer('❌ Неверный формат даты. Отправьте число от 1 до 31:')
return
await state.update_data(due_date=due_date)
await message.answer(
f'✅ Дата оплаты: {due_date} число каждого месяца\n\n'
'Отправьте описание (необязательно) или нажмите "Пропустить":',
parse_mode='HTML'
)
await state.set_state(PaymentState.waiting_for_description)
@router.message(PaymentState.waiting_for_description)
async def process_payment_description(message: Message, state: FSMContext):
"""Получили описание или пропустили"""
if message.text and message.text.lower() != 'пропустить':
description = message.text
else:
description = None
await state.update_data(description=description)
# Сохраняем напоминание в БД
data = await state.get_data()
# Создаём дату с текущим месяцем
now = datetime.utcnow()
due_date = now.replace(day=data['due_date'])
async with AsyncSessionLocal() as session:
reminder = PaymentReminder(
title=data['title'],
amount=data.get('amount'),
due_date=due_date,
description=description,
is_recurring=True,
created_by=message.from_user.id,
)
session.add(reminder)
await session.commit()
await session.refresh(reminder)
# Формируем сообщение о создании
type_name = data.get('type_name', '📋 Платёж')
amount_text = f'{data["amount"]}' if data.get('amount') else 'Переменная'
await message.answer(
f'✅ <b>Напоминание о платеже добавлено!</b>\n\n'
f'{type_name}{data["title"]}\n\n'
f'💰 Сумма: {amount_text}\n'
f'📅 Срок оплаты: {data["due_date"]} число каждого месяца\n'
f'🔖 ID: {reminder.reminder_id}\n\n'
f'Для просмотра: /payments',
parse_mode='HTML'
)
await state.clear()
# =============================================================================
# ПРОСМОТР НАПОМИНАНИЙ
# =============================================================================
@router.message(Command('payments'))
@router.message(Command('платежи'))
async def cmd_payments_list(message: Message):
"""Показать все активные напоминания о платежах"""
async with AsyncSessionLocal() as session:
stmt = (
select(PaymentReminder)
.where(PaymentReminder.is_active == True)
.order_by(PaymentReminder.due_date)
)
result = await session.execute(stmt)
reminders = list(result.scalars().all())
if not reminders:
await message.answer('✅ Нет активных напоминаний о платежах')
return
text = '💳 <b>Напоминания о платежах ЖКХ</b>\n\n'
current_day = datetime.utcnow().day
for i, r in enumerate(reminders, 1):
due_day = r.due_date.day if hasattr(r.due_date, 'day') else 10
days_left = (due_day - current_day) % 30
amount_text = f'{r.amount}' if r.amount else 'Переменная'
text += f'{i}. {r.title}\n'
text += f' └─ Сумма: {amount_text}\n'
text += f' └─ Срок: {due_day} число\n'
text += f' └─ Осталось дней: {days_left}\n\n'
text += '💡 Для добавления: /payment_add'
await message.answer(text, parse_mode='HTML')
# =============================================================================
# УДАЛЕНИЕ НАПОМИНАНИЯ
# =============================================================================
@router.message(Command('payment_del'))
@admin_only
async def cmd_payment_del(message: Message):
"""
Удалить напоминание
Использование: /payment_del <ID>
"""
args = message.text.split()
if len(args) < 2:
await message.answer(
'❌ <b>Использование:</b>\n'
'<code>/payment_del &lt;ID&gt;</code>',
parse_mode='HTML'
)
return
try:
reminder_id = int(args[1])
except ValueError:
await message.answer('❌ Неверный ID')
return
async with AsyncSessionLocal() as session:
reminder = await session.get(PaymentReminder, reminder_id)
if not reminder:
await message.answer(f'❌ Напоминание {reminder_id} не найдено')
return
reminder.is_active = False
await session.commit()
await message.answer(f'✅ Напоминание "{reminder.title}" удалено')
# =============================================================================
# РАССЫЛКА НАПОМИНАНИЙ
# =============================================================================
async def send_payment_reminders(bot):
"""Отправить напоминания о платежах (5-го и 9-го числа)"""
current_day = datetime.utcnow().day
# Проверяем нужно ли отправлять (5 или 9 число)
if current_day not in [5, 9]:
return
async with AsyncSessionLocal() as session:
# Получаем все активные напоминания
stmt = select(PaymentReminder).where(PaymentReminder.is_active == True)
result = await session.execute(stmt)
reminders = list(result.scalars().all())
if not reminders:
return
# Формируем сообщение
text = f'⏰ <b>Напоминание об оплате ЖКХ!</b>\n\n'
text += f'Сегодня {current_day} число. Не забудьте оплатить:\n\n'
for r in reminders:
due_day = r.due_date.day if hasattr(r.due_date, 'day') else 10
days_left = due_day - current_day
if days_left < 0:
days_left += 30
amount_text = f'{r.amount}' if r.amount else ''
emoji = '⚠️' if days_left <= 2 else ''
text += f'{emoji} <b>{r.title}</b>\n'
text += f' └─ Срок: {due_day} число (осталось {days_left} дн.)\n'
if amount_text:
text += f' └─ Сумма: {amount_text}\n'
text += '\n'
# Загружаем ссылки на оплату
try:
with open('data/payment_links.json', 'r', encoding='utf-8') as f:
payment_data = json.load(f)
links = payment_data.get('payment_links', [])
if links:
text += '💳 <b>Ссылки на оплату:</b>\n'
for link in links[:3]:
text += f'• [{link["name"]}]({link["url"]})\n'
except Exception:
pass
text += '\n_Оплатить можно через Госуслуги, Сбербанк или Тинькофф_'
# Отправляем в чат в тему "Важные объявления"
try:
await bot.send_message(
chat_id=config.ADMIN_CHAT_ID,
message_thread_id=config.get_topic_id('general'), # В общую тему
text=text,
parse_mode='HTML',
disable_web_page_preview=True
)
# Обновляем last_reminder_sent
for r in reminders:
r.last_reminder_sent = datetime.utcnow()
await session.commit()
logger.info(f'Напоминания о платежах отправлены ({len(reminders)} платежей)')
except Exception as e:
logger.error(f'Не удалось отправить напоминания о платежах: {e}')