Stabilize staff concurrency and premium emoji; enable verified Drone deployment
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2026-09-13 19:48:41 +09:00
parent 733298bf06
commit cb35bb12e3
86 changed files with 2993 additions and 2455 deletions

View File

@@ -1,6 +1,11 @@
"""
Расширенная админ-панель для управления розыгрышами
"""
from src.utils.errors import public_error
from src.core.access import is_admin
from src.core.access import is_admin as check_admin_access
from sqlalchemy.orm import selectinload
from datetime import timezone
import logging
from aiogram import Router, F
from aiogram.types import (
@@ -112,9 +117,6 @@ class AdminStates(StatesGroup):
admin_router = Router()
def is_admin(user_id: int) -> bool:
"""Проверка прав администратора (быстрая проверка только .env)"""
return user_id in ADMIN_IDS
def is_super_admin(user_id: int) -> bool:
@@ -122,23 +124,6 @@ def is_super_admin(user_id: int) -> bool:
return user_id in ADMIN_IDS
async def check_admin_access(user_id: int) -> bool:
"""
Асинхронная проверка доступа администратора.
Проверяет как главных администраторов (.env), так и назначенных (БД)
"""
# Сначала проверяем главных администраторов
if user_id in ADMIN_IDS:
return True
# Затем проверяем назначенных администраторов в БД
async with async_session_maker() as session:
from sqlalchemy import select
result = await session.execute(
select(User).where(User.telegram_id == user_id, User.is_admin == True)
)
user = result.scalar_one_or_none()
return user is not None
def get_admin_main_keyboard() -> InlineKeyboardMarkup:
@@ -233,7 +218,7 @@ async def show_admin_panel(callback: CallbackQuery):
# УПРАВЛЕНИЕ РОЗЫГРЫШАМИ
# ======================
@admin_router.callback_query(F.data == "admin_lotteries")
@admin_router.callback_query(F.data.in_({"admin_lotteries", "lottery_management"}))
async def show_lottery_management(callback: CallbackQuery):
"""Управление розыгрышами"""
if not await check_admin_access(callback.from_user.id):
@@ -277,7 +262,7 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
logging.info(f"✅ Состояние установлено: AdminStates.lottery_title")
except Exception as e:
logging.error(f"❌ Ошибка при создании розыгрыша: {e}")
await callback.message.answer(f"❌ Ошибка: {str(e)}")
await callback.message.answer(f"❌ Ошибка: {public_error(e)}")
@admin_router.message(StateFilter(AdminStates.lottery_title))
@@ -784,7 +769,7 @@ async def choose_user_to_add(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.add_participant_user)
@admin_router.message(StateFilter(AdminStates.add_participant_user))
@admin_router.message(StateFilter(AdminStates.add_participant_user), F.text)
async def process_add_participant(message: Message, state: FSMContext):
"""Обработка добавления участника"""
if not await check_admin_access(message.from_user.id):
@@ -905,7 +890,7 @@ async def remove_participant_select_lottery(callback: CallbackQuery, state: FSMC
)
@admin_router.message(StateFilter(AdminStates.remove_participant_user))
@admin_router.message(StateFilter(AdminStates.remove_participant_user), F.text)
async def process_remove_participant(message: Message, state: FSMContext):
"""Обработка удаления участника"""
if not await check_admin_access(message.from_user.id):
@@ -1180,7 +1165,7 @@ async def start_search_participants(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.participant_search)
@admin_router.message(StateFilter(AdminStates.participant_search))
@admin_router.message(StateFilter(AdminStates.participant_search), F.text)
async def process_search_participants(message: Message, state: FSMContext):
"""Обработка поиска участников"""
if not await check_admin_access(message.from_user.id):
@@ -1490,7 +1475,7 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_add_to_"))
async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext):
"""Добавление участника в конкретный розыгрыш"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -1520,10 +1505,10 @@ async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext)
await state.set_state(AdminStates.add_to_lottery_user)
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user))
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user), F.text)
async def process_add_to_lottery(message: Message, state: FSMContext):
"""Обработка добавления участника в конкретный розыгрыш"""
if not is_admin(message.from_user.id):
if not await is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
@@ -1624,7 +1609,7 @@ async def process_add_to_lottery(message: Message, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_remove_from_"))
async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMContext):
"""Удаление участника из конкретного розыгрыша"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -1656,10 +1641,10 @@ async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMCon
await state.set_state(AdminStates.remove_from_lottery_user)
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user))
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user), F.text)
async def process_remove_from_lottery(message: Message, state: FSMContext):
"""Обработка удаления участника из конкретного розыгрыша"""
if not is_admin(message.from_user.id):
if not await is_admin(message.from_user.id):
await message.answer("❌ Недостаточно прав")
return
@@ -1741,7 +1726,7 @@ async def process_remove_from_lottery(message: Message, state: FSMContext):
@admin_router.callback_query(F.data.startswith("admin_check_winners_"))
async def check_winners(callback: CallbackQuery):
"""Проверка подтверждения победителей"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -1810,7 +1795,7 @@ async def check_winners(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_redraw_"))
async def redraw_lottery(callback: CallbackQuery):
"""Повторный розыгрыш для неподтверждённых призов"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -1860,123 +1845,23 @@ async def redraw_lottery(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_redraw_confirm_"))
async def confirm_redraw(callback: CallbackQuery):
"""Подтверждение и выполнение повторного розыгрыша"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
if not await check_admin_access(callback.from_user.id):
await callback.answer("Недостаточно прав", show_alert=True)
return
from src.core.redraw_services import redraw_unclaimed
from src.utils.notifications import notify_winners_async
lottery_id = int(callback.data.split("_")[-1])
await callback.answer("⏳ Проводится повторный розыгрыш...", show_alert=True)
await callback.answer("Проверяю неподтверждённые призы")
async with async_session_maker() as session:
from sqlalchemy import select, delete
from ..core.models import Winner, Participation
import random
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.message.edit_text("❌ Розыгрыш не найден")
return
winners = await LotteryService.get_winners(session, lottery_id)
# Собираем подтверждённые и неподтверждённые
confirmed_winners = [w for w in winners if w.is_claimed]
unconfirmed_winners = [w for w in winners if not w.is_claimed]
if not unconfirmed_winners:
await callback.message.edit_text(
"Все победители уже подтверждены!",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
])
)
return
# Собираем исключённые счета (подтверждённые победители + бывшие неподтверждённые)
excluded_accounts = set()
for w in winners:
if w.account_number:
excluded_accounts.add(w.account_number)
# Получаем всех участников, исключая уже выигравших
result = await session.execute(
select(Participation)
.where(Participation.lottery_id == lottery_id)
)
all_participations = result.scalars().all()
# Фильтруем участников
available_participations = [
p for p in all_participations
if p.account_number not in excluded_accounts
]
if not available_participations:
await callback.message.edit_text(
"❌ Нет доступных участников для переигровки.\n"
"Все участники уже выиграли или были исключены.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
])
)
return
# Удаляем неподтверждённых победителей
for winner in unconfirmed_winners:
await session.delete(winner)
# Проводим розыгрыш для неподтверждённых мест
new_winners_text = ""
random.shuffle(available_participations)
for i, old_winner in enumerate(unconfirmed_winners):
if i >= len(available_participations):
break
new_participation = available_participations[i]
# Создаём нового победителя
new_winner = Winner(
lottery_id=lottery_id,
user_id=new_participation.user_id,
account_number=new_participation.account_number,
place=old_winner.place,
is_manual=False,
is_claimed=False
)
session.add(new_winner)
# Исключаем из следующих итераций
if new_participation.account_number:
excluded_accounts.add(new_participation.account_number)
prize = lottery.prizes[old_winner.place - 1] if lottery.prizes and len(lottery.prizes) >= old_winner.place else "Приз"
name = new_participation.account_number or f"ID: {new_participation.user_id}"
new_winners_text += f"🏆 {old_winner.place} место: {name}{prize}\n"
await session.commit()
# Отправляем уведомления новым победителям
from ..utils.notifications import notify_winners_async
try:
winners = await redraw_unclaimed(session, lottery_id)
if winners:
await notify_winners_async(callback.bot, session, lottery_id)
except Exception as e:
logger.error(f"Ошибка при отправке уведомлений: {e}")
text = "Повторный розыгрыш завершён.\n" if winners else "Нет просроченных неподтверждённых призов или новых участников."
for winner in winners:
text += f"{winner.place} место: {winner.account_number or winner.user_id}{winner.prize}\n"
await callback.message.answer(text)
text = f"🎉 Повторный розыгрыш завершён!\n\n"
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
text += f"Новые победители:\n{new_winners_text}\n"
text += "✅ Уведомления отправлены новым победителям"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Проверить победителей", callback_data=f"admin_check_winners_{lottery_id}")],
[InlineKeyboardButton(text="🔙 К розыгрышу", callback_data=f"admin_lottery_detail_{lottery_id}")]
])
)
# ======================
@@ -1986,7 +1871,7 @@ async def confirm_redraw(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_del_lottery_"))
async def delete_lottery_confirm(callback: CallbackQuery):
"""Подтверждение удаления розыгрыша"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -2020,7 +1905,7 @@ async def delete_lottery_confirm(callback: CallbackQuery):
@admin_router.callback_query(F.data.startswith("admin_del_lottery_yes_"))
async def delete_lottery_execute(callback: CallbackQuery):
"""Выполнение удаления розыгрыша"""
if not is_admin(callback.from_user.id):
if not await is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
@@ -2037,15 +1922,7 @@ async def delete_lottery_execute(callback: CallbackQuery):
lottery_title = lottery.title
# Удаляем победителей
await session.execute(sql_delete(Winner).where(Winner.lottery_id == lottery_id))
# Удаляем участников
await session.execute(sql_delete(Participation).where(Participation.lottery_id == lottery_id))
# Удаляем розыгрыш
await session.delete(lottery)
await session.commit()
await LotteryService.delete_lottery(session, lottery_id)
await callback.message.edit_text(
f"✅ Розыгрыш удалён\n\n"
@@ -2384,7 +2261,7 @@ async def show_participants_report(callback: CallbackQuery):
select(
User.first_name,
User.username,
User.account_number,
func.min(Participation.account_number),
func.count(Participation.id).label('participations')
)
.join(Participation)
@@ -2396,11 +2273,11 @@ async def show_participants_report(callback: CallbackQuery):
# Участники с аккаунтами vs без
users_with_accounts = await session.scalar(
select(func.count(User.id)).where(User.account_number.isnot(None))
select(func.count(User.id)).where(User.accounts.any())
)
users_without_accounts = await session.scalar(
select(func.count(User.id)).where(User.account_number.is_(None))
select(func.count(User.id)).where(~User.accounts.any())
)
text = "📈 Отчет по участникам\n\n"
@@ -2951,7 +2828,7 @@ async def process_winner_place(message: Message, state: FSMContext):
await state.set_state(AdminStates.set_winner_user)
@admin_router.message(StateFilter(AdminStates.set_winner_user))
@admin_router.message(StateFilter(AdminStates.set_winner_user), F.text)
async def process_winner_user(message: Message, state: FSMContext):
"""Обработка пользователя-победителя (по ID, username или номеру счета)"""
if not await check_admin_access(message.from_user.id):
@@ -3391,6 +3268,8 @@ async def do_remove_winner(callback: CallbackQuery):
username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
# Удаляем победителя
from src.core.models import WinnerVerification
await session.execute(delete(WinnerVerification).where(WinnerVerification.winner_id == winner_id))
await session.execute(delete(Winner).where(Winner.id == winner_id))
await session.commit()
@@ -3443,7 +3322,7 @@ async def choose_lottery_for_draw(callback: CallbackQuery):
)
])
buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_draws")])
buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_lotteries")])
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@@ -3546,7 +3425,7 @@ async def conduct_lottery_draw(callback: CallbackQuery):
except Exception as e:
logger.error(f"Ошибка при проведении розыгрыша {lottery_id}: {e}", exc_info=True)
await session.rollback()
await callback.answer(f"❌ Ошибка: {e}", show_alert=True)
await callback.answer(f"❌ Ошибка: {public_error(e)}", show_alert=True)
return
if winners_dict:
@@ -3587,7 +3466,7 @@ async def conduct_lottery_draw(callback: CallbackQuery):
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="◀️ К розыгрышам", callback_data="admin_draws")]
[InlineKeyboardButton(text="◀️ К розыгрышам", callback_data="admin_lotteries")]
])
)
else:
@@ -4095,7 +3974,7 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
except Exception as e:
logger.error(f"💥 Исключение при смене типа отображения: {e}")
text = f"❌ Ошибка: {str(e)}"
text = f"❌ Ошибка: {public_error(e)}"
await callback.answer("❌ Ошибка при сохранении!", show_alert=True)
return
await callback.answer("❌ Ошибка сохранения", show_alert=True)
@@ -4402,6 +4281,7 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
winners_dict: Словарь с победителями {место: данные}
"""
import asyncio
from html import escape
# Получаем розыгрыш
lottery = await LotteryService.get_lottery(session, lottery_id)
@@ -4428,7 +4308,7 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
# Формируем сообщение с результатами
message = (
f"📢 <b>Результаты розыгрыша</b>\n\n"
f"🎯 <b>{lottery.title}</b>\n\n"
f"🎯 <b>{escape(lottery.title)}</b>\n\n"
f"🏆 <b>Победители:</b>\n"
)
@@ -4451,7 +4331,7 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
# Формируем строку победителя
winner_name = nickname if nickname else display_name if display_name else f"Счет {winner.account_number}"
message += f"{winner.place} место: {winner_name}\n"
message += f"{winner.place} место: {escape(winner_name)}\n"
message += (
f"\n🎁 Поздравляем победителей!\n"
@@ -4459,6 +4339,8 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
)
# Рассылаем всем кроме победителей
await session.commit()
from src.utils.delivery import send_background
success_count = 0
fail_count = 0
@@ -4468,9 +4350,8 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
continue
try:
await bot.send_message(
user.telegram_id,
message,
await send_background(bot, chat_id=user.telegram_id,
text=message,
parse_mode="HTML"
)
success_count += 1
@@ -4488,100 +4369,19 @@ async def _notify_all_participants_about_results(bot, session: AsyncSession, lot
@admin_router.callback_query(F.data == "admin_export_users")
async def admin_export_users(callback: CallbackQuery):
"""Экспорт всех пользователей в XLSX"""
if not await check_admin_access(callback.from_user.id):
await callback.answer("Доступ запрещен", show_alert=True)
await callback.answer("Недостаточно прав", show_alert=True)
return
await callback.answer("Готовлю файл")
import asyncio
from aiogram.types import BufferedInputFile
from src.utils.spreadsheets import export_users
async with async_session_maker() as session:
users = await UserService.get_all_users(session)
content = await asyncio.to_thread(export_users, users)
await callback.message.answer_document(BufferedInputFile(content, filename="users.xlsx"),
caption=f"Пользователей: {len(users)}")
await callback.answer("⏳ Формирую файл...", show_alert=False)
try:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from io import BytesIO
from aiogram.types import BufferedInputFile
async with async_session_maker() as session:
# Получаем всех пользователей
all_users = await UserService.get_all_users(session)
# Создаем Excel файл
wb = Workbook()
ws = wb.active
ws.title = "Пользователи"
# Заголовки
headers = [
'Telegram ID', 'Username', 'Имя', 'Фамилия', 'Никнейм',
'Телефон', 'Клубная карта', 'Зарегистрирован', 'Админ',
'Код верификации', 'Дата создания', 'Последняя активность', 'Заблокирован в чате'
]
# Стиль для заголовков
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
header_font = Font(bold=True, color="FFFFFF")
for col_num, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col_num, value=header)
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal="center", vertical="center")
# Данные пользователей
for row_num, user in enumerate(all_users, 2):
ws.cell(row=row_num, column=1, value=user.telegram_id)
ws.cell(row=row_num, column=2, value=user.username or '')
ws.cell(row=row_num, column=3, value=user.first_name or '')
ws.cell(row=row_num, column=4, value=user.last_name or '')
ws.cell(row=row_num, column=5, value=user.nickname or '')
ws.cell(row=row_num, column=6, value=user.phone or '')
ws.cell(row=row_num, column=7, value=user.club_card_number or '')
ws.cell(row=row_num, column=8, value='Да' if user.is_registered else 'Нет')
ws.cell(row=row_num, column=9, value='Да' if user.is_admin else 'Нет')
ws.cell(row=row_num, column=10, value=user.verification_code or '')
ws.cell(row=row_num, column=11, value=user.created_at.strftime('%d.%m.%Y %H:%M') if user.created_at else '')
ws.cell(row=row_num, column=12, value=user.last_activity.strftime('%d.%m.%Y %H:%M') if user.last_activity else '')
ws.cell(row=row_num, column=13, value='Да' if user.is_chat_banned else 'Нет')
# Автоподбор ширины колонок
for column in ws.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
ws.column_dimensions[column_letter].width = adjusted_width
# Сохраняем в BytesIO
excel_file = BytesIO()
wb.save(excel_file)
excel_file.seek(0)
# Отправляем файл
filename = f"users_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
file = BufferedInputFile(excel_file.read(), filename=filename)
registered_count = len([u for u in all_users if u.is_registered])
await callback.message.answer_document(
document=file,
caption=(
f"📥 <b>Экспорт пользователей</b>\n\n"
f"📊 Всего пользователей: {len(all_users)}\n"
f"✅ Зарегистрировано: {registered_count}\n"
f"📅 Дата экспорта: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
),
parse_mode="HTML"
)
await callback.answer("✅ Файл отправлен", show_alert=False)
except Exception as e:
logger.error(f"Ошибка экспорта пользователей: {e}")
await callback.answer("❌ Ошибка при создании файла", show_alert=True)
@admin_router.callback_query(F.data == "admin_import_users")
@@ -4619,172 +4419,32 @@ async def admin_import_users_start(callback: CallbackQuery, state: FSMContext):
@admin_router.message(StateFilter(AdminStates.import_users_json), F.document)
async def admin_import_users_process(message: Message, state: FSMContext):
"""Обработка импорта пользователей из XLSX"""
if not await check_admin_access(message.from_user.id):
return
# Проверяем формат файла
if not message.document.file_name.endswith('.xlsx'):
await message.answer("❌ Неверный формат файла. Отправьте XLSX файл.")
import asyncio
from src.utils.spreadsheets import read_users_xlsx, MAX_FILE_BYTES
from src.core.import_services import import_users
if not (message.document.file_name or "").lower().endswith(".xlsx"):
await message.answer("Отправьте XLSX файл.")
return
status_msg = await message.answer("⏳ Загружаю файл...")
if (message.document.file_size or 0) > MAX_FILE_BYTES:
await message.answer("Файл должен быть не больше 5 МБ.")
return
status = await message.answer("Импортирую пользователей…")
try:
from openpyxl import load_workbook
from io import BytesIO
# Скачиваем файл
file = await message.bot.get_file(message.document.file_id)
file_content = await message.bot.download_file(file.file_path)
# Читаем Excel файл
excel_file = BytesIO(file_content.read())
wb = load_workbook(excel_file, read_only=True)
ws = wb.active
# Читаем данные
rows = list(ws.iter_rows(values_only=True))
if len(rows) < 2:
await status_msg.edit_text("❌ Файл пуст или не содержит данных.")
await state.clear()
return
# Первая строка - заголовки
headers = [h if h else '' for h in rows[0]]
# Находим индекс колонки Telegram ID
try:
telegram_id_idx = headers.index('Telegram ID')
except ValueError:
await status_msg.edit_text("Не найдена обязательная колонка 'Telegram ID'.")
await state.clear()
return
# Создаем маппинг индексов для других полей
field_mapping = {
'Username': 'username',
'Имя': 'first_name',
'Фамилия': 'last_name',
'Никнейм': 'nickname',
'Телефон': 'phone',
'Клубная карта': 'club_card_number',
'Зарегистрирован': 'is_registered',
'Код верификации': 'verification_code'
}
users_data = []
for row in rows[1:]: # Пропускаем заголовки
if not row or len(row) <= telegram_id_idx or not row[telegram_id_idx]:
continue
user_dict = {'telegram_id': row[telegram_id_idx]}
for header_name, field_name in field_mapping.items():
try:
idx = headers.index(header_name)
if idx < len(row):
value = row[idx]
if field_name == 'is_registered':
user_dict[field_name] = value in ['Да', 'Yes', 'True', True, 1]
else:
user_dict[field_name] = value if value else None
except (ValueError, IndexError):
user_dict[field_name] = None
users_data.append(user_dict)
await status_msg.edit_text(
f"📊 Найдено пользователей в файле: {len(users_data)}\n"
f"⏳ Импортирую..."
)
# Импортируем пользователей
async with async_session_maker() as session:
added_count = 0
updated_count = 0
error_count = 0
for user_data in users_data:
try:
telegram_id = user_data.get('telegram_id')
if not telegram_id:
error_count += 1
continue
# Преобразуем telegram_id в int если это строка
try:
telegram_id = int(telegram_id)
except (ValueError, TypeError):
error_count += 1
continue
# Ищем существующего пользователя
existing_user = await UserService.get_user_by_telegram_id(session, telegram_id)
if existing_user:
# Обновляем существующего
if user_data.get('username') is not None:
existing_user.username = user_data.get('username')
if user_data.get('first_name') is not None:
existing_user.first_name = user_data.get('first_name')
if user_data.get('last_name') is not None:
existing_user.last_name = user_data.get('last_name')
if user_data.get('nickname') is not None:
existing_user.nickname = user_data.get('nickname')
if user_data.get('phone') is not None:
existing_user.phone = user_data.get('phone')
if user_data.get('club_card_number') is not None:
existing_user.club_card_number = user_data.get('club_card_number')
if user_data.get('is_registered') is not None:
existing_user.is_registered = user_data.get('is_registered', False)
if user_data.get('verification_code') is not None:
existing_user.verification_code = user_data.get('verification_code')
# is_admin не обновляем из соображений безопасности
updated_count += 1
else:
# Создаем нового
new_user = User(
telegram_id=telegram_id,
username=user_data.get('username'),
first_name=user_data.get('first_name'),
last_name=user_data.get('last_name'),
nickname=user_data.get('nickname'),
phone=user_data.get('phone'),
club_card_number=user_data.get('club_card_number'),
is_registered=user_data.get('is_registered', False),
is_admin=False, # Не импортируем админов из соображений безопасности
verification_code=user_data.get('verification_code')
)
session.add(new_user)
added_count += 1
except Exception as e:
logger.error(f"Ошибка импорта пользователя {user_data.get('telegram_id')}: {e}")
error_count += 1
# Сохраняем изменения
await session.commit()
# Итоговый отчет
await status_msg.edit_text(
f"✅ <b>Импорт завершен!</b>\n\n"
f"📊 <b>Статистика:</b>\n"
f" Добавлено: {added_count}\n"
f"🔄 Обновлено: {updated_count}\n"
f"❌ Ошибок: {error_count}\n"
f"📝 Всего обработано: {added_count + updated_count}",
parse_mode="HTML"
)
content = await message.bot.download_file(file.file_path)
records = await asyncio.to_thread(read_users_xlsx, content.read())
added, updated, errors = await import_users(records)
await status.edit_text(f"Добавлено: {added}. Обновлено: {updated}. Ошибок: {errors}.")
except ValueError as error:
await status.edit_text(str(error))
except Exception:
logger.exception("Не удалось импортировать XLSX")
await status.edit_text("Не удалось обработать файл. Проверьте формат данных.")
finally:
await state.clear()
except Exception as e:
logger.error(f"Ошибка импорта пользователей: {e}")
await status_msg.edit_text(f"❌ Ошибка импорта: {str(e)}\n\nПроверьте формат файла.")
await state.clear()
# ============================================================================
@@ -5176,7 +4836,7 @@ async def admin_broadcast_channels_menu(callback: CallbackQuery, state: FSMConte
buttons = [
[InlineKeyboardButton(text="✨ Добавить канал/группу", callback_data="admin_broadcast_add_channel")],
[InlineKeyboardButton(text="📜 Список всех", callback_data="admin_broadcast_list_channels")],
[InlineKeyboardButton(text="📜 Список всех", callback_data="admin_broadcast_channels")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_broadcast")]
]
@@ -5278,7 +4938,7 @@ async def admin_broadcast_add_channel_id(message: Message, state: FSMContext):
f"• Бот не добавлен в канал/группу\n"
f"• Неверный ID\n"
f"• Бот не имеет прав администратора\n\n"
f"Детали: {str(e)}",
f"Детали: {public_error(e)}",
parse_mode="HTML"
)
await state.clear()
@@ -5551,7 +5211,7 @@ async def admin_users_search_prompt(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.user_management_search)
@admin_router.message(AdminStates.user_management_search)
@admin_router.message(AdminStates.user_management_search, F.text)
async def admin_users_search_process(message: Message, state: FSMContext):
"""Обработка поискового запроса"""
if not await check_admin_access(message.from_user.id):
@@ -5932,7 +5592,7 @@ async def add_admin_start(callback: CallbackQuery, state: FSMContext):
await state.set_state(AdminStates.admin_add_search)
@admin_router.message(StateFilter(AdminStates.admin_add_search))
@admin_router.message(StateFilter(AdminStates.admin_add_search), F.text)
async def search_user_for_admin(message: Message, state: FSMContext):
"""Поиск пользователя для назначения админом"""
if not is_super_admin(message.from_user.id):
@@ -6085,4 +5745,4 @@ async def confirm_remove_admin(callback: CallbackQuery, state: FSMContext):
# Экспорт роутера
__all__ = ['admin_router']
__all__ = ['admin_router']