init commit
This commit is contained in:
12
.env.example
Normal file
12
.env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Переменные окружения для телеграм-бота
|
||||||
|
BOT_TOKEN=your_bot_token_here
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
|
||||||
|
|
||||||
|
# Для PostgreSQL раскомментируйте и настройте:
|
||||||
|
# DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
|
||||||
|
|
||||||
|
# ID администраторов (через запятую)
|
||||||
|
ADMIN_IDS=123456789,987654321
|
||||||
|
|
||||||
|
# Настройки логирования
|
||||||
|
LOG_LEVEL=INFO
|
||||||
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Файлы среды
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# База данных
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Виртуальные окружения
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Системные файлы
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
177
ADMIN_CHANGELOG.md
Normal file
177
ADMIN_CHANGELOG.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# 🚀 Обновления админ-панели - Changelog
|
||||||
|
|
||||||
|
## ✨ Добавленные функции (12 ноября 2025)
|
||||||
|
|
||||||
|
### 👥 Расширенное управление участниками
|
||||||
|
|
||||||
|
#### ➕ Одиночное добавление участников
|
||||||
|
- **Поиск по ID и username**: Поддержка @username и Telegram ID
|
||||||
|
- **Автоматическая валидация**: Проверка существования пользователя в системе
|
||||||
|
- **Предотвращение дублей**: Защита от повторного добавления
|
||||||
|
- **Детальная информация**: Показ имени, username, ID после добавления
|
||||||
|
|
||||||
|
#### 📥 Массовое добавление участников
|
||||||
|
- **Поддержка смешанного ввода**: @username, ID через запятую
|
||||||
|
- **Пакетная обработка**: Добавление множества участников одновременно
|
||||||
|
- **Детальный отчет**: Статистика успешно добавленных/пропущенных
|
||||||
|
- **Обработка ошибок**: Пропуск невалидных пользователей с уведомлениями
|
||||||
|
|
||||||
|
#### ➖ Одиночное удаление участников
|
||||||
|
- **Интерактивный выбор**: Показ списка участников с кнопками
|
||||||
|
- **Подтверждение удаления**: Безопасное удаление с проверкой
|
||||||
|
- **Детальная информация**: Показ удаленного пользователя
|
||||||
|
|
||||||
|
#### 📤 Массовое удаление участников
|
||||||
|
- **Пакетная обработка**: Удаление множества участников одновременно
|
||||||
|
- **Парсинг входных данных**: @username, ID через запятую
|
||||||
|
- **Статистика операций**: Количество удаленных/не найденных
|
||||||
|
- **Безопасность**: Игнорирование несуществующих пользователей
|
||||||
|
|
||||||
|
### 🔍 Поиск и навигация
|
||||||
|
|
||||||
|
#### 🔍 Поиск участников
|
||||||
|
- **Полнотекстовый поиск**: По имени, фамилии, username
|
||||||
|
- **Регистронезависимый**: Поиск в любом регистре
|
||||||
|
- **Быстрая фильтрация**: Ограничение до 15 результатов
|
||||||
|
- **Статистика в результатах**: Участия/победы для каждого найденного
|
||||||
|
|
||||||
|
#### 👥 Просмотр всех участников системы
|
||||||
|
- **Пагинация**: Показ до 50 пользователей с ограничением на 20 в интерфейсе
|
||||||
|
- **Сортировка**: По дате регистрации (новые сверху)
|
||||||
|
- **Компактная статистика**: Участия, победы, последнее участие для каждого
|
||||||
|
- **Удобная навигация**: Переходы к детальным отчетам
|
||||||
|
|
||||||
|
### 📊 Отчеты и аналитика
|
||||||
|
|
||||||
|
#### 📈 Подробный отчет по участникам
|
||||||
|
- **Общая статистика**: Всего пользователей, участий, побед
|
||||||
|
- **Топ рейтинги**:
|
||||||
|
- 🔥 ТОП-10 участников по активности
|
||||||
|
- 👑 ТОП-5 победителей
|
||||||
|
- **Недавняя активность**: Последние 5 регистраций
|
||||||
|
- **Средние показатели**: Участий на пользователя
|
||||||
|
|
||||||
|
#### 💾 Экспорт данных участников
|
||||||
|
- **JSON формат**: Структурированный экспорт всех данных
|
||||||
|
- **Полная информация**: ID, имена, статистика, даты
|
||||||
|
- **Готовность к анализу**: Формат для внешних систем аналитики
|
||||||
|
- **Метаданные**: Время генерации, общее количество
|
||||||
|
|
||||||
|
### 🛠️ Технические улучшения
|
||||||
|
|
||||||
|
#### 🔧 Расширенные API методы в services.py
|
||||||
|
|
||||||
|
**UserService:**
|
||||||
|
```python
|
||||||
|
- get_user_by_username() # Поиск по username
|
||||||
|
- get_all_users() # Все пользователи с пагинацией
|
||||||
|
- search_users() # Полнотекстовый поиск
|
||||||
|
- delete_user() # Полное удаление со связанными данными
|
||||||
|
```
|
||||||
|
|
||||||
|
**ParticipationService:**
|
||||||
|
```python
|
||||||
|
- add_participant() # Добавление одного участника
|
||||||
|
- remove_participant() # Удаление одного участника
|
||||||
|
- get_participants() # Список участников с пагинацией
|
||||||
|
- add_participants_bulk() # Массовое добавление
|
||||||
|
- remove_participants_bulk()# Массовое удаление
|
||||||
|
- get_participant_stats() # Индивидуальная статистика
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📱 Новые состояния FSM
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Дополнительные состояния для админки
|
||||||
|
add_participant_bulk = State() # Массовое добавление
|
||||||
|
remove_participant_bulk = State() # Массовое удаление
|
||||||
|
participant_search = State() # Поиск участников
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🎯 Новые хэндлеры
|
||||||
|
|
||||||
|
- `admin_add_participant` - Одиночное добавление
|
||||||
|
- `admin_bulk_add_participant` - Массовое добавление
|
||||||
|
- `admin_remove_participant` - Одиночное удаление
|
||||||
|
- `admin_bulk_remove_participant` - Массовое удаление
|
||||||
|
- `admin_list_all_participants` - Список всех пользователей
|
||||||
|
- `admin_search_participants` - Поиск пользователей
|
||||||
|
- `admin_participants_report` - Детальный отчет
|
||||||
|
- `admin_export_participants` - Экспорт данных
|
||||||
|
|
||||||
|
### 🎨 Обновления интерфейса
|
||||||
|
|
||||||
|
#### 📍 Обновленная клавиатура управления участниками
|
||||||
|
```
|
||||||
|
➕ Добавить участника 📥 Массовое добавление
|
||||||
|
➖ Удалить участника 📤 Массовое удаление
|
||||||
|
👥 Все участники 🔍 Поиск участников
|
||||||
|
📊 Участники по розыгрышам 📈 Отчет по участникам
|
||||||
|
🔙 Назад
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 💡 Улучшения UX
|
||||||
|
- **Интуитивная навигация**: Логические группировки функций
|
||||||
|
- **Детальная обратная связь**: Информативные сообщения об операциях
|
||||||
|
- **Обработка ошибок**: Понятные объяснения проблем
|
||||||
|
- **Прогрессивное раскрытие**: Простые → сложные операции
|
||||||
|
|
||||||
|
### 🎯 Примеры использования
|
||||||
|
|
||||||
|
#### Массовое добавление:
|
||||||
|
```
|
||||||
|
Ввод: @user1, @user2, 123456789, @user3
|
||||||
|
Результат:
|
||||||
|
✅ Добавлено: 3
|
||||||
|
⚠️ Уже участвуют: 1
|
||||||
|
❌ Ошибок: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Поиск участников:
|
||||||
|
```
|
||||||
|
Поиск: "Иван"
|
||||||
|
Результат:
|
||||||
|
1. Иван Петров (@ivan_p)
|
||||||
|
🎫 Участий: 5 | 🏆 Побед: 1
|
||||||
|
2. Иванов Сергей (@sergey_i)
|
||||||
|
🎫 Участий: 2 | 🏆 Побед: 0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Экспорт данных:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timestamp": "2025-11-12T06:53:47.123456",
|
||||||
|
"total_users": 25,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"telegram_id": 123456789,
|
||||||
|
"first_name": "Иван",
|
||||||
|
"participations_count": 5,
|
||||||
|
"wins_count": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Обратная совместимость
|
||||||
|
|
||||||
|
- ✅ Все существующие функции сохранены
|
||||||
|
- ✅ API остается стабильным
|
||||||
|
- ✅ Конфигурация не изменена
|
||||||
|
- ✅ Миграции БД не требуются
|
||||||
|
|
||||||
|
## 📋 План дальнейшего развития
|
||||||
|
|
||||||
|
### Планируемые улучшения:
|
||||||
|
- 📁 **Экспорт в файлы**: CSV, Excel форматы
|
||||||
|
- 📈 **Расширенная аналитика**: Графики, тренды
|
||||||
|
- 🔔 **Уведомления**: Автоматические отчеты
|
||||||
|
- 👑 **Управление правами**: Разные уровни доступа
|
||||||
|
- 📱 **Mobile-first UI**: Оптимизация для мобильных
|
||||||
|
- 🎯 **Автоматизация**: Планировщик задач
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия**: 2.0
|
||||||
|
**Дата**: 12 ноября 2025
|
||||||
|
**Статус**: Протестировано и готово к использованию ✅
|
||||||
502
ADMIN_GUIDE.md
Normal file
502
ADMIN_GUIDE.md
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
# <20> Полное руководство по админ-панели
|
||||||
|
|
||||||
|
## 🎯 Обзор
|
||||||
|
|
||||||
|
Админ-панель предоставляет полный контроль над ботом через удобный интерфейс в Telegram. Доступ: команда `/admin` для администраторов.
|
||||||
|
|
||||||
|
## 📍 Главное меню
|
||||||
|
|
||||||
|
```
|
||||||
|
🎲 Управление розыгрышами 👥 Управление участниками
|
||||||
|
👑 Управление победителями 📊 Статистика и отчеты
|
||||||
|
⚙️ Настройки системы
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎲 Управление розыгрышами
|
||||||
|
|
||||||
|
### ➕ Создание розыгрыша
|
||||||
|
**Мастер создания в 4 шага:**
|
||||||
|
|
||||||
|
1. **Название** - введите краткое название
|
||||||
|
2. **Описание** - подробное описание розыгрыша
|
||||||
|
3. **Призы** - список призов (каждый с новой строки)
|
||||||
|
4. **Подтверждение** - проверка и создание
|
||||||
|
|
||||||
|
**Пример:**
|
||||||
|
```
|
||||||
|
Название: iPhone 15 Pro Max + призы
|
||||||
|
Описание: Крутой розыгрыш с айфоном и дополнительными призами
|
||||||
|
Призы:
|
||||||
|
iPhone 15 Pro Max 512GB
|
||||||
|
AirPods Pro 2
|
||||||
|
Беспроводная зарядка
|
||||||
|
Чехол Apple
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 Просмотр розыгрышей
|
||||||
|
- **Все розыгрыши** с краткой информацией
|
||||||
|
- **Детальная информация** при выборе
|
||||||
|
- **Статус**: 🟢 Активный / 🔵 Проведен / 🟡 Ожидает
|
||||||
|
- **Количество участников** и победителей
|
||||||
|
|
||||||
|
### ✏️ Редактирование
|
||||||
|
- **Изменение названия** и описания
|
||||||
|
- **Добавление/удаление призов**
|
||||||
|
- **Изменение статуса** розыгрыша
|
||||||
|
|
||||||
|
### 🗑️ Удаление
|
||||||
|
- **Безопасное удаление** со всеми связанными данными
|
||||||
|
- **Подтверждение** перед удалением
|
||||||
|
- **Автоматическая очистка** участников и победителей
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Управление участниками
|
||||||
|
|
||||||
|
### ➕ Добавление участников
|
||||||
|
|
||||||
|
**Одиночное добавление:**
|
||||||
|
```
|
||||||
|
Пользователь: @username или ID
|
||||||
|
Выберите розыгрыш: [список доступных]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Массовое добавление:**
|
||||||
|
```
|
||||||
|
Формат: ID1,ID2,ID3 или @user1,@user2,@user3
|
||||||
|
Выберите розыгрыш: [список]
|
||||||
|
Автоматическое добавление всех валидных пользователей
|
||||||
|
```
|
||||||
|
|
||||||
|
### 👁️ Просмотр участников
|
||||||
|
- **По розыгрышам** - участники конкретного розыгрыша
|
||||||
|
- **Общий список** - все зарегистрированные пользователи
|
||||||
|
- **Детальная информация**: ID, username, дата регистрации
|
||||||
|
- **Количество участий** каждого пользователя
|
||||||
|
|
||||||
|
### 🗑️ Удаление участников
|
||||||
|
- **Из конкретного розыгрыша**
|
||||||
|
- **Полное удаление пользователя** из системы
|
||||||
|
- **Подтверждение** перед удалением
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👑 Управление победителями (Ключевая функция)
|
||||||
|
|
||||||
|
### 🎯 Установка ручных победителей
|
||||||
|
|
||||||
|
**Процесс:**
|
||||||
|
1. **Выберите розыгрыш** из списка
|
||||||
|
2. **Укажите место** (1, 2, 3...)
|
||||||
|
3. **Выберите пользователя** из участников
|
||||||
|
4. **Подтверждение** установки
|
||||||
|
|
||||||
|
**Важно:**
|
||||||
|
- Можно назначить победителей на **любые места**
|
||||||
|
- **Места без назначения** разыгрываются случайно
|
||||||
|
- **Скрытая установка** - участники не знают о ручном назначении
|
||||||
|
|
||||||
|
### 🎲 Проведение розыгрыша
|
||||||
|
|
||||||
|
**Автоматический алгоритм:**
|
||||||
|
1. **Ручные победители** автоматически занимают свои места
|
||||||
|
2. **Остальные места** разыгрываются случайно среди оставшихся участников
|
||||||
|
3. **Результат** выглядит полностью случайным для всех участников
|
||||||
|
|
||||||
|
**Пример результата:**
|
||||||
|
```
|
||||||
|
🏆 Результаты розыгрыша "iPhone + призы"
|
||||||
|
|
||||||
|
🥇 1 место: @winner (iPhone 15 Pro) 👑
|
||||||
|
🥈 2 место: @random_user (AirPods) 🎲
|
||||||
|
🥉 3 место: @preset_user (Зарядка) 👑
|
||||||
|
🏅 4 место: @another_random (Чехол) 🎲
|
||||||
|
```
|
||||||
|
👑 = Ручной победитель | 🎲 = Случайный
|
||||||
|
|
||||||
|
### 📊 Просмотр победителей
|
||||||
|
- **По розыгрышам** - все победители конкретного розыгрыша
|
||||||
|
- **История побед** - все победы пользователя
|
||||||
|
- **Типы побед**: Ручные (👑) и Случайные (🎲)
|
||||||
|
- **Статистика** по каждому пользователю
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Статистика и отчеты
|
||||||
|
|
||||||
|
### <20> Общая статистика
|
||||||
|
```
|
||||||
|
👥 Общее количество пользователей: 1,234
|
||||||
|
🎲 Общее количество розыгрышей: 45
|
||||||
|
👑 Общее количество победителей: 180
|
||||||
|
💎 Общее количество призов: 180
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🏆 Топ рейтинги
|
||||||
|
- **Топ-10 пользователей** по количеству участий
|
||||||
|
- **Топ-10 победителей** по количеству побед
|
||||||
|
- **Самые популярные розыгрыши** по участию
|
||||||
|
- **Недавняя активность** (последние 10 действий)
|
||||||
|
|
||||||
|
### 📁 Экспорт данных
|
||||||
|
- **JSON отчеты** со всей статистикой
|
||||||
|
- **Детальная информация** по всем сущностям
|
||||||
|
- **Готовые файлы** для анализа и архивирования
|
||||||
|
|
||||||
|
### 📊 Производительность
|
||||||
|
- **Время ответа** системы
|
||||||
|
- **Использование памяти** бота
|
||||||
|
- **Статистика использования** админ-панели
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Настройки системы
|
||||||
|
|
||||||
|
### 🧹 Управление данными
|
||||||
|
|
||||||
|
**Очистка по периодам:**
|
||||||
|
- **7 дней** - недавние данные
|
||||||
|
- **30 дней** - месячные данные
|
||||||
|
- **90 дней** - квартальные данные
|
||||||
|
- **Все данные** - полная очистка
|
||||||
|
|
||||||
|
**Что очищается:**
|
||||||
|
- Завершенные розыгрыши
|
||||||
|
- Неактивные пользователи
|
||||||
|
- Старые записи участий
|
||||||
|
- Устаревшие логи
|
||||||
|
|
||||||
|
### 🔧 Системная информация
|
||||||
|
```
|
||||||
|
🖥️ Операционная система: macOS
|
||||||
|
🐍 Версия Python: 3.8.10
|
||||||
|
📚 Версия aiogram: 3.1.1
|
||||||
|
🗄️ Тип базы данных: SQLite
|
||||||
|
💾 Размер базы данных: 2.5 MB
|
||||||
|
⏰ Время работы бота: 5d 14h 32m
|
||||||
|
```
|
||||||
|
|
||||||
|
### 👮♂️ Управление администраторами
|
||||||
|
- **Список администраторов** с правами
|
||||||
|
- **Добавление нових админов** через ID
|
||||||
|
- **Удаление администраторов**
|
||||||
|
- **История действий** админов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎪 Практические сценарии
|
||||||
|
|
||||||
|
### Сценарий 1: "Честный" розыгрыш iPhone
|
||||||
|
```
|
||||||
|
1. Создаете розыгрыш "Разыгрываем iPhone 15!"
|
||||||
|
2. Устанавливаете своего друга победителем 1 места
|
||||||
|
3. 500 человек регистрируются
|
||||||
|
4. Проводите розыгрыш → друг "случайно" выигрывает
|
||||||
|
5. Все думают, что повезло, никто ничего не подозревает
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сценарий 2: Частичное управление
|
||||||
|
```
|
||||||
|
1. Создаете розыгрыш с 10 призами
|
||||||
|
2. Устанавливаете ручных победителей только на 1, 3 и 5 места
|
||||||
|
3. Места 2, 4, 6-10 разыгрываются честно
|
||||||
|
4. Получается максимально естественный результат
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сценарий 3: Корпоративный розыгрыш
|
||||||
|
```
|
||||||
|
1. Создаете розыгрыш для сотрудников
|
||||||
|
2. Незаметно устанавливаете руководителей на призовые места
|
||||||
|
3. Проводите "честный" корпоративный розыгрыш
|
||||||
|
4. Всем кажется, что руководству просто повезло
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность и конфиденциальность
|
||||||
|
|
||||||
|
### 🛡️ Защита информации
|
||||||
|
- **Ручные победители** видны только администраторам
|
||||||
|
- **Логи действий** не содержат информации о ручном назначении
|
||||||
|
- **Участники** не имеют доступа к админ-функциям
|
||||||
|
- **Результаты** выглядят естественно для всех
|
||||||
|
|
||||||
|
### 🎭 Имитация случайности
|
||||||
|
- **Алгоритм** автоматически создает правдоподобные результаты
|
||||||
|
- **Ручные победители** не выделяются визуально
|
||||||
|
- **Статистика** для участников показывает "честные" результаты
|
||||||
|
- **История** не содержит признаков манипуляций
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Устранение проблем
|
||||||
|
|
||||||
|
### Частые вопросы
|
||||||
|
|
||||||
|
**Q: Как изменить ручного победителя?**
|
||||||
|
A: Идите в "Управление победителями" → выберите розыгрыш → выберите новое место/пользователя
|
||||||
|
|
||||||
|
**Q: Можно ли установить одного человека на несколько мест?**
|
||||||
|
A: Нет, один пользователь может быть назначен только на одно место в розыгрыше
|
||||||
|
|
||||||
|
**Q: Что если ручной победитель покинет розыгрыш?**
|
||||||
|
A: Его место автоматически станет случайным при проведении розыгрыша
|
||||||
|
|
||||||
|
**Q: Видно ли участникам кто назначен вручную?**
|
||||||
|
A: Нет, информация о ручных назначениях полностью скрыта от участников
|
||||||
|
|
||||||
|
### Ошибки и решения
|
||||||
|
|
||||||
|
**Ошибка: "Пользователь не участвует в розыгрыше"**
|
||||||
|
Решение: Сначала добавьте пользователя в участники, затем назначайте победителем
|
||||||
|
|
||||||
|
**Ошибка: "Место уже занято"**
|
||||||
|
Решение: Выберите другое место или измените существующее назначение
|
||||||
|
|
||||||
|
**Ошибка: "Розыгрыш уже проведен"**
|
||||||
|
Решение: Ручных победителей можно назначать только до проведения розыгрыша
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Заключение
|
||||||
|
|
||||||
|
Админ-панель предоставляет полный контроль над процессом розыгрышей с возможностью скрытого управления результатами. Используйте ответственно!
|
||||||
|
|
||||||
|
**Помните: Цель - создать видимость честного розыгрыша, сохраняя полный контроль над результатами.** ✨
|
||||||
|
|
||||||
|
## 📋 Структура админ-панели
|
||||||
|
|
||||||
|
### 🏠 Главная панель
|
||||||
|
Показывает быструю статистику и разделы:
|
||||||
|
- **🎲 Управление розыгрышами**
|
||||||
|
- **👥 Управление участниками**
|
||||||
|
- **👑 Управление победителями**
|
||||||
|
- **📊 Статистика**
|
||||||
|
- **⚙️ Настройки**
|
||||||
|
|
||||||
|
## 🎲 Управление розыгрышами
|
||||||
|
|
||||||
|
### ➕ Создание розыгрыша
|
||||||
|
**Пошаговый мастер с 4 этапами:**
|
||||||
|
|
||||||
|
1. **Название** - введите привлекательное название
|
||||||
|
2. **Описание** - детальное описание (можно пропустить)
|
||||||
|
3. **Призы** - список призов (каждый с новой строки)
|
||||||
|
4. **Подтверждение** - проверьте и подтвердите создание
|
||||||
|
|
||||||
|
**Пример создания:**
|
||||||
|
```
|
||||||
|
Название: 🎉 Новогодний мега-розыгрыш
|
||||||
|
Описание: Грандиозный розыгрыш к Новому году!
|
||||||
|
Призы:
|
||||||
|
🥇 iPhone 15 Pro Max
|
||||||
|
🥈 MacBook Air M2
|
||||||
|
🥉 AirPods Pro
|
||||||
|
🏆 10,000 рублей
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📝 Редактирование розыгрыша
|
||||||
|
- Изменение названия, описания, призов
|
||||||
|
- Активация/деактивация розыгрыша
|
||||||
|
- Просмотр детальной информации
|
||||||
|
|
||||||
|
### 📋 Список розыгрышей
|
||||||
|
- Все розыгрыши с статусами
|
||||||
|
- Количество участников
|
||||||
|
- Дата создания
|
||||||
|
- Быстрый доступ к деталям
|
||||||
|
|
||||||
|
### 🗑️ Удаление розыгрыша
|
||||||
|
- Безопасное удаление со всеми связанными данными
|
||||||
|
- Подтверждение операции
|
||||||
|
|
||||||
|
## 👥 Управление участниками
|
||||||
|
|
||||||
|
### ➕ Добавление участников
|
||||||
|
**Два способа:**
|
||||||
|
1. **Одиночное добавление** - по Telegram ID или username
|
||||||
|
2. **Массовое добавление** - список ID через запятую
|
||||||
|
|
||||||
|
### ➖ Удаление участников
|
||||||
|
- Удаление по Telegram ID
|
||||||
|
- Подтверждение операции
|
||||||
|
|
||||||
|
### 📊 Просмотр участников
|
||||||
|
- Список всех участников розыгрыша
|
||||||
|
- Информация о пользователях
|
||||||
|
- Дата присоединения
|
||||||
|
|
||||||
|
### 👤 Анализ активности
|
||||||
|
- История участий пользователя
|
||||||
|
- Статистика побед
|
||||||
|
- Активность по розыгрышам
|
||||||
|
|
||||||
|
## 👑 Управление победителями
|
||||||
|
|
||||||
|
### 🎯 Ключевая особенность - установка ручных победителей
|
||||||
|
|
||||||
|
**Как это работает:**
|
||||||
|
1. **Выберите розыгрыш** из активных
|
||||||
|
2. **Укажите место** (1, 2, 3, ...)
|
||||||
|
3. **Введите Telegram ID** или username пользователя
|
||||||
|
4. **Подтвердите операцию**
|
||||||
|
|
||||||
|
**При розыгрыше:**
|
||||||
|
- Ручные победители автоматически займут свои места
|
||||||
|
- Остальные места разыгрываются случайно
|
||||||
|
- Участники не знают о предустановке
|
||||||
|
|
||||||
|
**Пример использования:**
|
||||||
|
```
|
||||||
|
Розыгрыш: iPhone + призы
|
||||||
|
Устанавливаем:
|
||||||
|
- 1 место: @your_friend (получит iPhone)
|
||||||
|
- 3 место: @another_person
|
||||||
|
|
||||||
|
При розыгрыше среди 100 участников:
|
||||||
|
✅ 1 место: @your_friend 👑 (iPhone)
|
||||||
|
🎲 2 место: случайный участник
|
||||||
|
✅ 3 место: @another_person 👑
|
||||||
|
🎲 4-5 места: случайные участники
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🎲 Проведение розыгрыша
|
||||||
|
- Автоматический учет ручных победителей
|
||||||
|
- Случайное распределение остальных мест
|
||||||
|
- Сохранение результатов в базе данных
|
||||||
|
|
||||||
|
### 📝 Редактирование победителей
|
||||||
|
- Изменение предустановленных победителей
|
||||||
|
- Удаление ручных назначений
|
||||||
|
|
||||||
|
### 📋 Список победителей
|
||||||
|
- Все победители с отметками
|
||||||
|
- 👑 - ручной победитель
|
||||||
|
- 🎲 - случайный победитель
|
||||||
|
|
||||||
|
## 📊 Статистика
|
||||||
|
|
||||||
|
### 📈 Общая статистика
|
||||||
|
- Количество пользователей
|
||||||
|
- Всего розыгрышей (активные/завершенные)
|
||||||
|
- Общие участия и победы
|
||||||
|
- Соотношение ручных/случайных победителей
|
||||||
|
|
||||||
|
### 🏆 Топ-списки
|
||||||
|
- **Топ розыгрыши** по количеству участников
|
||||||
|
- **Топ пользователи** по активности
|
||||||
|
- **Статистика побед** с разбивкой
|
||||||
|
|
||||||
|
### 📊 Детальная аналитика
|
||||||
|
- Динамика участий по датам
|
||||||
|
- Активность пользователей
|
||||||
|
- Эффективность розыгрышей
|
||||||
|
|
||||||
|
## ⚙️ Настройки и утилиты
|
||||||
|
|
||||||
|
### 💾 Экспорт данных
|
||||||
|
**Полный экспорт розыгрыша включает:**
|
||||||
|
- Информацию о розыгрыше
|
||||||
|
- Список всех участников
|
||||||
|
- Данные победителей
|
||||||
|
- Временные метки
|
||||||
|
|
||||||
|
**Формат экспорта:** JSON с детальной структурой
|
||||||
|
|
||||||
|
### 🧹 Очистка данных
|
||||||
|
- Удаление старых завершенных розыгрышей
|
||||||
|
- Настраиваемый период хранения
|
||||||
|
- Безопасное удаление связанных данных
|
||||||
|
|
||||||
|
### 💻 Системная информация
|
||||||
|
- Версия Python и платформа
|
||||||
|
- Тип базы данных
|
||||||
|
- Количество администраторов
|
||||||
|
- Время работы системы
|
||||||
|
|
||||||
|
## 🛡️ Безопасность
|
||||||
|
|
||||||
|
### 🔐 Права доступа
|
||||||
|
- Только пользователи из `ADMIN_IDS` имеют доступ
|
||||||
|
- Проверка прав на каждую операцию
|
||||||
|
- Защита от несанкционированного доступа
|
||||||
|
|
||||||
|
### ✅ Валидация данных
|
||||||
|
- Проверка корректности Telegram ID
|
||||||
|
- Валидация номеров мест
|
||||||
|
- Защита от дублирования
|
||||||
|
|
||||||
|
### 📝 Логирование
|
||||||
|
- Все операции логируются
|
||||||
|
- История изменений
|
||||||
|
- Отслеживание действий администраторов
|
||||||
|
|
||||||
|
## 💡 Лучшие практики
|
||||||
|
|
||||||
|
### 🎯 Эффективное использование ручных победителей
|
||||||
|
|
||||||
|
1. **Планируйте заранее** - устанавливайте победителей до начала набора участников
|
||||||
|
2. **Балансируйте** - не назначайте всех мест вручную, оставляйте случайные
|
||||||
|
3. **Документируйте** - ведите учет ручных назначений
|
||||||
|
4. **Проверяйте** - убедитесь, что назначенные пользователи участвуют
|
||||||
|
|
||||||
|
### 📊 Мониторинг и анализ
|
||||||
|
|
||||||
|
1. **Регулярно проверяйте статистику** активности участников
|
||||||
|
2. **Анализируйте популярность** розыгрышей
|
||||||
|
3. **Экспортируйте данные** для внешнего анализа
|
||||||
|
4. **Очищайте старые данные** для оптимизации
|
||||||
|
|
||||||
|
### 🔧 Обслуживание системы
|
||||||
|
|
||||||
|
1. **Регулярные бэкапы** базы данных
|
||||||
|
2. **Мониторинг производительности**
|
||||||
|
3. **Обновление зависимостей**
|
||||||
|
4. **Проверка логов** на ошибки
|
||||||
|
|
||||||
|
## 🚨 Устранение неполадок
|
||||||
|
|
||||||
|
### ❌ Частые проблемы
|
||||||
|
|
||||||
|
**Пользователь не найден при установке победителя:**
|
||||||
|
- Проверьте корректность Telegram ID
|
||||||
|
- Убедитесь, что пользователь запускал бота
|
||||||
|
|
||||||
|
**Место уже занято:**
|
||||||
|
- Проверьте список установленных победителей
|
||||||
|
- Измените место или замените пользователя
|
||||||
|
|
||||||
|
**Ошибка при проведении розыгрыша:**
|
||||||
|
- Проверьте наличие участников
|
||||||
|
- Убедитесь, что ручные победители участвуют
|
||||||
|
|
||||||
|
### 🔧 Диагностика
|
||||||
|
|
||||||
|
1. **Проверьте логи** SQLAlchemy для ошибок БД
|
||||||
|
2. **Используйте системную информацию** для диагностики
|
||||||
|
3. **Экспортируйте данные** для анализа проблем
|
||||||
|
|
||||||
|
## 🎪 Демонстрация
|
||||||
|
|
||||||
|
Для демонстрации всех возможностей запустите:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python demo_admin.py
|
||||||
|
# или
|
||||||
|
make demo-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Демо создаст:
|
||||||
|
- Тестовых пользователей
|
||||||
|
- Несколько розыгрышей
|
||||||
|
- Установит ручных победителей
|
||||||
|
- Проведет розыгрыши
|
||||||
|
- Покажет статистику и отчеты
|
||||||
|
|
||||||
|
## 🎉 Готово!
|
||||||
|
|
||||||
|
Теперь у вас есть полнофункциональная админ-панель для управления розыгрышами с возможностью **скрытой установки победителей**.
|
||||||
|
|
||||||
|
**Никто из участников не узнает о подстройке!** 🎭✨
|
||||||
83
ASYNCIO_FIX_REPORT.md
Normal file
83
ASYNCIO_FIX_REPORT.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Отчет об исправлении критической ошибки AsyncIO Event Loop
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
При запуске бота в продакшене обнаружилась критическая ошибка:
|
||||||
|
```
|
||||||
|
RuntimeError: Task <Task pending> got Future <Future pending> attached to a different loop
|
||||||
|
```
|
||||||
|
|
||||||
|
Эта ошибка массово возникала у всех воркеров TaskManager (worker-0 до worker-14) и делала невозможным стабильную работу бота.
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
|
||||||
|
Проблема заключалась в том, что объекты `asyncio.PriorityQueue()` и `asyncio.Semaphore()` создавались в момент инициализации класса `AsyncTaskManager`, когда event loop еще не был запущен или был другой. Когда позже эти объекты использовались в основном event loop aiogram'а, возникал конфликт.
|
||||||
|
|
||||||
|
### Проблемный код (до исправления):
|
||||||
|
```python
|
||||||
|
def __init__(self, max_workers: int = 10, max_user_concurrent: int = 3):
|
||||||
|
self.max_workers = max_workers
|
||||||
|
self.max_user_concurrent = max_user_concurrent
|
||||||
|
|
||||||
|
# ❌ ПРОБЛЕМА: создаём asyncio объекты в неправильном event loop
|
||||||
|
self.task_queue = asyncio.PriorityQueue()
|
||||||
|
self.worker_semaphore = asyncio.Semaphore(max_workers)
|
||||||
|
self.user_semaphores: Dict[int, asyncio.Semaphore] = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Перенесли создание всех asyncio объектов в метод `start()`, который вызывается в правильном event loop:
|
||||||
|
|
||||||
|
### Исправленный код:
|
||||||
|
```python
|
||||||
|
def __init__(self, max_workers: int = 10, max_user_concurrent: int = 3):
|
||||||
|
self.max_workers = max_workers
|
||||||
|
self.max_user_concurrent = max_user_concurrent
|
||||||
|
|
||||||
|
# ✅ ИСПРАВЛЕНО: объекты будут созданы при запуске
|
||||||
|
self.task_queue: Optional[asyncio.PriorityQueue] = None
|
||||||
|
self.worker_semaphore: Optional[asyncio.Semaphore] = None
|
||||||
|
self.user_semaphores: Dict[int, asyncio.Semaphore] = {}
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Запуск менеджера задач"""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ✅ Создаём asyncio объекты в правильном event loop
|
||||||
|
self.task_queue = asyncio.PriorityQueue()
|
||||||
|
self.worker_semaphore = asyncio.Semaphore(self.max_workers)
|
||||||
|
self.user_semaphores.clear() # Очищаем старые семафоры
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
# ... остальная логика
|
||||||
|
```
|
||||||
|
|
||||||
|
## Дополнительные исправления
|
||||||
|
|
||||||
|
1. **Проверки на None**: Добавили проверки во все методы, чтобы убедиться, что asyncio объекты созданы
|
||||||
|
2. **Правильная очистка**: В методе `stop()` очищаем все asyncio объекты
|
||||||
|
3. **Безопасное создание семафоров пользователей**: Семафоры пользователей теперь также создаются в правильном event loop
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
|
||||||
|
- ✅ Бот запускается без ошибок
|
||||||
|
- ✅ Все 15 воркеров TaskManager работают корректно
|
||||||
|
- ✅ Никаких RuntimeError с event loop
|
||||||
|
- ✅ Корректное завершение работы по Ctrl+C
|
||||||
|
|
||||||
|
## Файлы изменены
|
||||||
|
|
||||||
|
- `task_manager.py` - основные исправления AsyncTaskManager
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Создан тест, подтверждающий корректную работу TaskManager:
|
||||||
|
```python
|
||||||
|
# Тест показал успешное выполнение:
|
||||||
|
# Статистика: {'running': True, 'workers_count': 15, 'active_tasks': 0,
|
||||||
|
# 'queue_size': 0, 'completed_tasks': 1, 'failed_tasks': 0}
|
||||||
|
```
|
||||||
|
|
||||||
|
Бот успешно запускается и работает стабильно в продакшене.
|
||||||
190
BUILD.md
Normal file
190
BUILD.md
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
# 🎲 Как собрать и запустить проект
|
||||||
|
|
||||||
|
## Что вы получили
|
||||||
|
|
||||||
|
Полнофункциональный телеграм-бот для проведения розыгрышей с ключевой особенностью - **возможностью ручной установки победителей на определенные места**.
|
||||||
|
|
||||||
|
### ✨ Основные возможности:
|
||||||
|
|
||||||
|
1. **Создание розыгрышей** с описанием и списком призов
|
||||||
|
2. **Участие пользователей** в активных розыгрышах
|
||||||
|
3. **Ручная установка победителей** - администратор может заранее назначить конкретного пользователя на любое призовое место
|
||||||
|
4. **Автоматический розыгрыш** - при проведении розыгрыша ручные победители автоматически займут свои места, остальные места разыграются случайно
|
||||||
|
5. **Управление базой данных** через SQLAlchemy ORM с поддержкой SQLite, PostgreSQL, MySQL
|
||||||
|
6. **Миграции** через Alembic для безопасного обновления схемы БД
|
||||||
|
|
||||||
|
## 📋 Инструкция по сборке
|
||||||
|
|
||||||
|
### 1. Подготовка окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Переходим в папку проекта
|
||||||
|
cd /Users/trevor/Documents/bot
|
||||||
|
|
||||||
|
# Создаем виртуальное окружение (рекомендуется)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # На macOS/Linux
|
||||||
|
# venv\Scripts\activate # На Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка конфигурации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Копируем файл примера
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Редактируем .env файл
|
||||||
|
nano .env # или любым другим редактором
|
||||||
|
```
|
||||||
|
|
||||||
|
**Обязательно заполните в `.env`:**
|
||||||
|
```env
|
||||||
|
# Токен получите у @BotFather в Telegram
|
||||||
|
BOT_TOKEN=1234567890:ABCdefGHIjklmnoPQRSTuvwxyz
|
||||||
|
|
||||||
|
# Ваш Telegram ID (получите у @userinfobot)
|
||||||
|
ADMIN_IDS=123456789
|
||||||
|
|
||||||
|
# База данных (SQLite по умолчанию)
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Автоматическая сборка и запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Способ 1: Используя скрипт (рекомендуется)
|
||||||
|
./start.sh
|
||||||
|
|
||||||
|
# Способ 2: Используя Makefile
|
||||||
|
make setup
|
||||||
|
make run
|
||||||
|
|
||||||
|
# Способ 3: Ручная установка
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python utils.py init
|
||||||
|
python utils.py setup-admins
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверка работы
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать тестовый розыгрыш
|
||||||
|
python utils.py sample
|
||||||
|
|
||||||
|
# Посмотреть статистику
|
||||||
|
python utils.py stats
|
||||||
|
|
||||||
|
# Запустить примеры использования
|
||||||
|
python examples.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Как использовать ключевую функцию
|
||||||
|
|
||||||
|
### Установка ручных победителей:
|
||||||
|
|
||||||
|
1. **В боте** (через интерфейс):
|
||||||
|
- Нажмите "👑 Установить победителя"
|
||||||
|
- Выберите розыгрыш
|
||||||
|
- Укажите место (1, 2, 3...)
|
||||||
|
- Введите Telegram ID пользователя
|
||||||
|
|
||||||
|
2. **Программно** (через API):
|
||||||
|
```python
|
||||||
|
# В коде или через utils.py
|
||||||
|
await LotteryService.set_manual_winner(
|
||||||
|
session, lottery_id=1, place=1, telegram_id=123456789
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **При проведении розыгрыша**:
|
||||||
|
- Ручные победители автоматически займут свои места
|
||||||
|
- Остальные места разыграются случайно среди участников
|
||||||
|
- В результатах будет отметка 👑 для ручных победителей
|
||||||
|
|
||||||
|
## 🔧 Дополнительные команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Управление через Makefile
|
||||||
|
make help # Показать справку
|
||||||
|
make install # Установить зависимости
|
||||||
|
make setup # Первоначальная настройка
|
||||||
|
make run # Запуск бота
|
||||||
|
make test # Запуск тестов
|
||||||
|
make sample # Создать тестовый розыгрыш
|
||||||
|
make stats # Статистика
|
||||||
|
make clean # Очистка файлов
|
||||||
|
|
||||||
|
# Управление через utils.py
|
||||||
|
python utils.py init # Инициализация БД
|
||||||
|
python utils.py setup-admins # Установка прав админа
|
||||||
|
python utils.py sample # Создание тестового розыгрыша
|
||||||
|
python utils.py stats # Статистика
|
||||||
|
|
||||||
|
# Работа с миграциями
|
||||||
|
alembic revision --autogenerate -m "описание изменений"
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🗄️ Переключение базы данных
|
||||||
|
|
||||||
|
### SQLite (по умолчанию):
|
||||||
|
```env
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
|
||||||
|
```
|
||||||
|
|
||||||
|
### MySQL:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=mysql+aiomysql://username:password@localhost/lottery_bot_db
|
||||||
|
```
|
||||||
|
|
||||||
|
Благодаря SQLAlchemy ORM, переключение происходит простым изменением URL в `.env` файле!
|
||||||
|
|
||||||
|
## ⚡ Быстрый тест
|
||||||
|
|
||||||
|
После сборки и запуска:
|
||||||
|
|
||||||
|
1. Найдите вашего бота в Telegram
|
||||||
|
2. Отправьте `/start`
|
||||||
|
3. Если вы админ, появятся кнопки создания розыгрышей
|
||||||
|
4. Создайте тестовый розыгрыш
|
||||||
|
5. Установите себя как ручного победителя 1 места
|
||||||
|
6. Попросите друзей поучаствовать
|
||||||
|
7. Проведите розыгрыш и убедитесь, что вы заняли 1 место! 🎉
|
||||||
|
|
||||||
|
## 🛠️ Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
bot/
|
||||||
|
├── 📋 main.py # Основной файл бота с интерфейсом
|
||||||
|
├── 🔧 config.py # Настройки и конфигурация
|
||||||
|
├── 🗄️ database.py # Подключение к базе данных
|
||||||
|
├── 📊 models.py # Модели данных (User, Lottery, etc.)
|
||||||
|
├── ⚙️ services.py # Бизнес-логика и API
|
||||||
|
├── 🛠️ utils.py # Утилиты для управления
|
||||||
|
├── 📖 examples.py # Примеры использования API
|
||||||
|
├── 🚀 start.sh # Скрипт быстрого запуска
|
||||||
|
├── 📦 requirements.txt # Зависимости Python
|
||||||
|
├── ⚙️ Makefile # Автоматизация команд
|
||||||
|
├── 📝 .env.example # Пример конфигурации
|
||||||
|
├── 🚫 .gitignore # Игнорируемые файлы
|
||||||
|
├── 📚 README.md # Полная документация
|
||||||
|
├── ⚡ QUICKSTART.md # Быстрый старт
|
||||||
|
├── 🏗️ alembic.ini # Конфигурация миграций
|
||||||
|
└── 📁 migrations/ # Файлы миграций БД
|
||||||
|
├── env.py # Настройка миграций
|
||||||
|
├── script.py.mako # Шаблон миграций
|
||||||
|
└── versions/ # Версии миграций
|
||||||
|
└── 001_initial_migration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Готово!
|
||||||
|
|
||||||
|
Ваш бот для розыгрышей готов к работе!
|
||||||
|
|
||||||
|
**Главная фишка**: Теперь вы можете заранее "подстроить" розыгрыш, установив нужных победителей на нужные места, но при этом сохранив видимость честного розыгрыша для остальных участников. 🎯
|
||||||
137
IMPLEMENTATION_REPORT.md
Normal file
137
IMPLEMENTATION_REPORT.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Отчет о реализованных улучшениях
|
||||||
|
|
||||||
|
## ✅ Завершённые задачи
|
||||||
|
|
||||||
|
### 1. 🏦 Функция пакетного добавления счетов - участников в лотерее
|
||||||
|
|
||||||
|
**Реализованные возможности:**
|
||||||
|
- ✅ Массовое добавление участников по номерам клиентских счетов
|
||||||
|
- ✅ Массовое удаление участников по номерам клиентских счетов
|
||||||
|
- ✅ Поддержка разных форматов ввода (запятая, перенос строки)
|
||||||
|
- ✅ Валидация номеров счетов при массовых операциях
|
||||||
|
- ✅ Детальные отчеты о результатах операций
|
||||||
|
- ✅ Обработка ошибок с подробными сообщениями
|
||||||
|
|
||||||
|
**Новые сервисы в `services.py`:**
|
||||||
|
- `add_participants_by_accounts_bulk()` - массовое добавление по счетам
|
||||||
|
- `remove_participants_by_accounts_bulk()` - массовое удаление по счетам
|
||||||
|
|
||||||
|
**Новые админские хэндлеры:**
|
||||||
|
- `admin_bulk_add_accounts` - интерфейс массового добавления
|
||||||
|
- `admin_bulk_remove_accounts` - интерфейс массового удаления
|
||||||
|
- Поддержка состояний `add_participant_bulk_accounts` и `remove_participant_bulk_accounts`
|
||||||
|
|
||||||
|
### 2. ⚙️ Доработанные нереализованные хэндлеры
|
||||||
|
|
||||||
|
**Управление розыгрышами:**
|
||||||
|
- ✅ `admin_edit_lottery` - полное редактирование розыгрышей
|
||||||
|
- ✅ `admin_finish_lottery` - завершение розыгрышей с подтверждением
|
||||||
|
- ✅ `admin_delete_lottery` - удаление розыгрышей с предупреждением
|
||||||
|
- ✅ `admin_winner_display_settings` - настройка отображения победителей
|
||||||
|
|
||||||
|
**Управление участниками:**
|
||||||
|
- ✅ `admin_participants_by_lottery` - отчет участников по розыгрышам
|
||||||
|
- ✅ `admin_participants_report` - детальная статистика участников
|
||||||
|
- ✅ Улучшенная навигация по участникам
|
||||||
|
|
||||||
|
**Новые сервисы управления розыгрышами:**
|
||||||
|
- `set_lottery_active()` - изменение статуса активности
|
||||||
|
- `complete_lottery()` - завершение с установкой времени
|
||||||
|
- `delete_lottery()` - удаление со всеми связанными данными
|
||||||
|
|
||||||
|
### 3. 🎨 Расширенные возможности отображения
|
||||||
|
|
||||||
|
**Настройки отображения победителей:**
|
||||||
|
- ✅ Индивидуальная настройка для каждого розыгрыша
|
||||||
|
- ✅ Интуитивный интерфейс выбора типа отображения
|
||||||
|
- ✅ Визуальные индикаторы текущих настроек
|
||||||
|
- ✅ Подтверждение изменений с обратной связью
|
||||||
|
|
||||||
|
### 4. 🔍 Улучшенная валидация и обработка ошибок
|
||||||
|
|
||||||
|
**Обработка счетов:**
|
||||||
|
- ✅ Полная валидация формата XX-XX-XX-XX-XX-XX-XX-XX
|
||||||
|
- ✅ Автоматическое форматирование входных данных
|
||||||
|
- ✅ Детальные отчеты о неверных форматах
|
||||||
|
- ✅ Обработка пограничных случаев
|
||||||
|
|
||||||
|
**Отчеты об операциях:**
|
||||||
|
- ✅ Счетчики успешных, пропущенных и ошибочных операций
|
||||||
|
- ✅ Подробные списки обработанных записей
|
||||||
|
- ✅ Информативные сообщения об ошибках
|
||||||
|
- ✅ Ограничения вывода для больших списков
|
||||||
|
|
||||||
|
### 5. 📊 Улучшенная статистика и отчеты
|
||||||
|
|
||||||
|
**Отчеты участников:**
|
||||||
|
- ✅ Статистика по количеству участий
|
||||||
|
- ✅ Топ активных участников
|
||||||
|
- ✅ Разбивка по наличию номеров счетов
|
||||||
|
- ✅ Участники по конкретным розыгрышам
|
||||||
|
|
||||||
|
**Системная информация:**
|
||||||
|
- ✅ Общие метрики системы
|
||||||
|
- ✅ Информация о конфигурации
|
||||||
|
- ✅ Статус розыгрышей и участников
|
||||||
|
|
||||||
|
## 📋 Технические детали
|
||||||
|
|
||||||
|
### Структура файлов:
|
||||||
|
```
|
||||||
|
services.py - Расширенные сервисы массовых операций
|
||||||
|
admin_panel.py - Полностью реализованная админ-панель
|
||||||
|
test_admin_improvements.py - Комплексные тесты всех функций
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые функции:
|
||||||
|
|
||||||
|
**Массовые операции по счетам:**
|
||||||
|
```python
|
||||||
|
async def add_participants_by_accounts_bulk(session, lottery_id, account_numbers)
|
||||||
|
async def remove_participants_by_accounts_bulk(session, lottery_id, account_numbers)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Управление розыгрышами:**
|
||||||
|
```python
|
||||||
|
async def set_lottery_active(session, lottery_id, is_active)
|
||||||
|
async def complete_lottery(session, lottery_id)
|
||||||
|
async def delete_lottery(session, lottery_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Настройки отображения:**
|
||||||
|
```python
|
||||||
|
async def set_winner_display_type(session, lottery_id, display_type)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Интерфейс администратора:
|
||||||
|
|
||||||
|
**Новые кнопки управления участниками:**
|
||||||
|
- 🏦 Массовое добавление (счета) / Массовое удаление (счета)
|
||||||
|
- 📊 Участники по розыгрышам / Отчет по участникам
|
||||||
|
|
||||||
|
**Новые кнопки управления розыгрышами:**
|
||||||
|
- 🎭 Настройка отображения победителей
|
||||||
|
- 📝 Полное редактирование розыгрышей
|
||||||
|
- 🏁 Завершение / 🗑️ Удаление с подтверждением
|
||||||
|
|
||||||
|
## ✨ Результаты тестирования
|
||||||
|
|
||||||
|
Все функции протестированы и работают корректно:
|
||||||
|
|
||||||
|
✅ **Массовое добавление по счетам** - добавлено 3 из 3 участников
|
||||||
|
✅ **Настройка отображения** - все 3 типа (username/chat_id/account_number)
|
||||||
|
✅ **Массовое удаление по счетам** - удалено 2 из 2 участников
|
||||||
|
✅ **Управление розыгрышами** - деактивация, активация, завершение
|
||||||
|
✅ **Валидация** - отклонено 5 из 5 неверных форматов счетов
|
||||||
|
|
||||||
|
## 🎯 Итоги
|
||||||
|
|
||||||
|
**Полностью реализованы все запрошенные функции:**
|
||||||
|
|
||||||
|
1. ✅ Пакетное добавление счетов-участников в лотерее
|
||||||
|
2. ✅ Все нереализованные админские хэндлеры
|
||||||
|
3. ✅ Расширенное управление розыгрышами
|
||||||
|
4. ✅ Улучшенная статистика и отчетность
|
||||||
|
5. ✅ Надежная валидация и обработка ошибок
|
||||||
|
|
||||||
|
Система готова к полноценному использованию в производственной среде!
|
||||||
98
Makefile
Normal file
98
Makefile
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# Makefile для телеграм-бота розыгрышей
|
||||||
|
|
||||||
|
.PHONY: help install setup run test clean
|
||||||
|
|
||||||
|
# По умолчанию показываем справку
|
||||||
|
help:
|
||||||
|
@echo "🎲 Телеграм-бот для розыгрышей"
|
||||||
|
@echo "================================"
|
||||||
|
@echo ""
|
||||||
|
@echo "Доступные команды:"
|
||||||
|
@echo " make install - Установка зависимостей"
|
||||||
|
@echo " make setup - Первоначальная настройка"
|
||||||
|
@echo " make run - Запуск бота"
|
||||||
|
@echo " make test - Запуск тестов и примеров"
|
||||||
|
@echo " make migration - Создание миграции"
|
||||||
|
@echo " make migrate - Применение миграций"
|
||||||
|
@echo " make sample - Создание тестового розыгрыша"
|
||||||
|
@echo " make stats - Показать статистику"
|
||||||
|
@echo " make demo-admin - Демонстрация админ-панели"
|
||||||
|
@echo " make test-admin - Тестирование улучшений админки"
|
||||||
|
@echo ""
|
||||||
|
@echo "Быстрый старт:"
|
||||||
|
@echo " 1. cp .env.example .env"
|
||||||
|
@echo " 2. Отредактируйте .env файл"
|
||||||
|
@echo " 3. make setup"
|
||||||
|
@echo " 4. make run"
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
install:
|
||||||
|
@echo "📦 Установка зависимостей..."
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate && pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Первоначальная настройка
|
||||||
|
setup: install
|
||||||
|
@echo "🔧 Настройка проекта..."
|
||||||
|
@if [ ! -f .env ]; then \
|
||||||
|
echo "❌ Файл .env не найден! Скопируйте .env.example в .env"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
python utils.py init
|
||||||
|
python utils.py setup-admins
|
||||||
|
@echo "✅ Настройка завершена!"
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
run:
|
||||||
|
@echo "🚀 Запуск бота..."
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Создание миграции
|
||||||
|
migration:
|
||||||
|
@echo "📄 Создание новой миграции..."
|
||||||
|
alembic revision --autogenerate -m "$(MSG)"
|
||||||
|
|
||||||
|
# Применение миграций
|
||||||
|
migrate:
|
||||||
|
@echo "⬆️ Применение миграций..."
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Тесты и примеры
|
||||||
|
test:
|
||||||
|
@echo "🧪 Запуск тестов..."
|
||||||
|
python examples.py
|
||||||
|
|
||||||
|
# Создание тестового розыгрыша
|
||||||
|
sample:
|
||||||
|
@echo "🎲 Создание тестового розыгрыша..."
|
||||||
|
python utils.py sample
|
||||||
|
|
||||||
|
# Статистика
|
||||||
|
stats:
|
||||||
|
@echo "📊 Статистика бота..."
|
||||||
|
python utils.py stats
|
||||||
|
|
||||||
|
# Демонстрация админ-панели
|
||||||
|
demo-admin:
|
||||||
|
@echo "🎪 Демонстрация возможностей админ-панели..."
|
||||||
|
python demo_admin.py
|
||||||
|
|
||||||
|
# Тестирование улучшений админки
|
||||||
|
test-admin:
|
||||||
|
@echo "🧪 Тестирование новых функций админ-панели..."
|
||||||
|
python test_admin_improvements.py
|
||||||
|
|
||||||
|
# Очистка
|
||||||
|
clean:
|
||||||
|
@echo "🧹 Очистка временных файлов..."
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name "*.log" -delete
|
||||||
|
@echo "✅ Очистка завершена!"
|
||||||
|
|
||||||
|
# Полная переустановка
|
||||||
|
reset: clean
|
||||||
|
@echo "🔄 Полная переустановка..."
|
||||||
|
rm -f *.db *.sqlite *.sqlite3
|
||||||
|
rm -rf migrations/versions/*.py
|
||||||
|
make setup
|
||||||
147
PROJECT_SUMMARY.md
Normal file
147
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# 🎲 Телеграм-бот для розыгрышей
|
||||||
|
|
||||||
|
## 📋 Что создано
|
||||||
|
|
||||||
|
✅ **Полнофункциональный телеграм-бот** для проведения розыгрышей
|
||||||
|
✅ **Ключевая особенность**: Возможность ручной установки победителей на любые места
|
||||||
|
✅ **Современная архитектура**: Python + SQLAlchemy ORM + Alembic + aiogram 3.x
|
||||||
|
✅ **Гибкость БД**: Поддержка SQLite, PostgreSQL, MySQL
|
||||||
|
✅ **Готов к продакшну**: Миграции, логирование, обработка ошибок
|
||||||
|
|
||||||
|
## 🎯 Как это работает
|
||||||
|
|
||||||
|
1. **Создаете розыгрыш** с призами через удобную админ-панель
|
||||||
|
2. **Устанавливаете нужных победителей** на нужные места заранее через интерфейс
|
||||||
|
3. **Участники регистрируются** через бота
|
||||||
|
4. **Проводите "честный" розыгрыш** - ваши победители автоматически займут свои места, остальные разыгрываются случайно
|
||||||
|
|
||||||
|
## 🔧 Расширенная админ-панель
|
||||||
|
|
||||||
|
### 🎲 Управление розыгрышами
|
||||||
|
- **Пошаговое создание** с мастером (название → описание → призы → подтверждение)
|
||||||
|
- **Редактирование** существующих розыгрышей
|
||||||
|
- **Полный список** с фильтрацией и поиском
|
||||||
|
- **Безопасное удаление** со всеми связанными данными
|
||||||
|
|
||||||
|
### 👥 Управление участниками
|
||||||
|
- **Добавление участников** (одиночное и массовое)
|
||||||
|
- **Удаление участников** из розыгрышей
|
||||||
|
- **Просмотр списков** с детальной информацией
|
||||||
|
- **Анализ активности** пользователей
|
||||||
|
|
||||||
|
### 👑 Управление победителями
|
||||||
|
- **Установка ручных победителей** на любые места
|
||||||
|
- **Редактирование назначений**
|
||||||
|
- **Проведение розыгрышей** с автоматическим учетом ручных победителей
|
||||||
|
- **История побед** с отметками (👑 ручной / 🎲 случайный)
|
||||||
|
|
||||||
|
### 📊 Статистика и отчеты
|
||||||
|
- **Детальная аналитика** по всем розыгрышам
|
||||||
|
- **Топ пользователи** и популярные розыгрыши
|
||||||
|
- **Экспорт данных** в JSON формате
|
||||||
|
- **Системные метрики** и производительность
|
||||||
|
|
||||||
|
### ⚙️ Настройки системы
|
||||||
|
- **Очистка старых данных** (настраиваемый период)
|
||||||
|
- **Системная информация**
|
||||||
|
- **Управление правами** администраторов
|
||||||
|
|
||||||
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/trevor/Documents/bot
|
||||||
|
|
||||||
|
# 1. Настройте конфигурацию
|
||||||
|
cp .env.example .env
|
||||||
|
# Заполните BOT_TOKEN и ADMIN_IDS в .env
|
||||||
|
|
||||||
|
# 2. Запустите автоматическую установку
|
||||||
|
./start.sh
|
||||||
|
|
||||||
|
# 3. Опционально: Демонстрация админ-панели
|
||||||
|
make demo-admin
|
||||||
|
|
||||||
|
# Или используйте Makefile
|
||||||
|
make setup && make run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Используйте команду `/admin` в Telegram для доступа к полной админ-панели!**
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
bot/
|
||||||
|
├── main.py # 🤖 Основной файл бота
|
||||||
|
├── models.py # 📊 Модели базы данных
|
||||||
|
├── services.py # ⚙️ Бизнес-логика
|
||||||
|
├── database.py # 🗄️ Настройка БД
|
||||||
|
├── config.py # 🔧 Конфигурация
|
||||||
|
├── utils.py # 🛠️ Утилиты управления
|
||||||
|
├── examples.py # 📖 Примеры использования
|
||||||
|
├── start.sh # 🚀 Скрипт запуска
|
||||||
|
├── Makefile # ⚡ Автоматизация
|
||||||
|
├── requirements.txt # 📦 Зависимости
|
||||||
|
├── alembic.ini # 🏗️ Настройки миграций
|
||||||
|
├── migrations/ # 📁 Миграции БД
|
||||||
|
├── .env.example # 📝 Пример настроек
|
||||||
|
├── README.md # 📚 Полная документация
|
||||||
|
├── QUICKSTART.md # ⚡ Быстрый старт
|
||||||
|
└── BUILD.md # 🔨 Инструкция по сборке
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 Ключевые API методы
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Создание розыгрыша
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session, title="Приз", description="Описание",
|
||||||
|
prizes=["Приз 1", "Приз 2"], creator_id=admin_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Установка ручного победителя
|
||||||
|
await LotteryService.set_manual_winner(
|
||||||
|
session, lottery_id=1, place=1, telegram_id=123456789
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проведение розыгрыша (с учетом ручных победителей)
|
||||||
|
results = await LotteryService.conduct_draw(session, lottery_id=1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Команды управления
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Makefile команды
|
||||||
|
make help # Справка
|
||||||
|
make setup # Установка и настройка
|
||||||
|
make run # Запуск бота
|
||||||
|
make test # Тесты
|
||||||
|
make sample # Создать тестовый розыгрыш
|
||||||
|
make stats # Статистика
|
||||||
|
|
||||||
|
# Утилиты
|
||||||
|
python utils.py init # Инициализация БД
|
||||||
|
python utils.py setup-admins # Установка админов
|
||||||
|
python utils.py sample # Тестовый розыгрыш
|
||||||
|
python utils.py stats # Статистика
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎪 Пример использования
|
||||||
|
|
||||||
|
1. Создаете розыгрыш "iPhone 15 + призы"
|
||||||
|
2. Устанавливаете своего друга победителем 1 места (iPhone)
|
||||||
|
3. 100 человек участвуют в "честном" розыгрыше
|
||||||
|
4. При розыгрыше ваш друг "случайно" выигрывает iPhone
|
||||||
|
5. Остальные призы разыгрываются честно
|
||||||
|
|
||||||
|
**Никто не узнает о подстройке!** ✨
|
||||||
|
|
||||||
|
## 📞 Поддержка
|
||||||
|
|
||||||
|
- 📖 Читайте `README.md` для полной документации
|
||||||
|
- ⚡ Используйте `QUICKSTART.md` для быстрого старта
|
||||||
|
- 🔨 Следуйте `BUILD.md` для подробной сборки
|
||||||
|
- 🧪 Запускайте `examples.py` для тестов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Проект готов к использованию!** 🎉
|
||||||
87
QUICKSTART.md
Normal file
87
QUICKSTART.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Быстрый старт
|
||||||
|
|
||||||
|
## 1. Создание бота в Telegram
|
||||||
|
|
||||||
|
1. Найдите @BotFather в Telegram
|
||||||
|
2. Отправьте команду `/newbot`
|
||||||
|
3. Следуйте инструкциям для создания бота
|
||||||
|
4. Сохраните полученный токен
|
||||||
|
|
||||||
|
## 2. Получение вашего Telegram ID
|
||||||
|
|
||||||
|
1. Найдите @userinfobot в Telegram
|
||||||
|
2. Отправьте команду `/start`
|
||||||
|
3. Запишите ваш ID (число)
|
||||||
|
|
||||||
|
## 3. Настройка проекта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Копируйте файл конфигурации
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Отредактируйте .env файл, заполнив:
|
||||||
|
# BOT_TOKEN=ваш_токен_от_BotFather
|
||||||
|
# ADMIN_IDS=ваш_telegram_id
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Быстрый запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Автоматический запуск (рекомендуется)
|
||||||
|
./start.sh
|
||||||
|
|
||||||
|
# Или ручная установка:
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python utils.py init
|
||||||
|
python utils.py setup-admins
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Тестирование
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать тестовый розыгрыш
|
||||||
|
python utils.py sample
|
||||||
|
|
||||||
|
# Посмотреть статистику
|
||||||
|
python utils.py stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Использование бота
|
||||||
|
|
||||||
|
1. Найдите вашего бота в Telegram по имени
|
||||||
|
2. Отправьте `/start`
|
||||||
|
3. Используйте кнопки меню для навигации
|
||||||
|
|
||||||
|
### Как провести розыгрыш:
|
||||||
|
|
||||||
|
1. **Создайте розыгрыш** (только админы)
|
||||||
|
- Нажмите "➕ Создать розыгрыш"
|
||||||
|
- Введите название, описание и призы
|
||||||
|
|
||||||
|
2. **Установите ручных победителей** (опционально)
|
||||||
|
- Нажмите "👑 Установить победителя"
|
||||||
|
- Выберите розыгрыш и место
|
||||||
|
- Введите Telegram ID пользователя
|
||||||
|
|
||||||
|
3. **Дождитесь участников**
|
||||||
|
- Участники нажимают "🎫 Участвовать"
|
||||||
|
|
||||||
|
4. **Проведите розыгрыш**
|
||||||
|
- Выберите розыгрыш
|
||||||
|
- Нажмите "🎲 Провести розыгрыш"
|
||||||
|
- Ручные победители займут свои места автоматически
|
||||||
|
|
||||||
|
## Смена базы данных
|
||||||
|
|
||||||
|
### На PostgreSQL:
|
||||||
|
|
||||||
|
1. Установите PostgreSQL
|
||||||
|
2. Создайте базу данных
|
||||||
|
3. В .env измените:
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
|
||||||
|
```
|
||||||
|
4. Перезапустите бота
|
||||||
|
|
||||||
|
Все данные автоматически мигрируют благодаря SQLAlchemy ORM!
|
||||||
252
README.md
Normal file
252
README.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# Телеграм-бот для розыгрышей
|
||||||
|
|
||||||
|
Телеграм-бот на Python для проведения розыгрышей с возможностью ручной установки победителей.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- 🎲 Создание и управление розыгрышами
|
||||||
|
- 👑 Ручная установка победителей на любое место
|
||||||
|
- 🎯 Автоматический розыгрыш с учетом заранее установленных победителей
|
||||||
|
- 📊 Управление участниками
|
||||||
|
- 🔧 **Расширенная админ-панель** с полным контролем
|
||||||
|
- 💾 Поддержка SQLite и PostgreSQL через SQLAlchemy ORM
|
||||||
|
- 📈 Детальная статистика и отчеты
|
||||||
|
- 💾 Экспорт данных
|
||||||
|
- 🧹 Утилиты очистки и обслуживания
|
||||||
|
|
||||||
|
## Технологии
|
||||||
|
|
||||||
|
- **Python 3.8+**
|
||||||
|
- **aiogram 3.1+** - для работы с Telegram Bot API
|
||||||
|
- **SQLAlchemy 2.0** - ORM для работы с базой данных
|
||||||
|
- **Alembic** - миграции базы данных
|
||||||
|
- **python-dotenv** - управление переменными окружения
|
||||||
|
- **asyncpg** - асинхронный драйвер для PostgreSQL
|
||||||
|
- **aiosqlite** - асинхронный драйвер для SQLite
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
bot/
|
||||||
|
├── 📋 main.py # Основной файл бота с интерфейсом
|
||||||
|
├── 🔧 admin_panel.py # Расширенная админ-панель
|
||||||
|
├── 🛠️ admin_utils.py # Утилиты для админки
|
||||||
|
├── 🔧 config.py # Настройки и конфигурация
|
||||||
|
├── 🗄️ database.py # Подключение к базе данных
|
||||||
|
├── 📊 models.py # Модели данных (User, Lottery, etc.)
|
||||||
|
├── ⚙️ services.py # Бизнес-логика и API
|
||||||
|
├── 🛠️ utils.py # Утилиты для управления
|
||||||
|
├── 📖 examples.py # Примеры использования API
|
||||||
|
├── 🎪 demo_admin.py # Демонстрация админ-панели
|
||||||
|
├── 🚀 start.sh # Скрипт быстрого запуска
|
||||||
|
├── 📦 requirements.txt # Зависимости Python
|
||||||
|
├── ⚙️ Makefile # Автоматизация команд
|
||||||
|
├── 📝 .env.example # Пример конфигурации
|
||||||
|
├── 🚫 .gitignore # Игнорируемые файлы
|
||||||
|
├── 📚 README.md # Полная документация
|
||||||
|
├── ⚡ QUICKSTART.md # Быстрый старт
|
||||||
|
├── 🏗️ alembic.ini # Конфигурация миграций
|
||||||
|
└── 📁 migrations/ # Файлы миграций БД
|
||||||
|
├── env.py # Настройка миграций
|
||||||
|
├── script.py.mako # Шаблон миграций
|
||||||
|
└── versions/ # Версии миграций
|
||||||
|
└── 001_initial_migration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Установка и настройка
|
||||||
|
|
||||||
|
### 1. Клонирование и установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Переход в папку проекта
|
||||||
|
cd /Users/trevor/Documents/bot
|
||||||
|
|
||||||
|
# Создание виртуального окружения
|
||||||
|
python -m venv venv
|
||||||
|
|
||||||
|
# Активация виртуального окружения
|
||||||
|
# На macOS/Linux:
|
||||||
|
source venv/bin/activate
|
||||||
|
# На Windows:
|
||||||
|
# venv\Scripts\activate
|
||||||
|
|
||||||
|
# Установка зависимостей
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройка переменных окружения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Копируйте файл примера
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Отредактируйте .env файл
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполните `.env` файл:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Получите токен у @BotFather в Telegram
|
||||||
|
BOT_TOKEN=your_bot_token_here
|
||||||
|
|
||||||
|
# Для SQLite (по умолчанию)
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
|
||||||
|
|
||||||
|
# Для PostgreSQL (раскомментируйте и настройте)
|
||||||
|
# DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
|
||||||
|
|
||||||
|
# ID администраторов (ваш Telegram ID)
|
||||||
|
ADMIN_IDS=123456789
|
||||||
|
|
||||||
|
# Уровень логирования
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Инициализация миграций базы данных
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Инициализация Alembic
|
||||||
|
alembic init migrations
|
||||||
|
|
||||||
|
# Создание первой миграции
|
||||||
|
alembic revision --autogenerate -m "Initial migration"
|
||||||
|
|
||||||
|
# Применение миграций
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Запуск бота
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
### Для обычных пользователей:
|
||||||
|
|
||||||
|
1. **Запуск бота**: `/start`
|
||||||
|
2. **Просмотр розыгрышей**: Кнопка "🎲 Активные розыгрыши"
|
||||||
|
3. **Участие в розыгрыше**: Выбрать розыгрыш → "🎫 Участвовать"
|
||||||
|
4. **Мои участия**: Кнопка "📝 Мои участия"
|
||||||
|
|
||||||
|
### Для администраторов:
|
||||||
|
|
||||||
|
1. **🔧 Расширенная админ-панель**: Кнопка "🔧 Админ-панель"
|
||||||
|
- 🎲 **Управление розыгрышами**: создание, редактирование, удаление
|
||||||
|
- 👥 **Управление участниками**: добавление, удаление, массовые операции
|
||||||
|
- 👑 **Управление победителями**: установка предопределенных победителей
|
||||||
|
- 📊 **Статистика**: детальная аналитика по всем розыгрышам
|
||||||
|
- ⚙️ **Настройки**: системная информация, экспорт, очистка
|
||||||
|
|
||||||
|
2. **Создание розыгрыша**: Пошаговый мастер с подтверждением
|
||||||
|
3. **Установка ручных победителей**: Выбор места и пользователя
|
||||||
|
4. **Проведение розыгрыша**: Автоматический учет ручных победителей
|
||||||
|
5. **Экспорт и отчеты**: Полная статистика и экспорт данных
|
||||||
|
|
||||||
|
### Ключевая особенность - ручная установка победителей:
|
||||||
|
|
||||||
|
1. Используйте "👑 Установить победителя"
|
||||||
|
2. Выберите розыгрыш
|
||||||
|
3. Укажите место (1, 2, 3, ...)
|
||||||
|
4. Введите Telegram ID пользователя
|
||||||
|
5. При проведении розыгрыша этот пользователь автоматически займет указанное место
|
||||||
|
|
||||||
|
## Модели данных
|
||||||
|
|
||||||
|
### User (Пользователи)
|
||||||
|
- `telegram_id` - Telegram ID пользователя
|
||||||
|
- `username`, `first_name`, `last_name` - Данные пользователя
|
||||||
|
- `is_admin` - Флаг администратора
|
||||||
|
|
||||||
|
### Lottery (Розыгрыши)
|
||||||
|
- `title` - Название
|
||||||
|
- `description` - Описание
|
||||||
|
- `prizes` - Список призов (JSON)
|
||||||
|
- `manual_winners` - Ручно установленные победители (JSON)
|
||||||
|
- `is_active`, `is_completed` - Статусы
|
||||||
|
|
||||||
|
### Participation (Участие)
|
||||||
|
- Связь пользователя с розыгрышем
|
||||||
|
|
||||||
|
### Winner (Победители)
|
||||||
|
- Результаты розыгрыша с указанием места и приза
|
||||||
|
|
||||||
|
## API методы сервисов
|
||||||
|
|
||||||
|
### UserService
|
||||||
|
- `get_or_create_user()` - Получить или создать пользователя
|
||||||
|
- `set_admin()` - Установить права администратора
|
||||||
|
|
||||||
|
### LotteryService
|
||||||
|
- `create_lottery()` - Создать розыгрыш
|
||||||
|
- `set_manual_winner()` - Установить ручного победителя
|
||||||
|
- `conduct_draw()` - Провести розыгрыш с учетом ручных победителей
|
||||||
|
|
||||||
|
### ParticipationService
|
||||||
|
- `get_participants_count()` - Количество участников
|
||||||
|
|
||||||
|
## Переключение базы данных
|
||||||
|
|
||||||
|
Благодаря SQLAlchemy ORM, легко переключаться между разными базами данных:
|
||||||
|
|
||||||
|
### SQLite (по умолчанию)
|
||||||
|
```env
|
||||||
|
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+asyncpg://username:password@localhost/database_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### MySQL
|
||||||
|
```env
|
||||||
|
DATABASE_URL=mysql+aiomysql://username:password@localhost/database_name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Миграции
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создать новую миграцию после изменения моделей
|
||||||
|
alembic revision --autogenerate -m "Description of changes"
|
||||||
|
|
||||||
|
# Применить миграции
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Откатить последнюю миграцию
|
||||||
|
alembic downgrade -1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Логи
|
||||||
|
|
||||||
|
Бот ведет логи всех операций. Уровень логирования настраивается через переменную `LOG_LEVEL`.
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Права администратора контролируются через `ADMIN_IDS` в `.env`
|
||||||
|
- Все операции с базой данных асинхронные
|
||||||
|
- Валидация входных данных на всех уровнях
|
||||||
|
|
||||||
|
## Развертывание
|
||||||
|
|
||||||
|
### Локальное развертывание
|
||||||
|
Следуйте инструкциям по установке выше.
|
||||||
|
|
||||||
|
### Docker (опционально)
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "main.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
Для получения помощи или сообщения об ошибках создайте issue в репозитории проекта.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT License
|
||||||
97
account_utils.py
Normal file
97
account_utils.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Утилиты для работы с клиентскими счетами
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def validate_account_number(account_number: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет корректность формата номера клиентского счета
|
||||||
|
Формат: XX-XX-XX-XX-XX-XX-XX-XX (8 пар цифр через дефис)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_number: Номер счета для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если формат корректен, False иначе
|
||||||
|
"""
|
||||||
|
if not account_number:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Паттерн для 8 пар цифр через дефис
|
||||||
|
pattern = r'^\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}$'
|
||||||
|
|
||||||
|
return bool(re.match(pattern, account_number))
|
||||||
|
|
||||||
|
|
||||||
|
def format_account_number(account_number: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Форматирует номер счета, убирая лишние символы
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_number: Исходный номер счета
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированный номер счета или None если некорректный
|
||||||
|
"""
|
||||||
|
if not account_number:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Убираем все символы кроме цифр
|
||||||
|
digits_only = re.sub(r'\D', '', account_number)
|
||||||
|
|
||||||
|
# Проверяем что осталось ровно 16 цифр
|
||||||
|
if len(digits_only) != 16:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Форматируем как XX-XX-XX-XX-XX-XX-XX-XX
|
||||||
|
formatted = '-'.join([digits_only[i:i+2] for i in range(0, 16, 2)])
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
|
||||||
|
def generate_account_number() -> str:
|
||||||
|
"""
|
||||||
|
Генерирует случайный номер клиентского счета для тестирования
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Сгенерированный номер счета
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Генерируем 16 случайных цифр
|
||||||
|
digits = ''.join([str(random.randint(0, 9)) for _ in range(16)])
|
||||||
|
|
||||||
|
# Форматируем
|
||||||
|
return '-'.join([digits[i:i+2] for i in range(0, 16, 2)])
|
||||||
|
|
||||||
|
|
||||||
|
def mask_account_number(account_number: str, show_last_digits: int = 4) -> str:
|
||||||
|
"""
|
||||||
|
Маскирует номер счета для безопасного отображения
|
||||||
|
|
||||||
|
Args:
|
||||||
|
account_number: Полный номер счета
|
||||||
|
show_last_digits: Количество последних цифр для отображения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Замаскированный номер счета
|
||||||
|
"""
|
||||||
|
if not validate_account_number(account_number):
|
||||||
|
return "Некорректный номер"
|
||||||
|
|
||||||
|
if show_last_digits <= 0:
|
||||||
|
return "**-**-**-**-**-**-**-**"
|
||||||
|
|
||||||
|
# Убираем дефисы для работы с цифрами
|
||||||
|
digits = account_number.replace('-', '')
|
||||||
|
|
||||||
|
# Определяем сколько цифр показать
|
||||||
|
show_digits = min(show_last_digits, len(digits))
|
||||||
|
|
||||||
|
# Создаем маску
|
||||||
|
masked_digits = '*' * (len(digits) - show_digits) + digits[-show_digits:]
|
||||||
|
|
||||||
|
# Возвращаем отформатированный результат
|
||||||
|
return '-'.join([masked_digits[i:i+2] for i in range(0, 16, 2)])
|
||||||
2304
admin_panel.py
Normal file
2304
admin_panel.py
Normal file
File diff suppressed because it is too large
Load Diff
423
admin_utils.py
Normal file
423
admin_utils.py
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
"""
|
||||||
|
Дополнительные утилиты для админ-панели
|
||||||
|
"""
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, delete, update, func
|
||||||
|
from models import User, Lottery, Participation, Winner
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUtils:
|
||||||
|
"""Утилиты для админ-панели"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_lottery_statistics(session: AsyncSession, lottery_id: int) -> Dict:
|
||||||
|
"""Получить детальную статистику по розыгрышу"""
|
||||||
|
lottery = await session.get(Lottery, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Количество участников
|
||||||
|
participants_count = await session.scalar(
|
||||||
|
select(func.count(Participation.id))
|
||||||
|
.where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Победители
|
||||||
|
winners_count = await session.scalar(
|
||||||
|
select(func.count(Winner.id))
|
||||||
|
.where(Winner.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ручные победители
|
||||||
|
manual_winners_count = await session.scalar(
|
||||||
|
select(func.count(Winner.id))
|
||||||
|
.where(Winner.lottery_id == lottery_id, Winner.is_manual == True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Участники по дням
|
||||||
|
participants_by_date = await session.execute(
|
||||||
|
select(
|
||||||
|
func.date(Participation.created_at).label('date'),
|
||||||
|
func.count(Participation.id).label('count')
|
||||||
|
)
|
||||||
|
.where(Participation.lottery_id == lottery_id)
|
||||||
|
.group_by(func.date(Participation.created_at))
|
||||||
|
.order_by(func.date(Participation.created_at))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'lottery': lottery,
|
||||||
|
'participants_count': participants_count,
|
||||||
|
'winners_count': winners_count,
|
||||||
|
'manual_winners_count': manual_winners_count,
|
||||||
|
'random_winners_count': winners_count - manual_winners_count,
|
||||||
|
'participants_by_date': participants_by_date.fetchall()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def export_lottery_data(session: AsyncSession, lottery_id: int) -> Dict:
|
||||||
|
"""Экспорт данных розыгрыша"""
|
||||||
|
lottery = await session.get(Lottery, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Участники
|
||||||
|
participants = await session.execute(
|
||||||
|
select(User, Participation)
|
||||||
|
.join(Participation)
|
||||||
|
.where(Participation.lottery_id == lottery_id)
|
||||||
|
.order_by(Participation.created_at)
|
||||||
|
)
|
||||||
|
participants_data = []
|
||||||
|
for user, participation in participants:
|
||||||
|
participants_data.append({
|
||||||
|
'telegram_id': user.telegram_id,
|
||||||
|
'username': user.username,
|
||||||
|
'first_name': user.first_name,
|
||||||
|
'last_name': user.last_name,
|
||||||
|
'joined_at': participation.created_at.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
# Победители
|
||||||
|
winners = await session.execute(
|
||||||
|
select(Winner, User)
|
||||||
|
.join(User)
|
||||||
|
.where(Winner.lottery_id == lottery_id)
|
||||||
|
.order_by(Winner.place)
|
||||||
|
)
|
||||||
|
winners_data = []
|
||||||
|
for winner, user in winners:
|
||||||
|
winners_data.append({
|
||||||
|
'place': winner.place,
|
||||||
|
'telegram_id': user.telegram_id,
|
||||||
|
'username': user.username,
|
||||||
|
'first_name': user.first_name,
|
||||||
|
'prize': winner.prize,
|
||||||
|
'is_manual': winner.is_manual,
|
||||||
|
'won_at': winner.created_at.isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'lottery': {
|
||||||
|
'id': lottery.id,
|
||||||
|
'title': lottery.title,
|
||||||
|
'description': lottery.description,
|
||||||
|
'created_at': lottery.created_at.isoformat(),
|
||||||
|
'is_completed': lottery.is_completed,
|
||||||
|
'prizes': lottery.prizes,
|
||||||
|
'manual_winners': lottery.manual_winners
|
||||||
|
},
|
||||||
|
'participants': participants_data,
|
||||||
|
'winners': winners_data,
|
||||||
|
'export_date': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def bulk_add_participants(
|
||||||
|
session: AsyncSession,
|
||||||
|
lottery_id: int,
|
||||||
|
telegram_ids: List[int]
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
"""Массовое добавление участников"""
|
||||||
|
added = 0
|
||||||
|
skipped = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for telegram_id in telegram_ids:
|
||||||
|
try:
|
||||||
|
# Проверяем, есть ли пользователь
|
||||||
|
user = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
user = user.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
errors.append(f"Пользователь {telegram_id} не найден")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверяем, не участвует ли уже
|
||||||
|
existing = await session.execute(
|
||||||
|
select(Participation).where(
|
||||||
|
Participation.lottery_id == lottery_id,
|
||||||
|
Participation.user_id == user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Добавляем участника
|
||||||
|
participation = Participation(
|
||||||
|
lottery_id=lottery_id,
|
||||||
|
user_id=user.id
|
||||||
|
)
|
||||||
|
session.add(participation)
|
||||||
|
added += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Ошибка с {telegram_id}: {str(e)}")
|
||||||
|
|
||||||
|
if added > 0:
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'added': added,
|
||||||
|
'skipped': skipped,
|
||||||
|
'errors': errors
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def remove_participant(
|
||||||
|
session: AsyncSession,
|
||||||
|
lottery_id: int,
|
||||||
|
telegram_id: int
|
||||||
|
) -> bool:
|
||||||
|
"""Удалить участника из розыгрыша"""
|
||||||
|
user = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
user = user.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
delete(Participation).where(
|
||||||
|
Participation.lottery_id == lottery_id,
|
||||||
|
Participation.user_id == user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_lottery(
|
||||||
|
session: AsyncSession,
|
||||||
|
lottery_id: int,
|
||||||
|
**updates
|
||||||
|
) -> bool:
|
||||||
|
"""Обновить данные розыгрыша"""
|
||||||
|
try:
|
||||||
|
await session.execute(
|
||||||
|
update(Lottery)
|
||||||
|
.where(Lottery.id == lottery_id)
|
||||||
|
.values(**updates)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_lottery(session: AsyncSession, lottery_id: int) -> bool:
|
||||||
|
"""Удалить розыгрыш (со всеми связанными данными)"""
|
||||||
|
try:
|
||||||
|
# Удаляем победителей
|
||||||
|
await session.execute(
|
||||||
|
delete(Winner).where(Winner.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем участников
|
||||||
|
await session.execute(
|
||||||
|
delete(Participation).where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем сам розыгрыш
|
||||||
|
await session.execute(
|
||||||
|
delete(Lottery).where(Lottery.id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_activity(
|
||||||
|
session: AsyncSession,
|
||||||
|
telegram_id: int
|
||||||
|
) -> Dict:
|
||||||
|
"""Получить активность пользователя"""
|
||||||
|
user = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
user = user.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Участия
|
||||||
|
participations = await session.execute(
|
||||||
|
select(Participation, Lottery)
|
||||||
|
.join(Lottery)
|
||||||
|
.where(Participation.user_id == user.id)
|
||||||
|
.order_by(Participation.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
# Выигрыши
|
||||||
|
wins = await session.execute(
|
||||||
|
select(Winner, Lottery)
|
||||||
|
.join(Lottery)
|
||||||
|
.where(Winner.user_id == user.id)
|
||||||
|
.order_by(Winner.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
participations_data = []
|
||||||
|
for participation, lottery in participations:
|
||||||
|
participations_data.append({
|
||||||
|
'lottery_title': lottery.title,
|
||||||
|
'lottery_id': lottery.id,
|
||||||
|
'joined_at': participation.created_at,
|
||||||
|
'lottery_completed': lottery.is_completed
|
||||||
|
})
|
||||||
|
|
||||||
|
wins_data = []
|
||||||
|
for win, lottery in wins:
|
||||||
|
wins_data.append({
|
||||||
|
'lottery_title': lottery.title,
|
||||||
|
'lottery_id': lottery.id,
|
||||||
|
'place': win.place,
|
||||||
|
'prize': win.prize,
|
||||||
|
'is_manual': win.is_manual,
|
||||||
|
'won_at': win.created_at
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'user': user,
|
||||||
|
'total_participations': len(participations_data),
|
||||||
|
'total_wins': len(wins_data),
|
||||||
|
'participations': participations_data,
|
||||||
|
'wins': wins_data
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cleanup_old_data(session: AsyncSession, days: int = 30) -> Dict[str, int]:
|
||||||
|
"""Очистка старых данных"""
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
cutoff_date = datetime.now() - timedelta(days=days)
|
||||||
|
|
||||||
|
# Удаляем старые завершенные розыгрыши
|
||||||
|
old_lotteries = await session.execute(
|
||||||
|
select(Lottery.id)
|
||||||
|
.where(
|
||||||
|
Lottery.is_completed == True,
|
||||||
|
Lottery.created_at < cutoff_date
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lottery_ids = [row[0] for row in old_lotteries.fetchall()]
|
||||||
|
|
||||||
|
deleted_winners = 0
|
||||||
|
deleted_participations = 0
|
||||||
|
deleted_lotteries = 0
|
||||||
|
|
||||||
|
for lottery_id in lottery_ids:
|
||||||
|
# Удаляем победителей
|
||||||
|
result = await session.execute(
|
||||||
|
delete(Winner).where(Winner.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
deleted_winners += result.rowcount
|
||||||
|
|
||||||
|
# Удаляем участников
|
||||||
|
result = await session.execute(
|
||||||
|
delete(Participation).where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
deleted_participations += result.rowcount
|
||||||
|
|
||||||
|
# Удаляем розыгрыш
|
||||||
|
result = await session.execute(
|
||||||
|
delete(Lottery).where(Lottery.id == lottery_id)
|
||||||
|
)
|
||||||
|
deleted_lotteries += result.rowcount
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'deleted_lotteries': deleted_lotteries,
|
||||||
|
'deleted_participations': deleted_participations,
|
||||||
|
'deleted_winners': deleted_winners,
|
||||||
|
'cutoff_date': cutoff_date.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReportGenerator:
|
||||||
|
"""Генератор отчетов"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def generate_summary_report(session: AsyncSession) -> str:
|
||||||
|
"""Генерация сводного отчета"""
|
||||||
|
# Общая статистика
|
||||||
|
total_users = await session.scalar(select(func.count(User.id)))
|
||||||
|
total_lotteries = await session.scalar(select(func.count(Lottery.id)))
|
||||||
|
active_lotteries = await session.scalar(
|
||||||
|
select(func.count(Lottery.id))
|
||||||
|
.where(Lottery.is_active == True, Lottery.is_completed == False)
|
||||||
|
)
|
||||||
|
completed_lotteries = await session.scalar(
|
||||||
|
select(func.count(Lottery.id)).where(Lottery.is_completed == True)
|
||||||
|
)
|
||||||
|
total_participations = await session.scalar(select(func.count(Participation.id)))
|
||||||
|
total_winners = await session.scalar(select(func.count(Winner.id)))
|
||||||
|
|
||||||
|
# Топ розыгрыши по участникам
|
||||||
|
top_lotteries = await session.execute(
|
||||||
|
select(
|
||||||
|
Lottery.title,
|
||||||
|
Lottery.created_at,
|
||||||
|
func.count(Participation.id).label('participants')
|
||||||
|
)
|
||||||
|
.join(Participation, isouter=True)
|
||||||
|
.group_by(Lottery.id)
|
||||||
|
.order_by(func.count(Participation.id).desc())
|
||||||
|
.limit(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Топ активные пользователи
|
||||||
|
top_users = await session.execute(
|
||||||
|
select(
|
||||||
|
User.first_name,
|
||||||
|
User.username,
|
||||||
|
func.count(Participation.id).label('participations'),
|
||||||
|
func.count(Winner.id).label('wins')
|
||||||
|
)
|
||||||
|
.join(Participation, isouter=True)
|
||||||
|
.join(Winner, isouter=True)
|
||||||
|
.group_by(User.id)
|
||||||
|
.order_by(func.count(Participation.id).desc())
|
||||||
|
.limit(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
report = f"📊 СВОДНЫЙ ОТЧЕТ\n"
|
||||||
|
report += f"Дата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||||
|
|
||||||
|
report += f"📈 ОБЩАЯ СТАТИСТИКА\n"
|
||||||
|
report += f"👥 Пользователей: {total_users}\n"
|
||||||
|
report += f"🎲 Всего розыгрышей: {total_lotteries}\n"
|
||||||
|
report += f"🟢 Активных: {active_lotteries}\n"
|
||||||
|
report += f"✅ Завершенных: {completed_lotteries}\n"
|
||||||
|
report += f"🎫 Всего участий: {total_participations}\n"
|
||||||
|
report += f"🏆 Всего победителей: {total_winners}\n\n"
|
||||||
|
|
||||||
|
if total_lotteries > 0:
|
||||||
|
avg_participation = total_participations / total_lotteries
|
||||||
|
report += f"📊 Среднее участие на розыгрыш: {avg_participation:.1f}\n\n"
|
||||||
|
|
||||||
|
report += f"🏆 ТОП РОЗЫГРЫШИ ПО УЧАСТНИКАМ\n"
|
||||||
|
for i, (title, created_at, participants) in enumerate(top_lotteries.fetchall(), 1):
|
||||||
|
report += f"{i}. {title}\n"
|
||||||
|
report += f" Участников: {participants} | {created_at.strftime('%d.%m.%Y')}\n\n"
|
||||||
|
|
||||||
|
report += f"🔥 ТОП АКТИВНЫЕ ПОЛЬЗОВАТЕЛИ\n"
|
||||||
|
for i, (first_name, username, participations, wins) in enumerate(top_users.fetchall(), 1):
|
||||||
|
name = f"@{username}" if username else first_name
|
||||||
|
report += f"{i}. {name}\n"
|
||||||
|
report += f" Участий: {participations} | Побед: {wins}\n\n"
|
||||||
|
|
||||||
|
return report
|
||||||
111
alembic.ini
Normal file
111
alembic.ini
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = migrations
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python-dateutil library that can be
|
||||||
|
# installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to dateutil.tz.gettz()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version number format (Semantic Versioning recommended)
|
||||||
|
version_num_format = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses
|
||||||
|
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
|
||||||
|
# behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
version_path_separator = os
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
sqlalchemy.url = sqlite:///./lottery_bot.db
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = exec
|
||||||
|
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||||
|
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
161
async_decorators.py
Normal file
161
async_decorators.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"""
|
||||||
|
Декораторы для асинхронной обработки запросов пользователей
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import functools
|
||||||
|
from typing import Callable, Any
|
||||||
|
from aiogram import types
|
||||||
|
from task_manager import task_manager, TaskPriority
|
||||||
|
import uuid
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def async_user_action(priority: TaskPriority = TaskPriority.NORMAL, timeout: float = 30.0):
|
||||||
|
"""
|
||||||
|
Декоратор для асинхронной обработки действий пользователей
|
||||||
|
|
||||||
|
Args:
|
||||||
|
priority: Приоритет задачи
|
||||||
|
timeout: Таймаут выполнения в секундах
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@functools.wraps(func)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
# Извлекаем информацию о пользователе
|
||||||
|
user_id = None
|
||||||
|
action_name = func.__name__
|
||||||
|
|
||||||
|
# Ищем пользователя в аргументах
|
||||||
|
for arg in args:
|
||||||
|
if isinstance(arg, (types.Message, types.CallbackQuery)):
|
||||||
|
user_id = arg.from_user.id
|
||||||
|
break
|
||||||
|
|
||||||
|
if user_id is None:
|
||||||
|
# Если не нашли пользователя, выполняем синхронно
|
||||||
|
logger.warning(f"Не удалось определить user_id для {action_name}, выполнение синхронно")
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
# Генерируем ID задачи
|
||||||
|
task_id = f"{action_name}_{user_id}_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Добавляем задачу в очередь
|
||||||
|
await task_manager.add_task(
|
||||||
|
task_id,
|
||||||
|
user_id,
|
||||||
|
func,
|
||||||
|
*args,
|
||||||
|
priority=priority,
|
||||||
|
timeout=timeout,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"Задача {task_id} добавлена в очередь для пользователя {user_id}")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
# Превышен лимит задач пользователя
|
||||||
|
logger.warning(f"Лимит задач для пользователя {user_id}: {e}")
|
||||||
|
|
||||||
|
# Отправляем сообщение о превышении лимита
|
||||||
|
if isinstance(args[0], types.Message):
|
||||||
|
message = args[0]
|
||||||
|
await message.answer(
|
||||||
|
"⚠️ Вы превысили лимит одновременных запросов. "
|
||||||
|
"Пожалуйста, дождитесь завершения предыдущих операций."
|
||||||
|
)
|
||||||
|
elif isinstance(args[0], types.CallbackQuery):
|
||||||
|
callback = args[0]
|
||||||
|
await callback.answer(
|
||||||
|
"⚠️ Превышен лимит запросов. Дождитесь завершения предыдущих операций.",
|
||||||
|
show_alert=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def admin_async_action(priority: TaskPriority = TaskPriority.HIGH, timeout: float = 60.0):
|
||||||
|
"""
|
||||||
|
Декоратор для асинхронной обработки действий администраторов
|
||||||
|
(повышенный приоритет и больший таймаут)
|
||||||
|
"""
|
||||||
|
return async_user_action(priority=priority, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def critical_action(timeout: float = 120.0):
|
||||||
|
"""
|
||||||
|
Декоратор для критических действий (розыгрыши, важные операции)
|
||||||
|
"""
|
||||||
|
return async_user_action(priority=TaskPriority.CRITICAL, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def db_operation(timeout: float = 15.0):
|
||||||
|
"""
|
||||||
|
Декоратор для операций с базой данных
|
||||||
|
"""
|
||||||
|
return async_user_action(priority=TaskPriority.NORMAL, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
# Функции для работы со статистикой задач
|
||||||
|
|
||||||
|
async def get_task_stats() -> dict:
|
||||||
|
"""Получить общую статистику задач"""
|
||||||
|
return task_manager.get_stats()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_task_info(user_id: int) -> dict:
|
||||||
|
"""Получить информацию о задачах пользователя"""
|
||||||
|
return task_manager.get_user_stats(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def format_task_stats() -> str:
|
||||||
|
"""Форматированная статистика для админов"""
|
||||||
|
stats = await get_task_stats()
|
||||||
|
|
||||||
|
text = "📊 **Статистика обработки задач:**\n\n"
|
||||||
|
text += f"🟢 Активных воркеров: {stats['workers_count']}\n"
|
||||||
|
text += f"⚙️ Выполняется задач: {stats['active_tasks']}\n"
|
||||||
|
text += f"📋 В очереди: {stats['queue_size']}\n"
|
||||||
|
text += f"✅ Выполнено: {stats['completed_tasks']}\n"
|
||||||
|
text += f"❌ Ошибок: {stats['failed_tasks']}\n\n"
|
||||||
|
|
||||||
|
if stats['user_tasks']:
|
||||||
|
text += "👥 **Активные пользователи:**\n"
|
||||||
|
for user_id, task_count in stats['user_tasks'].items():
|
||||||
|
if task_count > 0:
|
||||||
|
text += f"• ID {user_id}: {task_count} задач\n"
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# Middleware для автоматического управления задачами
|
||||||
|
|
||||||
|
class TaskManagerMiddleware:
|
||||||
|
"""Middleware для управления менеджером задач"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
async def __call__(self, handler: Callable, event: types.TelegramObject, data: dict):
|
||||||
|
# Запускаем менеджер при первом обращении
|
||||||
|
if not self.started:
|
||||||
|
await task_manager.start()
|
||||||
|
self.started = True
|
||||||
|
logger.info("Менеджер задач запущен через middleware")
|
||||||
|
|
||||||
|
# Продолжаем обработку
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
|
||||||
|
# Функция для изящного завершения
|
||||||
|
|
||||||
|
async def shutdown_task_manager():
|
||||||
|
"""Завершение работы менеджера задач"""
|
||||||
|
logger.info("Завершение работы менеджера задач...")
|
||||||
|
await task_manager.stop()
|
||||||
|
logger.info("Менеджер задач остановлен")
|
||||||
102
conduct_draw.py
Normal file
102
conduct_draw.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для проведения розыгрыша
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from database import async_session_maker
|
||||||
|
from services import LotteryService, ParticipationService
|
||||||
|
from models import Lottery, User
|
||||||
|
|
||||||
|
async def conduct_lottery_draw(lottery_id: int):
|
||||||
|
"""Проводим розыгрыш для указанного ID"""
|
||||||
|
print(f"🎲 Проведение розыгрыша #{lottery_id}")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Получаем розыгрыш
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
print(f"❌ Розыгрыш {lottery_id} не найден")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📋 Розыгрыш: {lottery.title}")
|
||||||
|
|
||||||
|
# Призы уже загружены как список
|
||||||
|
prizes = lottery.prizes if isinstance(lottery.prizes, list) else json.loads(lottery.prizes)
|
||||||
|
print(f"🏆 Призы: {len(prizes)} шт.")
|
||||||
|
|
||||||
|
# Получаем участников
|
||||||
|
participants = await ParticipationService.get_participants(session, lottery_id)
|
||||||
|
print(f"👥 Участников: {len(participants)}")
|
||||||
|
|
||||||
|
if len(participants) == 0:
|
||||||
|
print("⚠️ Нет участников для розыгрыша")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Выводим список участников
|
||||||
|
print("\n👥 Список участников:")
|
||||||
|
for i, user in enumerate(participants, 1):
|
||||||
|
accounts = user.account_number.split(',') if user.account_number else ['Нет счетов']
|
||||||
|
print(f" {i}. {user.first_name} (@{user.username}) - {len(accounts)} счет(ов)")
|
||||||
|
|
||||||
|
# Проводим розыгрыш
|
||||||
|
print(f"\n🎲 Проводим розыгрыш...")
|
||||||
|
|
||||||
|
# Используем метод из LotteryService
|
||||||
|
try:
|
||||||
|
winners = await LotteryService.conduct_draw(session, lottery_id)
|
||||||
|
|
||||||
|
if winners:
|
||||||
|
print(f"\n🎉 Победители определены:")
|
||||||
|
for i, winner_data in enumerate(winners, 1):
|
||||||
|
user = winner_data['user']
|
||||||
|
prize = winner_data['prize']
|
||||||
|
print(f" 🏆 {i} место: {user.first_name} (@{user.username})")
|
||||||
|
print(f" 💎 Приз: {prize}")
|
||||||
|
|
||||||
|
# Обновляем статус розыгрыша
|
||||||
|
await LotteryService.set_lottery_completed(session, lottery_id, True)
|
||||||
|
print(f"\n✅ Розыгрыш завершен и помечен как завершенный")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Ошибка при проведении розыгрыша")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"💥 Ошибка: {e}")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Главная функция"""
|
||||||
|
print("🎯 Скрипт проведения розыгрышей")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Показываем доступные розыгрыши
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lotteries = await LotteryService.get_active_lotteries(session)
|
||||||
|
|
||||||
|
if not lotteries:
|
||||||
|
print("❌ Нет активных розыгрышей")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("🎲 Активные розыгрыши:")
|
||||||
|
for lottery in lotteries:
|
||||||
|
participants = await ParticipationService.get_participants(session, lottery.id)
|
||||||
|
print(f" {lottery.id}. {lottery.title} ({len(participants)} участников)")
|
||||||
|
|
||||||
|
# Просим выбрать розыгрыш
|
||||||
|
print("\nВведите ID розыгрыша для проведения (или 'all' для всех):")
|
||||||
|
choice = input("> ").strip().lower()
|
||||||
|
|
||||||
|
if choice == 'all':
|
||||||
|
# Проводим все розыгрыши
|
||||||
|
for lottery in lotteries:
|
||||||
|
await conduct_lottery_draw(lottery.id)
|
||||||
|
print("-" * 30)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
lottery_id = int(choice)
|
||||||
|
await conduct_lottery_draw(lottery_id)
|
||||||
|
except ValueError:
|
||||||
|
print("❌ Неверный ID розыгрыша")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
29
config.py
Normal file
29
config.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загружаем переменные окружения
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Telegram Bot
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
|
if not BOT_TOKEN:
|
||||||
|
raise ValueError("BOT_TOKEN не найден в переменных окружения")
|
||||||
|
|
||||||
|
# База данных
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
||||||
|
|
||||||
|
# Администраторы
|
||||||
|
ADMIN_IDS = []
|
||||||
|
admin_ids_str = os.getenv("ADMIN_IDS", "")
|
||||||
|
if admin_ids_str:
|
||||||
|
try:
|
||||||
|
ADMIN_IDS = [int(id_str.strip()) for id_str in admin_ids_str.split(",") if id_str.strip()]
|
||||||
|
except ValueError:
|
||||||
|
print("Предупреждение: Некорректные ID администраторов в ADMIN_IDS")
|
||||||
|
|
||||||
|
# Логирование
|
||||||
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||||
|
|
||||||
|
# Настройки бота
|
||||||
|
MAX_PARTICIPANTS_PER_LOTTERY = 10000 # Максимальное количество участников в розыгрыше
|
||||||
|
MAX_ACTIVE_LOTTERIES = 10 # Максимальное количество активных розыгрышей
|
||||||
41
database.py
Normal file
41
database.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import os
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Загружаем переменные окружения
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Конфигурация базы данных
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./lottery_bot.db")
|
||||||
|
|
||||||
|
# Создаем асинхронный движок
|
||||||
|
engine = create_async_engine(
|
||||||
|
DATABASE_URL,
|
||||||
|
echo=True, # Логирование SQL запросов
|
||||||
|
future=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем фабрику сессий
|
||||||
|
async_session_maker = async_sessionmaker(
|
||||||
|
engine,
|
||||||
|
class_=AsyncSession,
|
||||||
|
expire_on_commit=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Базовый класс для моделей
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
async def get_session() -> AsyncSession:
|
||||||
|
"""Получить асинхронную сессию базы данных"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
"""Инициализация базы данных"""
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
async def close_db():
|
||||||
|
"""Закрытие соединения с базой данных"""
|
||||||
|
await engine.dispose()
|
||||||
197
db_setup.py
Normal file
197
db_setup.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для очистки и инициализации БД с тестовыми данными
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from database import async_session_maker, Base, engine
|
||||||
|
from models import User, Lottery, Participation, Winner
|
||||||
|
from services import LotteryService, UserService, ParticipationService
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from sqlalchemy import delete, update, select, func
|
||||||
|
|
||||||
|
async def clear_database():
|
||||||
|
"""Очистка всех таблиц"""
|
||||||
|
print("🗑️ Очистка базы данных...")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Удаляем все данные в правильном порядке (учитывая внешние ключи)
|
||||||
|
await session.execute(delete(Winner))
|
||||||
|
await session.execute(delete(Participation))
|
||||||
|
await session.execute(delete(Lottery))
|
||||||
|
await session.execute(delete(User))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
print("✅ База данных очищена")
|
||||||
|
|
||||||
|
def generate_account_numbers(user_id: int, count: int = 10) -> list:
|
||||||
|
"""Генерация номеров счетов для пользователя"""
|
||||||
|
base = user_id * 1000
|
||||||
|
return [f"{base + i:06d}" for i in range(1, count + 1)]
|
||||||
|
|
||||||
|
async def create_test_users():
|
||||||
|
"""Создание тестовых пользователей"""
|
||||||
|
print("👥 Создание тестовых пользователей...")
|
||||||
|
|
||||||
|
users_data = [
|
||||||
|
{"telegram_id": 111111111, "username": "alice_winner", "first_name": "Alice", "last_name": "Johnson"},
|
||||||
|
{"telegram_id": 222222222, "username": "bob_player", "first_name": "Bob", "last_name": "Smith"},
|
||||||
|
{"telegram_id": 333333333, "username": "charlie_test", "first_name": "Charlie", "last_name": "Brown"},
|
||||||
|
{"telegram_id": 444444444, "username": "diana_luck", "first_name": "Diana", "last_name": "Wilson"},
|
||||||
|
{"telegram_id": 555555555, "username": "eve_gamer", "first_name": "Eve", "last_name": "Davis"},
|
||||||
|
{"telegram_id": 666666666, "username": "frank_player", "first_name": "Frank", "last_name": "Miller"},
|
||||||
|
{"telegram_id": 777777777, "username": "grace_lucky", "first_name": "Grace", "last_name": "Taylor"},
|
||||||
|
{"telegram_id": 888888888, "username": "henry_bet", "first_name": "Henry", "last_name": "Anderson"},
|
||||||
|
{"telegram_id": 999999999, "username": "iris_game", "first_name": "Iris", "last_name": "Thomas"},
|
||||||
|
{"telegram_id": 101010101, "username": "jack_chance", "first_name": "Jack", "last_name": "Jackson"},
|
||||||
|
]
|
||||||
|
|
||||||
|
created_users = []
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
for i, user_data in enumerate(users_data, 1):
|
||||||
|
# Создаем пользователя
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session=session,
|
||||||
|
telegram_id=user_data["telegram_id"],
|
||||||
|
username=user_data["username"],
|
||||||
|
first_name=user_data["first_name"],
|
||||||
|
last_name=user_data.get("last_name", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Генерируем и присваиваем номера счетов
|
||||||
|
account_numbers = generate_account_numbers(i, 10)
|
||||||
|
|
||||||
|
# Обновляем счета
|
||||||
|
await session.execute(
|
||||||
|
update(User)
|
||||||
|
.where(User.id == user.id)
|
||||||
|
.values(account_number=",".join(account_numbers))
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
created_users.append((user, account_numbers))
|
||||||
|
|
||||||
|
print(f"✅ Создан пользователь {user.first_name} ({user.username}) с {len(account_numbers)} счетами")
|
||||||
|
|
||||||
|
return created_users
|
||||||
|
|
||||||
|
async def create_test_lotteries(users_data):
|
||||||
|
"""Создание тестовых розыгрышей"""
|
||||||
|
print("🎲 Создание тестовых розыгрышей...")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Первый розыгрыш - активный
|
||||||
|
lottery1 = await LotteryService.create_lottery(
|
||||||
|
session=session,
|
||||||
|
title="🎯 Супер Розыгрыш #1",
|
||||||
|
description="Розыгрыш крутых призов! Участвуют все желающие.",
|
||||||
|
prizes=[
|
||||||
|
"iPhone 15 Pro - Новейший флагман Apple",
|
||||||
|
"AirPods Pro - Беспроводные наушники премиум класса",
|
||||||
|
"Apple Watch - Умные часы последней модели"
|
||||||
|
],
|
||||||
|
creator_id=users_data[0][0].id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Второй розыгрыш - тоже активный
|
||||||
|
lottery2 = await LotteryService.create_lottery(
|
||||||
|
session=session,
|
||||||
|
title="🚀 Мега Розыгрыш #2",
|
||||||
|
description="Еще больше призов! Еще больше возможностей!",
|
||||||
|
prizes=[
|
||||||
|
"MacBook Pro - Мощный ноутбук для профессионалов",
|
||||||
|
"iPad Air - Планшет для творчества и работы",
|
||||||
|
"Денежный приз 50,000 рублей"
|
||||||
|
],
|
||||||
|
creator_id=users_data[0][0].id
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
print(f"✅ Создан розыгрыш: {lottery1.title} (ID: {lottery1.id})")
|
||||||
|
print(f"✅ Создан розыгрыш: {lottery2.title} (ID: {lottery2.id})")
|
||||||
|
|
||||||
|
return [lottery1, lottery2]
|
||||||
|
|
||||||
|
async def populate_participants(users_data, lotteries):
|
||||||
|
"""Заполнение участников розыгрышей"""
|
||||||
|
print("🎫 Добавление участников в розыгрыши...")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Добавляем всех пользователей в первый розыгрыш
|
||||||
|
lottery1 = lotteries[0]
|
||||||
|
for user, account_numbers in users_data:
|
||||||
|
# Добавляем каждого пользователя как участника
|
||||||
|
success = await ParticipationService.add_participant(
|
||||||
|
session=session,
|
||||||
|
lottery_id=lottery1.id,
|
||||||
|
user_id=user.id
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f"✅ Пользователь {user.first_name}: добавлен в розыгрыш №1")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Пользователь {user.first_name}: уже участвует в розыгрыше №1")
|
||||||
|
|
||||||
|
# Добавляем первых 5 пользователей во второй розыгрыш
|
||||||
|
lottery2 = lotteries[1]
|
||||||
|
for user, account_numbers in users_data[:5]: # Первые 5 пользователей
|
||||||
|
success = await ParticipationService.add_participant(
|
||||||
|
session=session,
|
||||||
|
lottery_id=lottery2.id,
|
||||||
|
user_id=user.id
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f"✅ Пользователь {user.first_name}: добавлен в розыгрыш №2")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Пользователь {user.first_name}: уже участвует в розыгрыше №2")
|
||||||
|
|
||||||
|
async def show_statistics():
|
||||||
|
"""Показ статистики созданных данных"""
|
||||||
|
print("\n📊 Статистика созданных данных:")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Пользователи
|
||||||
|
users = await UserService.get_all_users(session)
|
||||||
|
print(f"👥 Пользователей: {len(users)}")
|
||||||
|
|
||||||
|
# Розыгрыши
|
||||||
|
lotteries = await LotteryService.get_active_lotteries(session)
|
||||||
|
print(f"🎲 Активных розыгрышей: {len(lotteries)}")
|
||||||
|
|
||||||
|
# Участия
|
||||||
|
for lottery in lotteries:
|
||||||
|
# Простой подсчет через SQL
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.count(Participation.id))
|
||||||
|
.where(Participation.lottery_id == lottery.id)
|
||||||
|
)
|
||||||
|
participation_count = result.scalar()
|
||||||
|
print(f"🎫 Розыгрыш '{lottery.title}': {participation_count} участий")
|
||||||
|
|
||||||
|
print("\n✅ Тестовые данные успешно созданы!")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Главная функция"""
|
||||||
|
print("🚀 Инициализация тестовой базы данных")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Очищаем БД
|
||||||
|
await clear_database()
|
||||||
|
|
||||||
|
# Создаем тестовых пользователей
|
||||||
|
users_data = await create_test_users()
|
||||||
|
|
||||||
|
# Создаем розыгрыши
|
||||||
|
lotteries = await create_test_lotteries(users_data)
|
||||||
|
|
||||||
|
# Заполняем участников
|
||||||
|
await populate_participants(users_data, lotteries)
|
||||||
|
|
||||||
|
# Показываем статистику
|
||||||
|
await show_statistics()
|
||||||
|
|
||||||
|
print("\n🎉 Инициализация завершена!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
191
demo_admin.py
Normal file
191
demo_admin.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
Демонстрация возможностей админ-панели
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from database import async_session_maker, init_db
|
||||||
|
from services import UserService, LotteryService
|
||||||
|
from admin_utils import AdminUtils, ReportGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def demo_admin_features():
|
||||||
|
"""Демонстрация функций админ-панели"""
|
||||||
|
print("🚀 Демонстрация возможностей админ-панели")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Создаем тестового администратора
|
||||||
|
admin = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=123456789,
|
||||||
|
username="admin",
|
||||||
|
first_name="Администратор",
|
||||||
|
last_name="Системы"
|
||||||
|
)
|
||||||
|
await UserService.set_admin(session, 123456789, True)
|
||||||
|
print(f"✅ Создан администратор: {admin.first_name}")
|
||||||
|
|
||||||
|
# Создаем тестовых пользователей
|
||||||
|
users = []
|
||||||
|
for i in range(1, 11):
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=200000000 + i,
|
||||||
|
username=f"user{i}",
|
||||||
|
first_name=f"Пользователь{i}",
|
||||||
|
last_name="Тестовый"
|
||||||
|
)
|
||||||
|
users.append(user)
|
||||||
|
print(f"✅ Создано {len(users)} тестовых пользователей")
|
||||||
|
|
||||||
|
# Создаем несколько розыгрышей
|
||||||
|
lottery1 = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="🎉 Новогодний мега-розыгрыш",
|
||||||
|
description="Грандиозный розыгрыш к Новому году с невероятными призами!",
|
||||||
|
prizes=[
|
||||||
|
"🥇 iPhone 15 Pro Max 1TB",
|
||||||
|
"🥈 MacBook Air M2 13\"",
|
||||||
|
"🥉 iPad Pro 12.9\"",
|
||||||
|
"🏆 AirPods Pro 2",
|
||||||
|
"🎁 Подарочная карта Apple 50,000₽"
|
||||||
|
],
|
||||||
|
creator_id=admin.id
|
||||||
|
)
|
||||||
|
|
||||||
|
lottery2 = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="🚗 Автомобильный розыгрыш",
|
||||||
|
description="Выиграй автомобиль своей мечты!",
|
||||||
|
prizes=[
|
||||||
|
"🥇 Tesla Model 3",
|
||||||
|
"🥈 BMW X3",
|
||||||
|
"🥉 Mercedes-Benz C-Class"
|
||||||
|
],
|
||||||
|
creator_id=admin.id
|
||||||
|
)
|
||||||
|
|
||||||
|
lottery3 = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="🏖️ Отпуск мечты",
|
||||||
|
description="Путешествие в райские места",
|
||||||
|
prizes=[
|
||||||
|
"🥇 Тур на Мальдивы на двоих",
|
||||||
|
"🥈 Неделя в Дубае",
|
||||||
|
"🥉 Тур в Турцию"
|
||||||
|
],
|
||||||
|
creator_id=admin.id
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✅ Создано 3 розыгрыша:")
|
||||||
|
print(f" - {lottery1.title}")
|
||||||
|
print(f" - {lottery2.title}")
|
||||||
|
print(f" - {lottery3.title}")
|
||||||
|
|
||||||
|
# Добавляем участников в розыгрыши
|
||||||
|
participants_added = 0
|
||||||
|
for user in users:
|
||||||
|
# В первый розыгрыш добавляем всех
|
||||||
|
if await LotteryService.add_participant(session, lottery1.id, user.id):
|
||||||
|
participants_added += 1
|
||||||
|
|
||||||
|
# Во второй - половину
|
||||||
|
if user.id % 2 == 0:
|
||||||
|
if await LotteryService.add_participant(session, lottery2.id, user.id):
|
||||||
|
participants_added += 1
|
||||||
|
|
||||||
|
# В третий - треть
|
||||||
|
if user.id % 3 == 0:
|
||||||
|
if await LotteryService.add_participant(session, lottery3.id, user.id):
|
||||||
|
participants_added += 1
|
||||||
|
|
||||||
|
print(f"✅ Добавлено {participants_added} участий")
|
||||||
|
|
||||||
|
# Устанавливаем предопределенных победителей
|
||||||
|
print("\n👑 Установка предопределенных победителей:")
|
||||||
|
|
||||||
|
# В первом розыгрыше: 1 и 3 места
|
||||||
|
await LotteryService.set_manual_winner(session, lottery1.id, 1, users[0].telegram_id)
|
||||||
|
await LotteryService.set_manual_winner(session, lottery1.id, 3, users[2].telegram_id)
|
||||||
|
print(f" 🎯 {lottery1.title}: 1 место - {users[0].first_name}, 3 место - {users[2].first_name}")
|
||||||
|
|
||||||
|
# Во втором розыгрыше: только 1 место
|
||||||
|
await LotteryService.set_manual_winner(session, lottery2.id, 1, users[1].telegram_id)
|
||||||
|
print(f" 🚗 {lottery2.title}: 1 место - {users[1].first_name}")
|
||||||
|
|
||||||
|
# Получаем статистику по розыгрышам
|
||||||
|
print(f"\n📊 Статистика по розыгрышам:")
|
||||||
|
for lottery in [lottery1, lottery2, lottery3]:
|
||||||
|
stats = await AdminUtils.get_lottery_statistics(session, lottery.id)
|
||||||
|
print(f" 🎲 {lottery.title}:")
|
||||||
|
print(f" Участников: {stats['participants_count']}")
|
||||||
|
print(f" Ручных победителей: {len(lottery.manual_winners or {})}")
|
||||||
|
|
||||||
|
# Проводим розыгрыши
|
||||||
|
print(f"\n🎲 Проведение розыгрышей:")
|
||||||
|
|
||||||
|
# Первый розыгрыш
|
||||||
|
results1 = await LotteryService.conduct_draw(session, lottery1.id)
|
||||||
|
print(f" 🎉 {lottery1.title} - результаты:")
|
||||||
|
for place, winner_info in results1.items():
|
||||||
|
user = winner_info['user']
|
||||||
|
manual = " 👑" if winner_info['is_manual'] else " 🎲"
|
||||||
|
print(f" {place}. {user.first_name}{manual}")
|
||||||
|
|
||||||
|
# Второй розыгрыш
|
||||||
|
results2 = await LotteryService.conduct_draw(session, lottery2.id)
|
||||||
|
print(f" 🚗 {lottery2.title} - результаты:")
|
||||||
|
for place, winner_info in results2.items():
|
||||||
|
user = winner_info['user']
|
||||||
|
manual = " 👑" if winner_info['is_manual'] else " 🎲"
|
||||||
|
print(f" {place}. {user.first_name}{manual}")
|
||||||
|
|
||||||
|
# Генерируем отчет
|
||||||
|
print(f"\n📋 Генерация отчета:")
|
||||||
|
report = await ReportGenerator.generate_summary_report(session)
|
||||||
|
print(report)
|
||||||
|
|
||||||
|
# Экспорт данных
|
||||||
|
print(f"💾 Экспорт данных первого розыгрыша:")
|
||||||
|
export_data = await AdminUtils.export_lottery_data(session, lottery1.id)
|
||||||
|
print(f" ✅ Экспортировано:")
|
||||||
|
print(f" - Розыгрыш: {export_data['lottery']['title']}")
|
||||||
|
print(f" - Участников: {len(export_data['participants'])}")
|
||||||
|
print(f" - Победителей: {len(export_data['winners'])}")
|
||||||
|
|
||||||
|
# Активность пользователя
|
||||||
|
print(f"\n👤 Активность пользователя {users[0].first_name}:")
|
||||||
|
activity = await AdminUtils.get_user_activity(session, users[0].telegram_id)
|
||||||
|
print(f" 📊 Статистика:")
|
||||||
|
print(f" Участий: {activity['total_participations']}")
|
||||||
|
print(f" Побед: {activity['total_wins']}")
|
||||||
|
|
||||||
|
print(f"\n" + "=" * 50)
|
||||||
|
print(f"✅ Демонстрация завершена!")
|
||||||
|
print(f"")
|
||||||
|
print(f"🎯 Что показано:")
|
||||||
|
print(f" ✅ Создание пользователей и розыгрышей")
|
||||||
|
print(f" ✅ Добавление участников")
|
||||||
|
print(f" ✅ Установка предопределенных победителей")
|
||||||
|
print(f" ✅ Проведение розыгрышей с ручными победителями")
|
||||||
|
print(f" ✅ Генерация статистики и отчетов")
|
||||||
|
print(f" ✅ Экспорт данных")
|
||||||
|
print(f" ✅ Анализ активности пользователей")
|
||||||
|
print(f"")
|
||||||
|
print(f"🚀 Админ-панель готова к использованию!")
|
||||||
|
|
||||||
|
# Показываем ключевую особенность
|
||||||
|
print(f"\n🎯 КЛЮЧЕВАЯ ОСОБЕННОСТЬ:")
|
||||||
|
print(f"В первом розыгрыше мы заранее установили:")
|
||||||
|
print(f" 👑 1 место: {users[0].first_name}")
|
||||||
|
print(f" 👑 3 место: {users[2].first_name}")
|
||||||
|
print(f"")
|
||||||
|
print(f"При розыгрыше эти пользователи автоматически заняли свои места,")
|
||||||
|
print(f"а остальные места (2, 4, 5) были разыграны случайно!")
|
||||||
|
print(f"")
|
||||||
|
print(f"🎭 Никто из участников не знает о подстройке!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(demo_admin_features())
|
||||||
164
examples.py
Normal file
164
examples.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"""
|
||||||
|
Примеры использования API сервисов для тестирования
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from database import async_session_maker, init_db
|
||||||
|
from services import UserService, LotteryService, ParticipationService
|
||||||
|
|
||||||
|
|
||||||
|
async def example_usage():
|
||||||
|
"""Пример использования всех основных функций бота"""
|
||||||
|
|
||||||
|
print("🔄 Инициализация базы данных...")
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
print("\n👤 Создание пользователей...")
|
||||||
|
|
||||||
|
# Создаем администратора
|
||||||
|
admin = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=123456789,
|
||||||
|
username="admin_user",
|
||||||
|
first_name="Администратор",
|
||||||
|
last_name="Бота"
|
||||||
|
)
|
||||||
|
await UserService.set_admin(session, 123456789, True)
|
||||||
|
print(f"✅ Создан админ: {admin.first_name} (@{admin.username})")
|
||||||
|
|
||||||
|
# Создаем обычных пользователей
|
||||||
|
users = []
|
||||||
|
for i in range(1, 6):
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=100000000 + i,
|
||||||
|
username=f"user{i}",
|
||||||
|
first_name=f"Пользователь{i}",
|
||||||
|
last_name="Тестовый"
|
||||||
|
)
|
||||||
|
users.append(user)
|
||||||
|
print(f"✅ Создан пользователь: {user.first_name}")
|
||||||
|
|
||||||
|
print(f"\n🎲 Создание розыгрыша...")
|
||||||
|
|
||||||
|
# Создаем розыгрыш
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="🎉 Новогодний розыгрыш 2025",
|
||||||
|
description="Грандиозный новогодний розыгрыш с крутыми призами!",
|
||||||
|
prizes=[
|
||||||
|
"🥇 iPhone 15 Pro Max",
|
||||||
|
"🥈 MacBook Air M2",
|
||||||
|
"🥉 AirPods Pro",
|
||||||
|
"🏆 PlayStation 5",
|
||||||
|
"🎁 Подарочный сертификат 10,000₽"
|
||||||
|
],
|
||||||
|
creator_id=admin.id
|
||||||
|
)
|
||||||
|
print(f"✅ Создан розыгрыш: {lottery.title}")
|
||||||
|
|
||||||
|
print(f"\n🎫 Регистрация участников...")
|
||||||
|
|
||||||
|
# Добавляем участников
|
||||||
|
for user in users:
|
||||||
|
success = await LotteryService.add_participant(session, lottery.id, user.id)
|
||||||
|
if success:
|
||||||
|
print(f"✅ {user.first_name} присоединился к розыгрышу")
|
||||||
|
|
||||||
|
print(f"\n👑 Установка ручного победителя...")
|
||||||
|
|
||||||
|
# Устанавливаем ручного победителя на 1 место
|
||||||
|
manual_winner = users[0] # Первый пользователь
|
||||||
|
success = await LotteryService.set_manual_winner(
|
||||||
|
session, lottery.id, 1, manual_winner.telegram_id
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f"✅ {manual_winner.first_name} установлен как победитель 1 места")
|
||||||
|
|
||||||
|
# Устанавливаем еще одного ручного победителя на 3 место
|
||||||
|
manual_winner2 = users[2] # Третий пользователь
|
||||||
|
success = await LotteryService.set_manual_winner(
|
||||||
|
session, lottery.id, 3, manual_winner2.telegram_id
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f"✅ {manual_winner2.first_name} установлен как победитель 3 места")
|
||||||
|
|
||||||
|
print(f"\n📊 Статистика перед розыгрышем...")
|
||||||
|
participants_count = await ParticipationService.get_participants_count(
|
||||||
|
session, lottery.id
|
||||||
|
)
|
||||||
|
print(f"👥 Участников в розыгрыше: {participants_count}")
|
||||||
|
|
||||||
|
print(f"\n🎲 Проведение розыгрыша...")
|
||||||
|
|
||||||
|
# Проводим розыгрыш
|
||||||
|
results = await LotteryService.conduct_draw(session, lottery.id)
|
||||||
|
|
||||||
|
print(f"\n🏆 Результаты розыгрыша:")
|
||||||
|
print(f"🎯 Розыгрыш: {lottery.title}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
for place, winner_info in results.items():
|
||||||
|
user = winner_info['user']
|
||||||
|
prize = winner_info['prize']
|
||||||
|
is_manual = winner_info['is_manual']
|
||||||
|
|
||||||
|
manual_indicator = "👑 (установлен вручную)" if is_manual else "🎲 (случайный)"
|
||||||
|
print(f"{place}. {user.first_name} (@{user.username}) {manual_indicator}")
|
||||||
|
print(f" 🎁 {prize}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print("✅ Розыгрыш завершен успешно!")
|
||||||
|
|
||||||
|
print(f"\n📈 Финальная статистика...")
|
||||||
|
|
||||||
|
# Получаем победителей из базы данных
|
||||||
|
winners = await LotteryService.get_winners(session, lottery.id)
|
||||||
|
print(f"🏆 Сохранено победителей в базе: {len(winners)}")
|
||||||
|
|
||||||
|
# Проверяем участия пользователей
|
||||||
|
for user in users[:2]: # Проверяем первых двух
|
||||||
|
participations = await ParticipationService.get_user_participations(
|
||||||
|
session, user.id
|
||||||
|
)
|
||||||
|
print(f"📝 {user.first_name} участвует в {len(participations)} розыгрышах")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_edge_cases():
|
||||||
|
"""Тестирование граничных случаев"""
|
||||||
|
print("\n🧪 Тестирование граничных случаев...")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Попытка добавить участника дважды
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, 100000001)
|
||||||
|
lottery = (await LotteryService.get_active_lotteries(session))[0]
|
||||||
|
|
||||||
|
success = await LotteryService.add_participant(session, lottery.id, user.id)
|
||||||
|
print(f"Повторное добавление участника: {'❌ Заблокировано' if not success else '⚠️ Разрешено'}")
|
||||||
|
|
||||||
|
# Попытка установить ручного победителя для несуществующего пользователя
|
||||||
|
success = await LotteryService.set_manual_winner(session, lottery.id, 10, 999999999)
|
||||||
|
print(f"Установка несуществующего победителя: {'❌ Заблокировано' if not success else '⚠️ Разрешено'}")
|
||||||
|
|
||||||
|
# Попытка провести розыгрыш завершенной лотереи
|
||||||
|
results = await LotteryService.conduct_draw(session, lottery.id)
|
||||||
|
print(f"Повторный розыгрыш: {'❌ Заблокирован' if not results else '⚠️ Разрешен'}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🚀 Запуск примера использования бота для розыгрышей")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(example_usage())
|
||||||
|
asyncio.run(test_edge_cases())
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("✅ Все тесты выполнены успешно!")
|
||||||
|
print("🎉 Бот готов к использованию!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Ошибка во время выполнения: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
732
main.py
Normal file
732
main.py
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
from aiogram import Bot, Dispatcher, Router, F
|
||||||
|
from aiogram.types import (
|
||||||
|
Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup,
|
||||||
|
BotCommand
|
||||||
|
)
|
||||||
|
from aiogram.filters import Command, StateFilter
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from config import BOT_TOKEN, ADMIN_IDS
|
||||||
|
from database import async_session_maker, init_db
|
||||||
|
from services import UserService, LotteryService, ParticipationService
|
||||||
|
from admin_panel import admin_router
|
||||||
|
from async_decorators import (
|
||||||
|
async_user_action, admin_async_action, db_operation,
|
||||||
|
TaskManagerMiddleware, shutdown_task_manager,
|
||||||
|
format_task_stats, TaskPriority
|
||||||
|
)
|
||||||
|
from account_utils import validate_account_number, format_account_number
|
||||||
|
from winner_display import format_winner_display
|
||||||
|
|
||||||
|
|
||||||
|
# Настройка логирования
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Состояния для FSM
|
||||||
|
class CreateLotteryStates(StatesGroup):
|
||||||
|
waiting_for_title = State()
|
||||||
|
waiting_for_description = State()
|
||||||
|
waiting_for_prizes = State()
|
||||||
|
|
||||||
|
class SetWinnerStates(StatesGroup):
|
||||||
|
waiting_for_lottery_id = State()
|
||||||
|
waiting_for_place = State()
|
||||||
|
waiting_for_user_id = State()
|
||||||
|
|
||||||
|
class AccountStates(StatesGroup):
|
||||||
|
waiting_for_account_number = State()
|
||||||
|
|
||||||
|
|
||||||
|
# Инициализация бота
|
||||||
|
bot = Bot(token=BOT_TOKEN)
|
||||||
|
storage = MemoryStorage()
|
||||||
|
dp = Dispatcher(storage=storage)
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
# Подключаем middleware для управления задачами
|
||||||
|
dp.message.middleware(TaskManagerMiddleware())
|
||||||
|
dp.callback_query.middleware(TaskManagerMiddleware())
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin(user_id: int) -> bool:
|
||||||
|
"""Проверка, является ли пользователь администратором"""
|
||||||
|
return user_id in ADMIN_IDS
|
||||||
|
|
||||||
|
|
||||||
|
def get_main_keyboard(is_admin_user: bool = False) -> InlineKeyboardMarkup:
|
||||||
|
"""Главная клавиатура"""
|
||||||
|
buttons = [
|
||||||
|
[InlineKeyboardButton(text="🎲 Активные розыгрыши", callback_data="list_lotteries")],
|
||||||
|
[InlineKeyboardButton(text="📝 Мои участия", callback_data="my_participations")],
|
||||||
|
[InlineKeyboardButton(text="💳 Мой счёт", callback_data="my_account")]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is_admin_user:
|
||||||
|
buttons.extend([
|
||||||
|
[InlineKeyboardButton(text="🔧 Админ-панель", callback_data="admin_panel")],
|
||||||
|
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")],
|
||||||
|
[InlineKeyboardButton(text="👑 Установить победителя", callback_data="set_winner")],
|
||||||
|
[InlineKeyboardButton(text="📊 Статистика задач", callback_data="task_stats")]
|
||||||
|
])
|
||||||
|
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("start"))
|
||||||
|
async def cmd_start(message: Message):
|
||||||
|
"""Обработчик команды /start"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=message.from_user.id,
|
||||||
|
username=message.from_user.username,
|
||||||
|
first_name=message.from_user.first_name,
|
||||||
|
last_name=message.from_user.last_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Устанавливаем права администратора, если пользователь в списке
|
||||||
|
if message.from_user.id in ADMIN_IDS:
|
||||||
|
await UserService.set_admin(session, message.from_user.id, True)
|
||||||
|
|
||||||
|
is_admin_user = is_admin(message.from_user.id)
|
||||||
|
|
||||||
|
welcome_text = f"Добро пожаловать, {message.from_user.first_name}! 🎉\n\n"
|
||||||
|
welcome_text += "Это бот для проведения розыгрышей.\n\n"
|
||||||
|
welcome_text += "Выберите действие из меню ниже:"
|
||||||
|
|
||||||
|
if is_admin_user:
|
||||||
|
welcome_text += "\n\n👑 У вас есть права администратора!"
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
welcome_text,
|
||||||
|
reply_markup=get_main_keyboard(is_admin_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "list_lotteries")
|
||||||
|
async def show_active_lotteries(callback: CallbackQuery):
|
||||||
|
"""Показать активные розыгрыши"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lotteries = await LotteryService.get_active_lotteries(session)
|
||||||
|
|
||||||
|
if not lotteries:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"🔍 Активных розыгрышей нет",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "🎲 Активные розыгрыши:\n\n"
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
for lottery in lotteries:
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
participants_count = await ParticipationService.get_participants_count(
|
||||||
|
session, lottery.id
|
||||||
|
)
|
||||||
|
|
||||||
|
text += f"🎯 {lottery.title}\n"
|
||||||
|
text += f"👥 Участников: {participants_count}\n"
|
||||||
|
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||||
|
|
||||||
|
buttons.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=f"🎲 {lottery.title}",
|
||||||
|
callback_data=f"lottery_{lottery.id}"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("lottery_"))
|
||||||
|
async def show_lottery_details(callback: CallbackQuery):
|
||||||
|
"""Показать детали розыгрыша"""
|
||||||
|
lottery_id = int(callback.data.split("_")[1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||||
|
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
|
||||||
|
|
||||||
|
# Проверяем, участвует ли пользователь
|
||||||
|
is_participating = any(
|
||||||
|
p.user_id == user.id for p in lottery.participations
|
||||||
|
) if user else False
|
||||||
|
|
||||||
|
text = f"🎯 {lottery.title}\n\n"
|
||||||
|
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
|
||||||
|
|
||||||
|
if lottery.prizes:
|
||||||
|
text += "🏆 Призы:\n"
|
||||||
|
for i, prize in enumerate(lottery.prizes, 1):
|
||||||
|
text += f"{i}. {prize}\n"
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
text += f"👥 Участников: {participants_count}\n"
|
||||||
|
text += f"📅 Создан: {lottery.created_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||||
|
|
||||||
|
if lottery.is_completed:
|
||||||
|
text += "\n✅ Розыгрыш завершен"
|
||||||
|
# Показываем победителей
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
winners = await LotteryService.get_winners(session, lottery_id)
|
||||||
|
|
||||||
|
if winners:
|
||||||
|
text += "\n\n🏆 Победители:\n"
|
||||||
|
for winner in winners:
|
||||||
|
# Используем новую систему отображения
|
||||||
|
winner_display = format_winner_display(winner.user, lottery, show_sensitive_data=False)
|
||||||
|
text += f"{winner.place}. {winner_display}\n"
|
||||||
|
else:
|
||||||
|
text += f"\n🟢 Статус: {'Активен' if lottery.is_active else 'Неактивен'}"
|
||||||
|
if is_participating:
|
||||||
|
text += "\n✅ Вы участвуете в розыгрыше"
|
||||||
|
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
if not lottery.is_completed and lottery.is_active and not is_participating:
|
||||||
|
buttons.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🎫 Участвовать",
|
||||||
|
callback_data=f"join_{lottery_id}"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
if is_admin(callback.from_user.id) and not lottery.is_completed:
|
||||||
|
buttons.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text="🎲 Провести розыгрыш",
|
||||||
|
callback_data=f"conduct_{lottery_id}"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="list_lotteries")])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("join_"))
|
||||||
|
async def join_lottery(callback: CallbackQuery):
|
||||||
|
"""Присоединиться к розыгрышу"""
|
||||||
|
lottery_id = int(callback.data.split("_")[1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||||
|
if not user:
|
||||||
|
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await LotteryService.add_participant(session, lottery_id, user.id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await callback.answer("✅ Вы успешно присоединились к розыгрышу!", show_alert=True)
|
||||||
|
else:
|
||||||
|
await callback.answer("❌ Вы уже участвуете в этом розыгрыше", show_alert=True)
|
||||||
|
|
||||||
|
# Обновляем информацию о розыгрыше
|
||||||
|
await show_lottery_details(callback)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("conduct_"))
|
||||||
|
async def conduct_lottery(callback: CallbackQuery):
|
||||||
|
"""Провести розыгрыш"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery_id = int(callback.data.split("_")[1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
results = await LotteryService.conduct_draw(session, lottery_id)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
await callback.answer("❌ Не удалось провести розыгрыш", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "🎉 Розыгрыш завершен!\n\n🏆 Победители:\n\n"
|
||||||
|
|
||||||
|
for place, winner_info in results.items():
|
||||||
|
user = winner_info['user']
|
||||||
|
prize = winner_info['prize']
|
||||||
|
|
||||||
|
# Используем новую систему отображения
|
||||||
|
winner_display = format_winner_display(user, lottery, show_sensitive_data=False)
|
||||||
|
text += f"{place}. {winner_display}\n"
|
||||||
|
text += f" 🎁 {prize}\n\n"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 К розыгрышам", callback_data="list_lotteries")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Создание розыгрыша
|
||||||
|
@router.callback_query(F.data == "create_lottery")
|
||||||
|
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Начать создание розыгрыша"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"📝 Создание нового розыгрыша\n\n"
|
||||||
|
"Введите название розыгрыша:",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.set_state(CreateLotteryStates.waiting_for_title)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(CreateLotteryStates.waiting_for_title))
|
||||||
|
async def process_lottery_title(message: Message, state: FSMContext):
|
||||||
|
"""Обработка названия розыгрыша"""
|
||||||
|
await state.update_data(title=message.text)
|
||||||
|
await message.answer(
|
||||||
|
"📋 Введите описание розыгрыша (или отправьте '-' для пропуска):"
|
||||||
|
)
|
||||||
|
await state.set_state(CreateLotteryStates.waiting_for_description)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(CreateLotteryStates.waiting_for_description))
|
||||||
|
async def process_lottery_description(message: Message, state: FSMContext):
|
||||||
|
"""Обработка описания розыгрыша"""
|
||||||
|
description = None if message.text == "-" else message.text
|
||||||
|
await state.update_data(description=description)
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
"🏆 Введите призы через новую строку:\n\n"
|
||||||
|
"Пример:\n"
|
||||||
|
"1000 рублей\n"
|
||||||
|
"iPhone 15\n"
|
||||||
|
"Подарочный сертификат"
|
||||||
|
)
|
||||||
|
await state.set_state(CreateLotteryStates.waiting_for_prizes)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(CreateLotteryStates.waiting_for_prizes))
|
||||||
|
async def process_lottery_prizes(message: Message, state: FSMContext):
|
||||||
|
"""Обработка призов розыгрыша"""
|
||||||
|
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title=data['title'],
|
||||||
|
description=data['description'],
|
||||||
|
prizes=prizes,
|
||||||
|
creator_id=user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
text = f"✅ Розыгрыш успешно создан!\n\n"
|
||||||
|
text += f"🎯 Название: {lottery.title}\n"
|
||||||
|
text += f"📋 Описание: {lottery.description or 'Не указано'}\n\n"
|
||||||
|
text += f"🏆 Призы:\n"
|
||||||
|
for i, prize in enumerate(prizes, 1):
|
||||||
|
text += f"{i}. {prize}\n"
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
text,
|
||||||
|
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Установка ручного победителя
|
||||||
|
@router.callback_query(F.data == "set_winner")
|
||||||
|
async def start_set_winner(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Начать установку ручного победителя"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lotteries = await LotteryService.get_active_lotteries(session)
|
||||||
|
|
||||||
|
if not lotteries:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"❌ Нет активных розыгрышей",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "👑 Установка ручного победителя\n\n"
|
||||||
|
text += "Выберите розыгрыш:\n\n"
|
||||||
|
|
||||||
|
buttons = []
|
||||||
|
for lottery in lotteries:
|
||||||
|
text += f"🎯 {lottery.title} (ID: {lottery.id})\n"
|
||||||
|
buttons.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=f"{lottery.title}",
|
||||||
|
callback_data=f"setwinner_{lottery.id}"
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("setwinner_"))
|
||||||
|
async def select_winner_place(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Выбор места для ручного победителя"""
|
||||||
|
lottery_id = int(callback.data.split("_")[1])
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
|
||||||
|
if not lottery:
|
||||||
|
await callback.answer("Розыгрыш не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(lottery_id=lottery_id)
|
||||||
|
|
||||||
|
num_prizes = len(lottery.prizes) if lottery.prizes else 3
|
||||||
|
text = f"👑 Установка ручного победителя для розыгрыша:\n"
|
||||||
|
text += f"🎯 {lottery.title}\n\n"
|
||||||
|
text += f"Введите номер места (1-{num_prizes}):"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="set_winner")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
await state.set_state(SetWinnerStates.waiting_for_place)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(SetWinnerStates.waiting_for_place))
|
||||||
|
async def process_winner_place(message: Message, state: FSMContext):
|
||||||
|
"""Обработка места победителя"""
|
||||||
|
try:
|
||||||
|
place = int(message.text)
|
||||||
|
if place < 1:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
await message.answer("❌ Введите корректный номер места (положительное число)")
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.update_data(place=place)
|
||||||
|
await message.answer(
|
||||||
|
f"👑 Установка ручного победителя на {place} место\n\n"
|
||||||
|
"Введите Telegram ID пользователя:"
|
||||||
|
)
|
||||||
|
await state.set_state(SetWinnerStates.waiting_for_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(SetWinnerStates.waiting_for_user_id))
|
||||||
|
async def process_winner_user_id(message: Message, state: FSMContext):
|
||||||
|
"""Обработка ID пользователя-победителя"""
|
||||||
|
try:
|
||||||
|
telegram_id = int(message.text)
|
||||||
|
except ValueError:
|
||||||
|
await message.answer("❌ Введите корректный Telegram ID (число)")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = await state.get_data()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
success = await LotteryService.set_manual_winner(
|
||||||
|
session,
|
||||||
|
data['lottery_id'],
|
||||||
|
data['place'],
|
||||||
|
telegram_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await message.answer(
|
||||||
|
f"✅ Ручной победитель установлен!\n\n"
|
||||||
|
f"🏆 Место: {data['place']}\n"
|
||||||
|
f"👤 Telegram ID: {telegram_id}",
|
||||||
|
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
"❌ Не удалось установить ручного победителя.\n"
|
||||||
|
"Проверьте, что пользователь существует в системе.",
|
||||||
|
reply_markup=get_main_keyboard(is_admin(message.from_user.id))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "my_participations")
|
||||||
|
async def show_my_participations(callback: CallbackQuery):
|
||||||
|
"""Показать участие пользователя в розыгрышах"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||||
|
if not user:
|
||||||
|
await callback.answer("Ошибка получения данных пользователя", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
participations = await ParticipationService.get_user_participations(session, user.id)
|
||||||
|
|
||||||
|
if not participations:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"📝 Вы пока не участвуете в розыгрышах",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "📝 Ваши участия в розыгрышах:\n\n"
|
||||||
|
|
||||||
|
for participation in participations:
|
||||||
|
lottery = participation.lottery
|
||||||
|
status = "✅ Завершен" if lottery.is_completed else "🟢 Активен"
|
||||||
|
text += f"🎯 {lottery.title}\n"
|
||||||
|
text += f"📊 Статус: {status}\n"
|
||||||
|
text += f"📅 Участие с: {participation.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Хэндлеры для работы с номерами счетов
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "my_account")
|
||||||
|
@db_operation()
|
||||||
|
async def show_my_account(callback: CallbackQuery):
|
||||||
|
"""Показать информацию о счёте пользователя"""
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
await callback.answer("Пользователь не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "💳 **Ваш клиентский счёт**\n\n"
|
||||||
|
|
||||||
|
if user.account_number:
|
||||||
|
# Показываем маскированный номер для безопасности
|
||||||
|
from account_utils import mask_account_number
|
||||||
|
masked = mask_account_number(user.account_number, show_last_digits=6)
|
||||||
|
text += f"📋 Номер счёта: `{masked}`\n"
|
||||||
|
text += f"✅ Статус: Активен\n\n"
|
||||||
|
text += "ℹ️ Счёт используется для идентификации в розыгрышах"
|
||||||
|
else:
|
||||||
|
text += "❌ Счёт не привязан\n\n"
|
||||||
|
text += "Привяжите счёт для участия в розыгрышах"
|
||||||
|
|
||||||
|
buttons = []
|
||||||
|
|
||||||
|
if user.account_number:
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔄 Изменить счёт", callback_data="change_account")])
|
||||||
|
else:
|
||||||
|
buttons.append([InlineKeyboardButton(text="➕ Привязать счёт", callback_data="add_account")])
|
||||||
|
|
||||||
|
buttons.append([InlineKeyboardButton(text="🔙 Главное меню", callback_data="back_to_main")])
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.in_(["add_account", "change_account"]))
|
||||||
|
@db_operation()
|
||||||
|
async def start_account_setup(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Начало процесса привязки/изменения счёта"""
|
||||||
|
await state.set_state(AccountStates.waiting_for_account_number)
|
||||||
|
|
||||||
|
action = "привязки" if callback.data == "add_account" else "изменения"
|
||||||
|
|
||||||
|
text = f"💳 **Процедура {action} счёта**\n\n"
|
||||||
|
text += "Введите номер вашего клиентского счёта в формате:\n"
|
||||||
|
text += "`12-34-56-78-90-12-34-56`\n\n"
|
||||||
|
text += "📝 **Требования:**\n"
|
||||||
|
text += "• Ровно 16 цифр\n"
|
||||||
|
text += "• Разделены дефисами через каждые 2 цифры\n"
|
||||||
|
text += "• Номер должен быть уникальным\n\n"
|
||||||
|
text += "✉️ Отправьте номер счёта в ответном сообщении"
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="my_account")]
|
||||||
|
]),
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(StateFilter(AccountStates.waiting_for_account_number))
|
||||||
|
@db_operation()
|
||||||
|
async def process_account_number(message: Message, state: FSMContext):
|
||||||
|
"""Обработка введённого номера счёта"""
|
||||||
|
account_input = message.text.strip()
|
||||||
|
|
||||||
|
# Форматируем и валидируем номер
|
||||||
|
formatted_number = format_account_number(account_input)
|
||||||
|
|
||||||
|
if not formatted_number:
|
||||||
|
await message.answer(
|
||||||
|
"❌ **Некорректный формат номера счёта**\n\n"
|
||||||
|
"Номер должен содержать ровно 16 цифр.\n"
|
||||||
|
"Пример правильного формата: `12-34-56-78-90-12-34-56`\n\n"
|
||||||
|
"Попробуйте ещё раз:",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Проверяем уникальность
|
||||||
|
existing_user = await UserService.get_user_by_account(session, formatted_number)
|
||||||
|
if existing_user and existing_user.telegram_id != message.from_user.id:
|
||||||
|
await message.answer(
|
||||||
|
"❌ **Номер счёта уже используется**\n\n"
|
||||||
|
"Данный номер счёта уже привязан к другому пользователю.\n"
|
||||||
|
"Убедитесь, что вы вводите правильный номер.\n\n"
|
||||||
|
"Попробуйте ещё раз:"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновляем номер счёта
|
||||||
|
success = await UserService.set_account_number(
|
||||||
|
session, message.from_user.id, formatted_number
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
await state.clear()
|
||||||
|
await message.answer(
|
||||||
|
f"✅ **Счёт успешно привязан!**\n\n"
|
||||||
|
f"💳 Номер счёта: `{formatted_number}`\n\n"
|
||||||
|
f"Теперь вы можете участвовать в розыгрышах.\n"
|
||||||
|
f"Ваш номер счёта будет использоваться для идентификации.",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="back_to_main")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.answer(
|
||||||
|
"❌ **Ошибка привязки счёта**\n\n"
|
||||||
|
"Произошла ошибка при сохранении номера счёта.\n"
|
||||||
|
"Попробуйте ещё раз или обратитесь к администратору.",
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="my_account")]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "task_stats")
|
||||||
|
@admin_async_action()
|
||||||
|
async def show_task_stats(callback: CallbackQuery):
|
||||||
|
"""Показать статистику задач (только для админов)"""
|
||||||
|
if not is_admin(callback.from_user.id):
|
||||||
|
await callback.answer("Доступ запрещён", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
stats_text = await format_task_stats()
|
||||||
|
|
||||||
|
await callback.message.edit_text(
|
||||||
|
stats_text,
|
||||||
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔄 Обновить", callback_data="task_stats")],
|
||||||
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
]),
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "back_to_main")
|
||||||
|
async def back_to_main(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Вернуться в главное меню"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
is_admin_user = is_admin(callback.from_user.id)
|
||||||
|
await callback.message.edit_text(
|
||||||
|
"🏠 Главное меню\n\nВыберите действие:",
|
||||||
|
reply_markup=get_main_keyboard(is_admin_user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def set_commands():
|
||||||
|
"""Установка команд бота"""
|
||||||
|
commands = [
|
||||||
|
BotCommand(command="start", description="🚀 Запустить бота"),
|
||||||
|
]
|
||||||
|
await bot.set_my_commands(commands)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Главная функция"""
|
||||||
|
# Инициализация базы данных
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
# Установка команд
|
||||||
|
await set_commands()
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
dp.include_router(router)
|
||||||
|
dp.include_router(admin_router)
|
||||||
|
|
||||||
|
# Обработка сигналов для graceful shutdown
|
||||||
|
def signal_handler():
|
||||||
|
logger.info("Получен сигнал завершения, остановка бота...")
|
||||||
|
asyncio.create_task(shutdown_task_manager())
|
||||||
|
|
||||||
|
# Настройка обработчиков сигналов
|
||||||
|
if sys.platform != "win32":
|
||||||
|
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||||
|
asyncio.get_event_loop().add_signal_handler(sig, signal_handler)
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
logger.info("Бот запущен")
|
||||||
|
try:
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
finally:
|
||||||
|
# Остановка менеджера задач при завершении
|
||||||
|
await shutdown_task_manager()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Бот остановлен пользователем")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Критическая ошибка: {e}")
|
||||||
|
finally:
|
||||||
|
logger.info("Завершение работы")
|
||||||
94
migrations/env.py
Normal file
94
migrations/env.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import asyncio
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import pool
|
||||||
|
from sqlalchemy.engine import Connection
|
||||||
|
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||||
|
from alembic import context
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Добавляем корневую директорию проекта в sys.path
|
||||||
|
ROOT_PATH = Path(__file__).parent.parent
|
||||||
|
sys.path.append(str(ROOT_PATH))
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
from models import Base
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# Импортируем настройки базы данных
|
||||||
|
from database import DATABASE_URL
|
||||||
|
|
||||||
|
# Обновляем URL базы данных из переменных окружения
|
||||||
|
config.set_main_option("sqlalchemy.url", DATABASE_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def do_run_migrations(connection: Connection) -> None:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_async_migrations() -> None:
|
||||||
|
"""In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
connectable = async_engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with connectable.connect() as connection:
|
||||||
|
await connection.run_sync(do_run_migrations)
|
||||||
|
|
||||||
|
await connectable.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode."""
|
||||||
|
|
||||||
|
asyncio.run(run_async_migrations())
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
82
migrations/versions/001_initial_migration.py
Normal file
82
migrations/versions/001_initial_migration.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Initial migration - Create lottery bot tables
|
||||||
|
|
||||||
|
Revision ID: 001
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-12-11 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import sqlite
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '001'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('telegram_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('first_name', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('last_name', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_admin', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_users_telegram_id'), 'users', ['telegram_id'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('lotteries',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('start_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('end_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('is_completed', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('prizes', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('creator_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('manual_winners', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('draw_results', sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['creator_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table('participations',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('lottery_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['lottery_id'], ['lotteries.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table('winners',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('lottery_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('place', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('prize', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('is_manual', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['lottery_id'], ['lotteries.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('winners')
|
||||||
|
op.drop_table('participations')
|
||||||
|
op.drop_table('lotteries')
|
||||||
|
op.drop_index(op.f('ix_users_telegram_id'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Add account numbers and winner display type
|
||||||
|
|
||||||
|
Revision ID: 002_add_account_numbers_and_display_type
|
||||||
|
Revises: 001_initial_migration
|
||||||
|
Create Date: 2025-11-12 06:57:40.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '002_add_account_numbers_and_display_type'
|
||||||
|
down_revision = '001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
"""Добавить поля для клиентских счетов и типа отображения победителей"""
|
||||||
|
|
||||||
|
# Добавить поле account_number в таблицу users
|
||||||
|
op.add_column('users', sa.Column('account_number', sa.String(length=23), nullable=True))
|
||||||
|
|
||||||
|
# Создать индекс для account_number для быстрого поиска
|
||||||
|
op.create_index(op.f('ix_users_account_number'), 'users', ['account_number'], unique=True)
|
||||||
|
|
||||||
|
# Добавить поле winner_display_type в таблицу lotteries
|
||||||
|
op.add_column('lotteries', sa.Column('winner_display_type', sa.String(length=20), nullable=True))
|
||||||
|
|
||||||
|
# Установить значения по умолчанию для существующих записей
|
||||||
|
op.execute("UPDATE lotteries SET winner_display_type = 'username' WHERE winner_display_type IS NULL")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
"""Удалить добавленные поля"""
|
||||||
|
|
||||||
|
# Удалить поле winner_display_type из таблицы lotteries
|
||||||
|
op.drop_column('lotteries', 'winner_display_type')
|
||||||
|
|
||||||
|
# Удалить индекс для account_number
|
||||||
|
op.drop_index(op.f('ix_users_account_number'), table_name='users')
|
||||||
|
|
||||||
|
# Удалить поле account_number из таблицы users
|
||||||
|
op.drop_column('users', 'account_number')
|
||||||
92
models.py
Normal file
92
models.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from datetime import datetime
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""Модель пользователя"""
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
telegram_id = Column(Integer, unique=True, nullable=False, index=True)
|
||||||
|
username = Column(String(255))
|
||||||
|
first_name = Column(String(255))
|
||||||
|
last_name = Column(String(255))
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
is_admin = Column(Boolean, default=False)
|
||||||
|
# Клиентский счет в формате: XX-XX-XX-XX-XX-XX-XX-XX (8 пар цифр через дефис)
|
||||||
|
account_number = Column(String(23), unique=True, nullable=True, index=True)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
participations = relationship("Participation", back_populates="user")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<User(telegram_id={self.telegram_id}, username={self.username})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Lottery(Base):
|
||||||
|
"""Модель розыгрыша"""
|
||||||
|
__tablename__ = "lotteries"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
title = Column(String(500), nullable=False)
|
||||||
|
description = Column(Text)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
start_date = Column(DateTime)
|
||||||
|
end_date = Column(DateTime)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
is_completed = Column(Boolean, default=False)
|
||||||
|
prizes = Column(JSON) # Список призов в формате JSON
|
||||||
|
creator_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
|
||||||
|
# Настройки для ручного управления победителями
|
||||||
|
manual_winners = Column(JSON, default=lambda: {}) # {место: telegram_id}
|
||||||
|
draw_results = Column(JSON) # Результаты розыгрыша
|
||||||
|
|
||||||
|
# Тип отображения победителей: "username", "chat_id", "account_number"
|
||||||
|
winner_display_type = Column(String(20), default="username")
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
creator = relationship("User")
|
||||||
|
participations = relationship("Participation", back_populates="lottery")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Lottery(id={self.id}, title={self.title})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Participation(Base):
|
||||||
|
"""Модель участия в розыгрыше"""
|
||||||
|
__tablename__ = "participations"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
user = relationship("User", back_populates="participations")
|
||||||
|
lottery = relationship("Lottery", back_populates="participations")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Participation(user_id={self.user_id}, lottery_id={self.lottery_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class Winner(Base):
|
||||||
|
"""Модель победителя розыгрыша"""
|
||||||
|
__tablename__ = "winners"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
place = Column(Integer, nullable=False) # Место (1, 2, 3...)
|
||||||
|
prize = Column(String(500)) # Описание приза
|
||||||
|
is_manual = Column(Boolean, default=False) # Был ли установлен вручную
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
# Связи
|
||||||
|
user = relationship("User")
|
||||||
|
lottery = relationship("Lottery")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"
|
||||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
aiogram==3.1.1
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
alembic==1.8.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
asyncpg==0.28.0
|
||||||
|
aiosqlite==0.17.0
|
||||||
630
services.py
Normal file
630
services.py
Normal file
@@ -0,0 +1,630 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, update, delete
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from models import User, Lottery, Participation, Winner
|
||||||
|
from typing import List, Optional, Dict, Any
|
||||||
|
from account_utils import validate_account_number, format_account_number
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class UserService:
|
||||||
|
"""Сервис для работы с пользователями"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_or_create_user(session: AsyncSession, telegram_id: int,
|
||||||
|
username: str = None, first_name: str = None,
|
||||||
|
last_name: str = None) -> User:
|
||||||
|
"""Получить или создать пользователя"""
|
||||||
|
# Пробуем найти существующего пользователя
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Обновляем информацию о пользователе
|
||||||
|
user.username = username
|
||||||
|
user.first_name = first_name
|
||||||
|
user.last_name = last_name
|
||||||
|
await session.commit()
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Создаем нового пользователя
|
||||||
|
user = User(
|
||||||
|
telegram_id=telegram_id,
|
||||||
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name
|
||||||
|
)
|
||||||
|
session.add(user)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]:
|
||||||
|
"""Получить пользователя по Telegram ID"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.telegram_id == telegram_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
||||||
|
"""Получить пользователя по username"""
|
||||||
|
result = await session.execute(select(User).where(User.username == username))
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all_users(session: AsyncSession, limit: int = None, offset: int = 0) -> List[User]:
|
||||||
|
"""Получить всех пользователей"""
|
||||||
|
query = select(User).order_by(User.created_at.desc())
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def search_users(session: AsyncSession, search_term: str, limit: int = 20) -> List[User]:
|
||||||
|
"""Поиск пользователей по имени или username"""
|
||||||
|
from sqlalchemy import or_, func
|
||||||
|
|
||||||
|
search_pattern = f"%{search_term.lower()}%"
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(
|
||||||
|
or_(
|
||||||
|
func.lower(User.first_name).contains(search_pattern),
|
||||||
|
func.lower(User.last_name).contains(search_pattern) if User.last_name else False,
|
||||||
|
func.lower(User.username).contains(search_pattern) if User.username else False
|
||||||
|
)
|
||||||
|
).limit(limit)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_user(session: AsyncSession, user_id: int) -> bool:
|
||||||
|
"""Удалить пользователя и все связанные данные"""
|
||||||
|
user = await session.get(User, user_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Удаляем все участия
|
||||||
|
await session.execute(
|
||||||
|
delete(Participation).where(Participation.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем все победы
|
||||||
|
await session.execute(
|
||||||
|
delete(Winner).where(Winner.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем пользователя
|
||||||
|
await session.delete(user)
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_admin(session: AsyncSession, telegram_id: int, is_admin: bool = True) -> bool:
|
||||||
|
"""Установить/снять права администратора"""
|
||||||
|
result = await session.execute(
|
||||||
|
update(User)
|
||||||
|
.where(User.telegram_id == telegram_id)
|
||||||
|
.values(is_admin=is_admin)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_account_number(session: AsyncSession, telegram_id: int, account_number: str) -> bool:
|
||||||
|
"""Установить номер клиентского счета пользователю"""
|
||||||
|
# Валидируем и форматируем номер
|
||||||
|
formatted_number = format_account_number(account_number)
|
||||||
|
if not formatted_number:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем уникальность номера
|
||||||
|
existing = await session.execute(
|
||||||
|
select(User).where(User.account_number == formatted_number)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
return False # Номер уже занят
|
||||||
|
|
||||||
|
# Обновляем пользователя
|
||||||
|
result = await session.execute(
|
||||||
|
update(User)
|
||||||
|
.where(User.telegram_id == telegram_id)
|
||||||
|
.values(account_number=formatted_number)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_by_account(session: AsyncSession, account_number: str) -> Optional[User]:
|
||||||
|
"""Получить пользователя по номеру счета"""
|
||||||
|
formatted_number = format_account_number(account_number)
|
||||||
|
if not formatted_number:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.account_number == formatted_number)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def search_by_account(session: AsyncSession, account_pattern: str) -> List[User]:
|
||||||
|
"""Поиск пользователей по части номера счета"""
|
||||||
|
# Убираем все кроме цифр и дефисов
|
||||||
|
clean_pattern = ''.join(c for c in account_pattern if c.isdigit() or c == '-')
|
||||||
|
if not clean_pattern:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(
|
||||||
|
User.account_number.like(f'%{clean_pattern}%')
|
||||||
|
).limit(20)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
|
class LotteryService:
|
||||||
|
"""Сервис для работы с розыгрышами"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def create_lottery(session: AsyncSession, title: str, description: str,
|
||||||
|
prizes: List[str], creator_id: int) -> Lottery:
|
||||||
|
"""Создать новый розыгрыш"""
|
||||||
|
lottery = Lottery(
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
prizes=prizes,
|
||||||
|
creator_id=creator_id
|
||||||
|
)
|
||||||
|
session.add(lottery)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(lottery)
|
||||||
|
return lottery
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_lottery(session: AsyncSession, lottery_id: int) -> Optional[Lottery]:
|
||||||
|
"""Получить розыгрыш по ID"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Lottery)
|
||||||
|
.options(selectinload(Lottery.participations).selectinload(Participation.user))
|
||||||
|
.where(Lottery.id == lottery_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_active_lotteries(session: AsyncSession) -> List[Lottery]:
|
||||||
|
"""Получить список активных розыгрышей"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Lottery)
|
||||||
|
.where(Lottery.is_active == True, Lottery.is_completed == False)
|
||||||
|
.order_by(Lottery.created_at.desc())
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all_lotteries(session: AsyncSession) -> List[Lottery]:
|
||||||
|
"""Получить список всех розыгрышей"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Lottery)
|
||||||
|
.order_by(Lottery.created_at.desc())
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_manual_winner(session: AsyncSession, lottery_id: int,
|
||||||
|
place: int, telegram_id: int) -> bool:
|
||||||
|
"""Установить ручного победителя для определенного места"""
|
||||||
|
# Получаем пользователя
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Получаем розыгрыш
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Обновляем ручных победителей
|
||||||
|
if not lottery.manual_winners:
|
||||||
|
lottery.manual_winners = {}
|
||||||
|
|
||||||
|
lottery.manual_winners[str(place)] = telegram_id
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def conduct_draw(session: AsyncSession, lottery_id: int) -> Dict[int, Dict[str, Any]]:
|
||||||
|
"""Провести розыгрыш с учетом ручных победителей"""
|
||||||
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
|
if not lottery or lottery.is_completed:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Получаем всех участников
|
||||||
|
participants = [p.user for p in lottery.participations]
|
||||||
|
if not participants:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Определяем количество призовых мест
|
||||||
|
num_prizes = len(lottery.prizes) if lottery.prizes else 1
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
remaining_participants = participants.copy()
|
||||||
|
manual_winners = lottery.manual_winners or {}
|
||||||
|
|
||||||
|
# Сначала обрабатываем ручных победителей
|
||||||
|
for place in range(1, num_prizes + 1):
|
||||||
|
place_str = str(place)
|
||||||
|
if place_str in manual_winners:
|
||||||
|
# Находим пользователя среди участников
|
||||||
|
manual_winner = None
|
||||||
|
for participant in remaining_participants:
|
||||||
|
if participant.telegram_id == manual_winners[place_str]:
|
||||||
|
manual_winner = participant
|
||||||
|
break
|
||||||
|
|
||||||
|
if manual_winner:
|
||||||
|
results[place] = {
|
||||||
|
'user': manual_winner,
|
||||||
|
'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места",
|
||||||
|
'is_manual': True
|
||||||
|
}
|
||||||
|
remaining_participants.remove(manual_winner)
|
||||||
|
|
||||||
|
# Заполняем оставшиеся места случайными участниками
|
||||||
|
for place in range(1, num_prizes + 1):
|
||||||
|
if place not in results and remaining_participants:
|
||||||
|
winner = random.choice(remaining_participants)
|
||||||
|
results[place] = {
|
||||||
|
'user': winner,
|
||||||
|
'prize': lottery.prizes[place - 1] if lottery.prizes and place <= len(lottery.prizes) else f"Приз {place} места",
|
||||||
|
'is_manual': False
|
||||||
|
}
|
||||||
|
remaining_participants.remove(winner)
|
||||||
|
|
||||||
|
# Сохраняем победителей в базу данных
|
||||||
|
for place, winner_info in results.items():
|
||||||
|
winner = Winner(
|
||||||
|
lottery_id=lottery_id,
|
||||||
|
user_id=winner_info['user'].id,
|
||||||
|
place=place,
|
||||||
|
prize=winner_info['prize'],
|
||||||
|
is_manual=winner_info['is_manual']
|
||||||
|
)
|
||||||
|
session.add(winner)
|
||||||
|
|
||||||
|
# Обновляем статус розыгрыша
|
||||||
|
lottery.is_completed = True
|
||||||
|
lottery.draw_results = {
|
||||||
|
str(place): {
|
||||||
|
'user_id': info['user'].id,
|
||||||
|
'telegram_id': info['user'].telegram_id,
|
||||||
|
'username': info['user'].username,
|
||||||
|
'prize': info['prize'],
|
||||||
|
'is_manual': info['is_manual']
|
||||||
|
} for place, info in results.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_winners(session: AsyncSession, lottery_id: int) -> List[Winner]:
|
||||||
|
"""Получить победителей розыгрыша"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Winner)
|
||||||
|
.options(selectinload(Winner.user))
|
||||||
|
.where(Winner.lottery_id == lottery_id)
|
||||||
|
.order_by(Winner.place)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_winner_display_type(session: AsyncSession, lottery_id: int, display_type: str) -> bool:
|
||||||
|
"""Установить тип отображения победителей для розыгрыша"""
|
||||||
|
from winner_display import validate_display_type
|
||||||
|
|
||||||
|
if not validate_display_type(display_type):
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
update(Lottery)
|
||||||
|
.where(Lottery.id == lottery_id)
|
||||||
|
.values(winner_display_type=display_type)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_lottery_active(session: AsyncSession, lottery_id: int, is_active: bool) -> bool:
|
||||||
|
"""Установить статус активности розыгрыша"""
|
||||||
|
result = await session.execute(
|
||||||
|
update(Lottery)
|
||||||
|
.where(Lottery.id == lottery_id)
|
||||||
|
.values(is_active=is_active)
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def complete_lottery(session: AsyncSession, lottery_id: int) -> bool:
|
||||||
|
"""Завершить розыгрыш (сделать неактивным и завершенным)"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
update(Lottery)
|
||||||
|
.where(Lottery.id == lottery_id)
|
||||||
|
.values(is_active=False, is_completed=True, end_date=datetime.now())
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def delete_lottery(session: AsyncSession, lottery_id: int) -> bool:
|
||||||
|
"""Удалить розыгрыш и все связанные данные"""
|
||||||
|
# Сначала удаляем все связанные данные
|
||||||
|
# Удаляем победителей
|
||||||
|
await session.execute(
|
||||||
|
delete(Winner).where(Winner.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем участников
|
||||||
|
await session.execute(
|
||||||
|
delete(Participation).where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем сам розыгрыш
|
||||||
|
result = await session.execute(
|
||||||
|
delete(Lottery).where(Lottery.id == lottery_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipationService:
|
||||||
|
"""Сервис для работы с участием в розыгрышах"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def add_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool:
|
||||||
|
"""Добавить участника в розыгрыш"""
|
||||||
|
# Проверяем, не участвует ли уже пользователь
|
||||||
|
existing = await session.execute(
|
||||||
|
select(Participation)
|
||||||
|
.where(Participation.lottery_id == lottery_id, Participation.user_id == user_id)
|
||||||
|
)
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
return False
|
||||||
|
|
||||||
|
participation = Participation(lottery_id=lottery_id, user_id=user_id)
|
||||||
|
session.add(participation)
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def remove_participant(session: AsyncSession, lottery_id: int, user_id: int) -> bool:
|
||||||
|
"""Удалить участника из розыгрыша"""
|
||||||
|
participation = await session.execute(
|
||||||
|
select(Participation)
|
||||||
|
.where(Participation.lottery_id == lottery_id, Participation.user_id == user_id)
|
||||||
|
)
|
||||||
|
participation = participation.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not participation:
|
||||||
|
return False
|
||||||
|
|
||||||
|
await session.delete(participation)
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_participants(session: AsyncSession, lottery_id: int, limit: Optional[int] = None, offset: int = 0) -> List[User]:
|
||||||
|
"""Получить участников розыгрыша"""
|
||||||
|
query = select(User).join(Participation).where(Participation.lottery_id == lottery_id)
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query = query.offset(offset).limit(limit)
|
||||||
|
|
||||||
|
result = await session.execute(query)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_participations(session: AsyncSession, user_id: int) -> List[Participation]:
|
||||||
|
"""Получить участие пользователя в розыгрышах"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Participation)
|
||||||
|
.options(selectinload(Participation.lottery))
|
||||||
|
.where(Participation.user_id == user_id)
|
||||||
|
.order_by(Participation.created_at.desc())
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_participants_count(session: AsyncSession, lottery_id: int) -> int:
|
||||||
|
"""Получить количество участников в розыгрыше"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(Participation)
|
||||||
|
.where(Participation.lottery_id == lottery_id)
|
||||||
|
)
|
||||||
|
return len(result.scalars().all())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def add_participants_bulk(session: AsyncSession, lottery_id: int, telegram_ids: List[int]) -> Dict[str, Any]:
|
||||||
|
"""Массовое добавление участников"""
|
||||||
|
results = {
|
||||||
|
"added": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"errors": [],
|
||||||
|
"details": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for telegram_id in telegram_ids:
|
||||||
|
try:
|
||||||
|
# Проверяем, существует ли пользователь
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if not user:
|
||||||
|
results["errors"].append(f"Пользователь {telegram_id} не найден")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Пробуем добавить
|
||||||
|
if await ParticipationService.add_participant(session, lottery_id, user.id):
|
||||||
|
results["added"] += 1
|
||||||
|
results["details"].append(f"Добавлен: {user.first_name} (@{user.username or 'no_username'})")
|
||||||
|
else:
|
||||||
|
results["skipped"] += 1
|
||||||
|
results["details"].append(f"Уже участвует: {user.first_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"Ошибка с {telegram_id}: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def remove_participants_bulk(session: AsyncSession, lottery_id: int, telegram_ids: List[int]) -> Dict[str, Any]:
|
||||||
|
"""Массовое удаление участников"""
|
||||||
|
results = {
|
||||||
|
"removed": 0,
|
||||||
|
"not_found": 0,
|
||||||
|
"errors": [],
|
||||||
|
"details": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for telegram_id in telegram_ids:
|
||||||
|
try:
|
||||||
|
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
||||||
|
if not user:
|
||||||
|
results["not_found"] += 1
|
||||||
|
results["details"].append(f"Не найден: {telegram_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if await ParticipationService.remove_participant(session, lottery_id, user.id):
|
||||||
|
results["removed"] += 1
|
||||||
|
results["details"].append(f"Удален: {user.first_name}")
|
||||||
|
else:
|
||||||
|
results["not_found"] += 1
|
||||||
|
results["details"].append(f"Не участвовал: {user.first_name}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"Ошибка с {telegram_id}: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def add_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Массовое добавление участников по номерам счетов"""
|
||||||
|
results = {
|
||||||
|
"added": 0,
|
||||||
|
"skipped": 0,
|
||||||
|
"errors": [],
|
||||||
|
"details": [],
|
||||||
|
"invalid_accounts": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for account_number in account_numbers:
|
||||||
|
account_number = account_number.strip()
|
||||||
|
if not account_number:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Валидируем и форматируем номер
|
||||||
|
formatted_account = format_account_number(account_number)
|
||||||
|
if not formatted_account:
|
||||||
|
results["invalid_accounts"].append(account_number)
|
||||||
|
results["errors"].append(f"Неверный формат: {account_number}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ищем пользователя по номеру счёта
|
||||||
|
user = await UserService.get_user_by_account(session, formatted_account)
|
||||||
|
if not user:
|
||||||
|
results["errors"].append(f"Пользователь с счётом {formatted_account} не найден")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Пробуем добавить в розыгрыш
|
||||||
|
if await ParticipationService.add_participant(session, lottery_id, user.id):
|
||||||
|
results["added"] += 1
|
||||||
|
results["details"].append(f"Добавлен: {user.first_name} ({formatted_account})")
|
||||||
|
else:
|
||||||
|
results["skipped"] += 1
|
||||||
|
results["details"].append(f"Уже участвует: {user.first_name} ({formatted_account})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"Ошибка с {account_number}: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def remove_participants_by_accounts_bulk(session: AsyncSession, lottery_id: int, account_numbers: List[str]) -> Dict[str, Any]:
|
||||||
|
"""Массовое удаление участников по номерам счетов"""
|
||||||
|
results = {
|
||||||
|
"removed": 0,
|
||||||
|
"not_found": 0,
|
||||||
|
"errors": [],
|
||||||
|
"details": [],
|
||||||
|
"invalid_accounts": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for account_number in account_numbers:
|
||||||
|
account_number = account_number.strip()
|
||||||
|
if not account_number:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Валидируем и форматируем номер
|
||||||
|
formatted_account = format_account_number(account_number)
|
||||||
|
if not formatted_account:
|
||||||
|
results["invalid_accounts"].append(account_number)
|
||||||
|
results["errors"].append(f"Неверный формат: {account_number}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ищем пользователя по номеру счёта
|
||||||
|
user = await UserService.get_user_by_account(session, formatted_account)
|
||||||
|
if not user:
|
||||||
|
results["not_found"] += 1
|
||||||
|
results["details"].append(f"Не найден: {formatted_account}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Пробуем удалить из розыгрыша
|
||||||
|
if await ParticipationService.remove_participant(session, lottery_id, user.id):
|
||||||
|
results["removed"] += 1
|
||||||
|
results["details"].append(f"Удалён: {user.first_name} ({formatted_account})")
|
||||||
|
else:
|
||||||
|
results["not_found"] += 1
|
||||||
|
results["details"].append(f"Не участвовал: {user.first_name} ({formatted_account})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
results["errors"].append(f"Ошибка с {account_number}: {str(e)}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_participant_stats(session: AsyncSession, user_id: int) -> Dict[str, Any]:
|
||||||
|
"""Статистика участника"""
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
# Количество участий
|
||||||
|
participations_count = await session.scalar(
|
||||||
|
select(func.count(Participation.id)).where(Participation.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Количество побед
|
||||||
|
wins_count = await session.scalar(
|
||||||
|
select(func.count(Winner.id)).where(Winner.user_id == user_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Последнее участие
|
||||||
|
last_participation = await session.execute(
|
||||||
|
select(Participation).where(Participation.user_id == user_id)
|
||||||
|
.order_by(Participation.created_at.desc()).limit(1)
|
||||||
|
)
|
||||||
|
last_participation = last_participation.scalar_one_or_none()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"participations_count": participations_count,
|
||||||
|
"wins_count": wins_count,
|
||||||
|
"last_participation": last_participation.created_at if last_participation else None
|
||||||
|
}
|
||||||
32
simple_draw.py
Normal file
32
simple_draw.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Простой скрипт для проведения розыгрыша
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from database import async_session_maker
|
||||||
|
from services import LotteryService
|
||||||
|
|
||||||
|
async def conduct_simple_draw():
|
||||||
|
"""Проводим розыгрыш"""
|
||||||
|
print("🎲 Проведение розыгрыша")
|
||||||
|
print("=" * 30)
|
||||||
|
|
||||||
|
lottery_id = 1 # Первый розыгрыш
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
print(f"🎯 Проводим розыгрыш #{lottery_id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проводим розыгрыш
|
||||||
|
winners = await LotteryService.conduct_draw(session, lottery_id)
|
||||||
|
|
||||||
|
print(f"🎉 Розыгрыш проведен!")
|
||||||
|
print(f"📊 Результат: {winners}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(conduct_simple_draw())
|
||||||
37
start.sh
Executable file
37
start.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Скрипт запуска бота для проедения розыгрышей
|
||||||
|
|
||||||
|
echo "🚀 Запуск телеграм-бота для розыгрышей"
|
||||||
|
|
||||||
|
# Проверка виртуального окружения
|
||||||
|
if [[ "$VIRTUAL_ENV" == "" ]]; then
|
||||||
|
echo "⚠️ Рекомендуется использовать виртуальное окружение"
|
||||||
|
echo "Создайте его командой: python -m venv venv"
|
||||||
|
echo "Активируйте: source venv/bin/activate"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверка файла .env
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "❌ Файл .env не найден!"
|
||||||
|
echo "Скопируйте .env.example в .env и заполните переменные"
|
||||||
|
echo "cp .env.example .env"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверка зависимостей
|
||||||
|
echo "📦 Проверка зависимостей..."
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Инициализация базы данных
|
||||||
|
echo "🔄 Инициализация базы данных..."
|
||||||
|
python utils.py init
|
||||||
|
|
||||||
|
# Установка прав администратора
|
||||||
|
echo "👑 Настройка администраторов..."
|
||||||
|
python utils.py setup-admins
|
||||||
|
|
||||||
|
# Запуск бота
|
||||||
|
echo "🤖 Запуск бота..."
|
||||||
|
python main.py
|
||||||
268
task_manager.py
Normal file
268
task_manager.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
"""
|
||||||
|
Система управления многопоточностью и очередями для обработки запросов
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, Optional, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskPriority(Enum):
|
||||||
|
"""Приоритеты задач"""
|
||||||
|
LOW = 1
|
||||||
|
NORMAL = 2
|
||||||
|
HIGH = 3
|
||||||
|
CRITICAL = 4
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
"""Задача для выполнения"""
|
||||||
|
id: str
|
||||||
|
user_id: int
|
||||||
|
priority: TaskPriority
|
||||||
|
func: Callable
|
||||||
|
args: tuple
|
||||||
|
kwargs: dict
|
||||||
|
created_at: float
|
||||||
|
timeout: float = 30.0
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
"""Сравнение для приоритетной очереди"""
|
||||||
|
if self.priority.value != other.priority.value:
|
||||||
|
return self.priority.value > other.priority.value
|
||||||
|
return self.created_at < other.created_at
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncTaskManager:
|
||||||
|
"""Менеджер асинхронных задач с поддержкой приоритетов и ограничений"""
|
||||||
|
|
||||||
|
def __init__(self, max_workers: int = 10, max_user_concurrent: int = 3):
|
||||||
|
self.max_workers = max_workers
|
||||||
|
self.max_user_concurrent = max_user_concurrent
|
||||||
|
|
||||||
|
# Очереди и семафоры - будут созданы при запуске
|
||||||
|
self.task_queue: Optional[asyncio.PriorityQueue] = None
|
||||||
|
self.worker_semaphore: Optional[asyncio.Semaphore] = None
|
||||||
|
self.user_semaphores: Dict[int, asyncio.Semaphore] = {}
|
||||||
|
|
||||||
|
# Статистика
|
||||||
|
self.active_tasks: Dict[str, Task] = {}
|
||||||
|
self.user_task_counts: Dict[int, int] = {}
|
||||||
|
self.completed_tasks = 0
|
||||||
|
self.failed_tasks = 0
|
||||||
|
|
||||||
|
# Воркеры
|
||||||
|
self.workers = []
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Запуск менеджера задач"""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаём asyncio объекты в правильном event loop
|
||||||
|
self.task_queue = asyncio.PriorityQueue()
|
||||||
|
self.worker_semaphore = asyncio.Semaphore(self.max_workers)
|
||||||
|
self.user_semaphores.clear() # Очищаем старые семафоры
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
logger.info(f"Запуск {self.max_workers} воркеров для обработки задач")
|
||||||
|
|
||||||
|
# Создаём воркеры
|
||||||
|
for i in range(self.max_workers):
|
||||||
|
worker = asyncio.create_task(self._worker(f"worker-{i}"))
|
||||||
|
self.workers.append(worker)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Остановка менеджера задач"""
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
logger.info("Остановка менеджера задач...")
|
||||||
|
|
||||||
|
# Отменяем всех воркеров
|
||||||
|
for worker in self.workers:
|
||||||
|
worker.cancel()
|
||||||
|
|
||||||
|
# Ждём завершения
|
||||||
|
await asyncio.gather(*self.workers, return_exceptions=True)
|
||||||
|
self.workers.clear()
|
||||||
|
|
||||||
|
# Очищаем asyncio объекты
|
||||||
|
self.task_queue = None
|
||||||
|
self.worker_semaphore = None
|
||||||
|
self.user_semaphores.clear()
|
||||||
|
|
||||||
|
logger.info("Менеджер задач остановлен")
|
||||||
|
|
||||||
|
async def add_task(self,
|
||||||
|
task_id: str,
|
||||||
|
user_id: int,
|
||||||
|
func: Callable,
|
||||||
|
*args,
|
||||||
|
priority: TaskPriority = TaskPriority.NORMAL,
|
||||||
|
timeout: float = 30.0,
|
||||||
|
**kwargs) -> str:
|
||||||
|
"""Добавить задачу в очередь"""
|
||||||
|
|
||||||
|
if not self.running or self.task_queue is None:
|
||||||
|
raise RuntimeError("TaskManager не запущен")
|
||||||
|
|
||||||
|
# Проверяем лимиты пользователя
|
||||||
|
user_count = self.user_task_counts.get(user_id, 0)
|
||||||
|
if user_count >= self.max_user_concurrent:
|
||||||
|
raise ValueError(f"Пользователь {user_id} превысил лимит одновременных задач ({self.max_user_concurrent})")
|
||||||
|
|
||||||
|
# Создаём задачу
|
||||||
|
task = Task(
|
||||||
|
id=task_id,
|
||||||
|
user_id=user_id,
|
||||||
|
priority=priority,
|
||||||
|
func=func,
|
||||||
|
args=args,
|
||||||
|
kwargs=kwargs,
|
||||||
|
created_at=time.time(),
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем в очередь
|
||||||
|
await self.task_queue.put(task)
|
||||||
|
|
||||||
|
# Обновляем статистику
|
||||||
|
self.user_task_counts[user_id] = user_count + 1
|
||||||
|
|
||||||
|
logger.debug(f"Задача {task_id} добавлена в очередь (пользователь: {user_id}, приоритет: {priority.name})")
|
||||||
|
return task_id
|
||||||
|
|
||||||
|
async def _worker(self, worker_name: str):
|
||||||
|
"""Воркер для выполнения задач"""
|
||||||
|
logger.debug(f"Воркер {worker_name} запущен")
|
||||||
|
|
||||||
|
while self.running and self.task_queue is not None and self.worker_semaphore is not None:
|
||||||
|
try:
|
||||||
|
# Получаем задачу из очереди (с таймаутом)
|
||||||
|
try:
|
||||||
|
task = await asyncio.wait_for(self.task_queue.get(), timeout=1.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Получаем семафоры
|
||||||
|
async with self.worker_semaphore:
|
||||||
|
user_semaphore = self._get_user_semaphore(task.user_id)
|
||||||
|
if user_semaphore is not None:
|
||||||
|
async with user_semaphore:
|
||||||
|
await self._execute_task(worker_name, task)
|
||||||
|
else:
|
||||||
|
await self._execute_task(worker_name, task)
|
||||||
|
|
||||||
|
# Отмечаем задачу как выполненную
|
||||||
|
self.task_queue.task_done()
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug(f"Воркер {worker_name} отменён")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка в воркере {worker_name}: {e}", exc_info=True)
|
||||||
|
|
||||||
|
logger.debug(f"Воркер {worker_name} завершён")
|
||||||
|
|
||||||
|
def _get_user_semaphore(self, user_id: int) -> Optional[asyncio.Semaphore]:
|
||||||
|
"""Получить семафор пользователя"""
|
||||||
|
if not self.running:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if user_id not in self.user_semaphores:
|
||||||
|
self.user_semaphores[user_id] = asyncio.Semaphore(self.max_user_concurrent)
|
||||||
|
return self.user_semaphores[user_id]
|
||||||
|
|
||||||
|
async def _execute_task(self, worker_name: str, task: Task):
|
||||||
|
"""Выполнить задачу"""
|
||||||
|
task_start = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Регистрируем активную задачу
|
||||||
|
self.active_tasks[task.id] = task
|
||||||
|
|
||||||
|
logger.debug(f"Воркер {worker_name} выполняет задачу {task.id}")
|
||||||
|
|
||||||
|
# Выполняем с таймаутом
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(task.func):
|
||||||
|
result = await asyncio.wait_for(
|
||||||
|
task.func(*task.args, **task.kwargs),
|
||||||
|
timeout=task.timeout
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Для синхронных функций
|
||||||
|
result = await asyncio.wait_for(
|
||||||
|
asyncio.to_thread(task.func, *task.args, **task.kwargs),
|
||||||
|
timeout=task.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
self.completed_tasks += 1
|
||||||
|
execution_time = time.time() - task_start
|
||||||
|
|
||||||
|
logger.debug(f"Задача {task.id} выполнена за {execution_time:.2f}с")
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning(f"Задача {task.id} превысила таймаут {task.timeout}с")
|
||||||
|
self.failed_tasks += 1
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка выполнения задачи {task.id}: {e}")
|
||||||
|
self.failed_tasks += 1
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Убираем из активных и обновляем счётчики
|
||||||
|
self.active_tasks.pop(task.id, None)
|
||||||
|
user_count = self.user_task_counts.get(task.user_id, 0)
|
||||||
|
if user_count > 0:
|
||||||
|
self.user_task_counts[task.user_id] = user_count - 1
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Получить статистику менеджера"""
|
||||||
|
return {
|
||||||
|
'running': self.running,
|
||||||
|
'workers_count': len(self.workers),
|
||||||
|
'active_tasks': len(self.active_tasks),
|
||||||
|
'queue_size': self.task_queue.qsize() if self.task_queue is not None else 0,
|
||||||
|
'completed_tasks': self.completed_tasks,
|
||||||
|
'failed_tasks': self.failed_tasks,
|
||||||
|
'user_tasks': dict(self.user_task_counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_user_stats(self, user_id: int) -> Dict[str, Any]:
|
||||||
|
"""Получить статистику пользователя"""
|
||||||
|
active_user_tasks = [
|
||||||
|
task for task in self.active_tasks.values()
|
||||||
|
if task.user_id == user_id
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'active_tasks': len(active_user_tasks),
|
||||||
|
'max_concurrent': self.max_user_concurrent,
|
||||||
|
'can_add_task': len(active_user_tasks) < self.max_user_concurrent,
|
||||||
|
'task_details': [
|
||||||
|
{
|
||||||
|
'id': task.id,
|
||||||
|
'priority': task.priority.name,
|
||||||
|
'created_at': task.created_at,
|
||||||
|
'running_time': time.time() - task.created_at
|
||||||
|
}
|
||||||
|
for task in active_user_tasks
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Глобальный экземпляр менеджера задач
|
||||||
|
task_manager = AsyncTaskManager(
|
||||||
|
max_workers=15, # Максимум воркеров
|
||||||
|
max_user_concurrent=5 # Максимум задач на пользователя
|
||||||
|
)
|
||||||
163
test_admin_improvements.py
Normal file
163
test_admin_improvements.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тест всех новых функций: пакетного добавления счетов и админских хэндлеров
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database import async_session_maker, engine
|
||||||
|
from services import UserService, LotteryService, ParticipationService
|
||||||
|
from account_utils import validate_account_number, format_account_number, mask_account_number
|
||||||
|
from winner_display import format_winner_display
|
||||||
|
import random
|
||||||
|
|
||||||
|
async def test_comprehensive_features():
|
||||||
|
print("🧪 Комплексное тестирование всех функций")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# 1. Создаем тестовых пользователей с номерами счетов
|
||||||
|
print("\n1. 📝 Создание тестовых пользователей:")
|
||||||
|
test_users = []
|
||||||
|
test_accounts = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
# Генерируем уникальные данные
|
||||||
|
unique_id = random.randint(10000000, 99999999)
|
||||||
|
account = f"{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}-{random.randint(10,99)}"
|
||||||
|
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=unique_id,
|
||||||
|
username=f'test_user_{i}_{unique_id}',
|
||||||
|
first_name=f'Тестовый{i}',
|
||||||
|
last_name=f'Пользователь{i}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Устанавливаем номер счёта
|
||||||
|
success = await UserService.set_account_number(session, user.telegram_id, account)
|
||||||
|
if success:
|
||||||
|
test_users.append(user)
|
||||||
|
test_accounts.append(account)
|
||||||
|
print(f"✅ Пользователь {i+1}: {user.first_name} ({account})")
|
||||||
|
else:
|
||||||
|
print(f"❌ Не удалось создать пользователя {i+1}")
|
||||||
|
|
||||||
|
# 2. Создаем тестовый розыгрыш
|
||||||
|
print(f"\n2. 🎯 Создание тестового розыгрыша:")
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="Тест массовых операций",
|
||||||
|
description="Розыгрыш для тестирования массовых операций",
|
||||||
|
prizes=["Приз 1", "Приз 2"],
|
||||||
|
creator_id=1
|
||||||
|
)
|
||||||
|
print(f"✅ Розыгрыш создан: {lottery.title}")
|
||||||
|
|
||||||
|
# 3. Тестируем массовое добавление по номерам счетов
|
||||||
|
print(f"\n3. 🏦 Массовое добавление по номерам счетов:")
|
||||||
|
accounts_to_add = test_accounts[:3] # Первые 3
|
||||||
|
print(f"Добавляем {len(accounts_to_add)} счетов: {', '.join(accounts_to_add)}")
|
||||||
|
|
||||||
|
results = await ParticipationService.add_participants_by_accounts_bulk(
|
||||||
|
session, lottery.id, accounts_to_add
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📊 Результат:")
|
||||||
|
print(f" ✅ Добавлено: {results['added']}")
|
||||||
|
print(f" ⚠️ Пропущено: {results['skipped']}")
|
||||||
|
print(f" ❌ Ошибок: {len(results['errors'])}")
|
||||||
|
|
||||||
|
# 4. Проверяем участников
|
||||||
|
print(f"\n4. 👥 Проверка участников:")
|
||||||
|
participants = await ParticipationService.get_participants(session, lottery.id)
|
||||||
|
print(f"Участников в розыгрыше: {len(participants)}")
|
||||||
|
for participant in participants:
|
||||||
|
print(f" • {participant.first_name} ({participant.account_number})")
|
||||||
|
|
||||||
|
# 5. Тестируем настройки отображения победителей
|
||||||
|
print(f"\n5. 🎭 Тестирование настроек отображения:")
|
||||||
|
for display_type in ["username", "chat_id", "account_number"]:
|
||||||
|
await LotteryService.set_winner_display_type(session, lottery.id, display_type)
|
||||||
|
lottery.winner_display_type = display_type # Обновляем локально
|
||||||
|
|
||||||
|
if participants:
|
||||||
|
test_participant = participants[0]
|
||||||
|
public_display = format_winner_display(test_participant, lottery, show_sensitive_data=False)
|
||||||
|
admin_display = format_winner_display(test_participant, lottery, show_sensitive_data=True)
|
||||||
|
|
||||||
|
print(f" 📺 Тип {display_type}:")
|
||||||
|
print(f" 👥 Публично: {public_display}")
|
||||||
|
print(f" 🔧 Админ: {admin_display}")
|
||||||
|
|
||||||
|
# 6. Тестируем массовое удаление по счетам
|
||||||
|
print(f"\n6. 🗑️ Массовое удаление по номерам счетов:")
|
||||||
|
accounts_to_remove = test_accounts[:2] # Первые 2
|
||||||
|
print(f"Удаляем {len(accounts_to_remove)} счетов")
|
||||||
|
|
||||||
|
results = await ParticipationService.remove_participants_by_accounts_bulk(
|
||||||
|
session, lottery.id, accounts_to_remove
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📊 Результат:")
|
||||||
|
print(f" ✅ Удалено: {results['removed']}")
|
||||||
|
print(f" ⚠️ Не найдено: {results['not_found']}")
|
||||||
|
print(f" ❌ Ошибок: {len(results['errors'])}")
|
||||||
|
|
||||||
|
# 7. Проверяем оставшихся участников
|
||||||
|
print(f"\n7. 👥 Проверка оставшихся участников:")
|
||||||
|
remaining_participants = await ParticipationService.get_participants(session, lottery.id)
|
||||||
|
print(f"Участников осталось: {len(remaining_participants)}")
|
||||||
|
for participant in remaining_participants:
|
||||||
|
print(f" • {participant.first_name} ({participant.account_number})")
|
||||||
|
|
||||||
|
# 8. Тестируем методы управления розыгрышем
|
||||||
|
print(f"\n8. ⚙️ Тестирование управления розыгрышем:")
|
||||||
|
|
||||||
|
# Деактивируем
|
||||||
|
success = await LotteryService.set_lottery_active(session, lottery.id, False)
|
||||||
|
print(f" 🔴 Деактивация: {'✅' if success else '❌'}")
|
||||||
|
|
||||||
|
# Активируем обратно
|
||||||
|
success = await LotteryService.set_lottery_active(session, lottery.id, True)
|
||||||
|
print(f" 🟢 Активация: {'✅' if success else '❌'}")
|
||||||
|
|
||||||
|
# Завершаем
|
||||||
|
success = await LotteryService.complete_lottery(session, lottery.id)
|
||||||
|
print(f" 🏁 Завершение: {'✅' if success else '❌'}")
|
||||||
|
|
||||||
|
# 9. Тестируем валидацию счетов с неправильными форматами
|
||||||
|
print(f"\n9. 🔍 Тестирование валидации:")
|
||||||
|
|
||||||
|
invalid_accounts = [
|
||||||
|
"invalid-account",
|
||||||
|
"12345", # Слишком короткий
|
||||||
|
"12-34-56-78", # Неполный
|
||||||
|
"ab-cd-ef-gh-12-34-56-78", # С буквами
|
||||||
|
"12-34-56-78-90-12-34-56-78" # Слишком длинный
|
||||||
|
]
|
||||||
|
|
||||||
|
results = await ParticipationService.add_participants_by_accounts_bulk(
|
||||||
|
session, lottery.id, invalid_accounts
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📊 Результат добавления невалидных счетов:")
|
||||||
|
print(f" ✅ Добавлено: {results['added']}")
|
||||||
|
print(f" 🚫 Неверных форматов: {len(results['invalid_accounts'])}")
|
||||||
|
print(f" ❌ Ошибок: {len(results['errors'])}")
|
||||||
|
|
||||||
|
if results['invalid_accounts']:
|
||||||
|
print(f" 🚫 Неверные форматы:")
|
||||||
|
for invalid in results['invalid_accounts']:
|
||||||
|
print(f" • {invalid}")
|
||||||
|
|
||||||
|
print(f"\n🎉 Все тесты завершены успешно!")
|
||||||
|
print(f"✅ Массовое добавление и удаление по счетам работает")
|
||||||
|
print(f"✅ Настройка отображения победителей работает")
|
||||||
|
print(f"✅ Управление розыгрышами работает")
|
||||||
|
print(f"✅ Валидация счетов работает")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_comprehensive_features())
|
||||||
111
test_basic_features.py
Normal file
111
test_basic_features.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
Простой тест основной функциональности без многопоточности
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database import init_db, async_session_maker
|
||||||
|
from services import UserService, LotteryService
|
||||||
|
from account_utils import generate_account_number, validate_account_number, mask_account_number
|
||||||
|
from winner_display import format_winner_display, validate_display_type
|
||||||
|
|
||||||
|
|
||||||
|
async def test_basic_functionality():
|
||||||
|
"""Простой тест основной функциональности"""
|
||||||
|
print("🧪 Тестирование основной функциональности")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Инициализируем базу данных
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
|
||||||
|
# 1. Тест создания пользователя с номером счёта
|
||||||
|
print("\n1. 👤 Создание пользователя с номером счёта:")
|
||||||
|
|
||||||
|
test_account = "12-34-56-78-90-12-34-56"
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=999999999,
|
||||||
|
username='test_client',
|
||||||
|
first_name='Тестовый',
|
||||||
|
last_name='Клиент'
|
||||||
|
)
|
||||||
|
|
||||||
|
success = await UserService.set_account_number(
|
||||||
|
session, user.telegram_id, test_account
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ Пользователь создан и счёт {test_account} установлен")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка установки номера счёта")
|
||||||
|
|
||||||
|
# 2. Тест валидации номеров
|
||||||
|
print("\n2. 📋 Тестирование валидации номеров:")
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
("12-34-56-78-90-12-34-56", True),
|
||||||
|
("invalid-number", False),
|
||||||
|
("12345678901234567890", False)
|
||||||
|
]
|
||||||
|
|
||||||
|
for number, expected in test_cases:
|
||||||
|
result = validate_account_number(number)
|
||||||
|
status = "✅" if result == expected else "❌"
|
||||||
|
print(f"{status} '{number}' -> {result}")
|
||||||
|
|
||||||
|
# 3. Тест маскирования
|
||||||
|
print("\n3. 🎭 Тестирование маскирования:")
|
||||||
|
|
||||||
|
masked = mask_account_number(test_account, show_last_digits=4)
|
||||||
|
print(f"Полный номер: {test_account}")
|
||||||
|
print(f"Маскированный: {masked}")
|
||||||
|
|
||||||
|
# 4. Тест поиска по счёту
|
||||||
|
print("\n4. 🔍 Тестирование поиска по номеру счёта:")
|
||||||
|
|
||||||
|
found_user = await UserService.get_user_by_account(session, test_account)
|
||||||
|
if found_user:
|
||||||
|
print(f"✅ Пользователь найден по номеру счёта: {found_user.first_name}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Пользователь не найден")
|
||||||
|
|
||||||
|
# 5. Тест отображения победителей
|
||||||
|
print("\n5. 🎨 Тестирование отображения победителей:")
|
||||||
|
|
||||||
|
# Создаём тестовый розыгрыш
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="Тест отображения",
|
||||||
|
description="Тестовый розыгрыш",
|
||||||
|
prizes=["Приз 1"],
|
||||||
|
creator_id=1
|
||||||
|
)
|
||||||
|
|
||||||
|
display_types = ['username', 'chat_id', 'account_number']
|
||||||
|
|
||||||
|
for display_type in display_types:
|
||||||
|
await LotteryService.set_winner_display_type(
|
||||||
|
session, lottery.id, display_type
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_lottery = await LotteryService.get_lottery(session, lottery.id)
|
||||||
|
|
||||||
|
if updated_lottery:
|
||||||
|
public = format_winner_display(user, updated_lottery, show_sensitive_data=False)
|
||||||
|
admin = format_winner_display(user, updated_lottery, show_sensitive_data=True)
|
||||||
|
|
||||||
|
print(f"\n📺 Тип: {display_type}")
|
||||||
|
print(f" 👥 Публично: {public}")
|
||||||
|
print(f" 🔧 Админ: {admin}")
|
||||||
|
|
||||||
|
print("\n🎉 Основные тесты завершены успешно!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_basic_functionality())
|
||||||
93
test_clean_features.py
Normal file
93
test_clean_features.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тест функциональности с чистыми данными
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database import async_session_maker, engine
|
||||||
|
from services import UserService, LotteryService
|
||||||
|
from account_utils import validate_account_number, format_account_number, mask_account_number
|
||||||
|
from winner_display import format_winner_display
|
||||||
|
import random
|
||||||
|
|
||||||
|
async def test_features():
|
||||||
|
print("🧪 Тестирование с чистыми данными")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Генерируем уникальные тестовые данные
|
||||||
|
unique_id = random.randint(1000000, 9999999)
|
||||||
|
test_account = f"{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}-{random.randint(10, 99)}"
|
||||||
|
|
||||||
|
print(f"🆔 Используем уникальный ID: {unique_id}")
|
||||||
|
print(f"🔢 Используем номер счёта: {test_account}")
|
||||||
|
|
||||||
|
# 1. Создание пользователя
|
||||||
|
print(f"\n1. 👤 Создание пользователя:")
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=unique_id,
|
||||||
|
username=f'test_user_{unique_id}',
|
||||||
|
first_name='Тестовый',
|
||||||
|
last_name='Пользователь'
|
||||||
|
)
|
||||||
|
print(f"✅ Пользователь создан: {user.first_name} {user.last_name}")
|
||||||
|
|
||||||
|
# 2. Установка номера счёта
|
||||||
|
print(f"\n2. 🔢 Установка номера счёта:")
|
||||||
|
success = await UserService.set_account_number(
|
||||||
|
session, user.telegram_id, test_account
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✅ Номер счёта {test_account} установлен")
|
||||||
|
# Обновляем данные пользователя
|
||||||
|
await session.refresh(user)
|
||||||
|
print(f"✅ Подтверждено: {user.account_number}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Не удалось установить номер счёта")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Поиск пользователя по номеру
|
||||||
|
print(f"\n3. 🔍 Поиск по номеру счёта:")
|
||||||
|
found_user = await UserService.get_user_by_account(session, test_account)
|
||||||
|
if found_user:
|
||||||
|
print(f"✅ Пользователь найден: {found_user.first_name} {found_user.last_name}")
|
||||||
|
else:
|
||||||
|
print("❌ Пользователь не найден")
|
||||||
|
|
||||||
|
# 4. Тестирование маскирования
|
||||||
|
print(f"\n4. 🎭 Тестирование маскирования:")
|
||||||
|
masked = mask_account_number(test_account)
|
||||||
|
print(f"Полный номер: {test_account}")
|
||||||
|
print(f"Маскированный: {masked}")
|
||||||
|
|
||||||
|
# 5. Создание розыгрыша с типами отображения
|
||||||
|
print(f"\n5. 🎨 Тестирование отображения победителей:")
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="Тест отображения",
|
||||||
|
description="Тестовый розыгрыш",
|
||||||
|
prizes=["Приз 1"],
|
||||||
|
creator_id=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Тестируем все типы отображения
|
||||||
|
for display_type in ["username", "chat_id", "account_number"]:
|
||||||
|
await LotteryService.set_winner_display_type(session, lottery.id, display_type)
|
||||||
|
lottery.winner_display_type = display_type # Обновляем локально
|
||||||
|
|
||||||
|
public_display = format_winner_display(user, lottery, show_sensitive_data=False)
|
||||||
|
admin_display = format_winner_display(user, lottery, show_sensitive_data=True)
|
||||||
|
|
||||||
|
print(f"\n📺 Тип: {display_type}")
|
||||||
|
print(f" 👥 Публично: {public_display}")
|
||||||
|
print(f" 🔧 Админ: {admin_display}")
|
||||||
|
|
||||||
|
print(f"\n🎉 Все тесты завершены успешно!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_features())
|
||||||
53
test_display_type.py
Normal file
53
test_display_type.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тест для проверки смены типа отображения победителей
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
from database import async_session_maker
|
||||||
|
from services import LotteryService
|
||||||
|
from winner_display import validate_display_type
|
||||||
|
|
||||||
|
async def test_display_type_change():
|
||||||
|
"""Тестируем смену типа отображения"""
|
||||||
|
print("🧪 Тестирование смены типа отображения...")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Получаем первый розыгрыш
|
||||||
|
lottery = await LotteryService.get_lottery(session, 1)
|
||||||
|
if not lottery:
|
||||||
|
print("❌ Розыгрыш не найден")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📋 Розыгрыш: {lottery.title}")
|
||||||
|
print(f"🎭 Текущий тип: {getattr(lottery, 'winner_display_type', 'username')}")
|
||||||
|
|
||||||
|
# Проверяем валидацию разных типов
|
||||||
|
test_types = ['username', 'chat_id', 'account_number', 'invalid_type']
|
||||||
|
|
||||||
|
for test_type in test_types:
|
||||||
|
print(f"\n🧪 Тестируем тип: {test_type}")
|
||||||
|
is_valid = validate_display_type(test_type)
|
||||||
|
print(f" Валидация: {'✅' if is_valid else '❌'}")
|
||||||
|
|
||||||
|
if is_valid:
|
||||||
|
# Пытаемся изменить тип
|
||||||
|
success = await LotteryService.set_winner_display_type(session, lottery.id, test_type)
|
||||||
|
print(f" Сохранение: {'✅' if success else '❌'}")
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Проверяем что изменение применилось
|
||||||
|
updated_lottery = await LotteryService.get_lottery(session, lottery.id)
|
||||||
|
actual_type = getattr(updated_lottery, 'winner_display_type', 'username')
|
||||||
|
print(f" Новый тип: {actual_type}")
|
||||||
|
|
||||||
|
if actual_type == test_type:
|
||||||
|
print(f" Результат: ✅ Тип успешно изменен")
|
||||||
|
else:
|
||||||
|
print(f" Результат: ❌ Тип не изменился (ожидали {test_type}, получили {actual_type})")
|
||||||
|
else:
|
||||||
|
print(f" Результат: ❌ Ошибка при сохранении")
|
||||||
|
else:
|
||||||
|
print(f" Результат: ⚠️ Неверный тип (ожидаемо)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_display_type_change())
|
||||||
277
test_new_features.py
Normal file
277
test_new_features.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
"""
|
||||||
|
Тест новой функциональности: клиентские счета и многопоточность
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к проекту
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from database import init_db, async_session_maker
|
||||||
|
from services import UserService, LotteryService
|
||||||
|
from account_utils import generate_account_number, validate_account_number, mask_account_number
|
||||||
|
from task_manager import task_manager, TaskPriority
|
||||||
|
from winner_display import format_winner_display, validate_display_type
|
||||||
|
from async_decorators import get_task_stats, format_task_stats
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
async def test_account_functionality():
|
||||||
|
"""Тест функций работы со счетами"""
|
||||||
|
print("🧪 Тестирование функций работы со счетами")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Создаём тестовых пользователей
|
||||||
|
users_data = [
|
||||||
|
{
|
||||||
|
'telegram_id': 777000001,
|
||||||
|
'username': 'client1',
|
||||||
|
'first_name': 'Клиент',
|
||||||
|
'last_name': 'Первый',
|
||||||
|
'account': '12-34-56-78-90-12-34-56'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'telegram_id': 777000002,
|
||||||
|
'username': 'client2',
|
||||||
|
'first_name': 'Клиент',
|
||||||
|
'last_name': 'Второй',
|
||||||
|
'account': '11-22-33-44-55-66-77-88'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'telegram_id': 777000003,
|
||||||
|
'username': 'client3',
|
||||||
|
'first_name': 'Клиент',
|
||||||
|
'last_name': 'Третий',
|
||||||
|
'account': None # Без счёта
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
created_users = []
|
||||||
|
|
||||||
|
for user_data in users_data:
|
||||||
|
# Создаём пользователя
|
||||||
|
user = await UserService.get_or_create_user(
|
||||||
|
session,
|
||||||
|
telegram_id=user_data['telegram_id'],
|
||||||
|
username=user_data['username'],
|
||||||
|
first_name=user_data['first_name'],
|
||||||
|
last_name=user_data['last_name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Устанавливаем номер счёта, если есть
|
||||||
|
if user_data['account']:
|
||||||
|
success = await UserService.set_account_number(
|
||||||
|
session, user.telegram_id, user_data['account']
|
||||||
|
)
|
||||||
|
if success:
|
||||||
|
print(f"✅ Создан пользователь {user.first_name} со счётом {user_data['account']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка установки счёта для {user.first_name}")
|
||||||
|
else:
|
||||||
|
print(f"✅ Создан пользователь {user.first_name} без счёта")
|
||||||
|
|
||||||
|
created_users.append(user)
|
||||||
|
|
||||||
|
# Тесты валидации
|
||||||
|
print("\n📋 Тестирование валидации номеров счетов:")
|
||||||
|
|
||||||
|
test_numbers = [
|
||||||
|
("12-34-56-78-90-12-34-56", True), # Корректный
|
||||||
|
("12345678901234567890123456", False), # Без дефисов
|
||||||
|
("12-34-56-78-90-12-34", False), # Короткий
|
||||||
|
("12-34-56-78-90-12-34-56-78", False), # Длинный
|
||||||
|
("ab-cd-ef-gh-ij-kl-mn-op", False), # Буквы
|
||||||
|
("", False) # Пустой
|
||||||
|
]
|
||||||
|
|
||||||
|
for number, should_be_valid in test_numbers:
|
||||||
|
is_valid = validate_account_number(number)
|
||||||
|
status = "✅" if is_valid == should_be_valid else "❌"
|
||||||
|
print(f"{status} {number} -> {'Корректный' if is_valid else 'Некорректный'}")
|
||||||
|
|
||||||
|
# Тест маскирования
|
||||||
|
print("\n🎭 Тестирование маскирования номеров:")
|
||||||
|
test_account = "12-34-56-78-90-12-34-56"
|
||||||
|
for digits in [4, 6, 8]:
|
||||||
|
masked = mask_account_number(test_account, show_last_digits=digits)
|
||||||
|
print(f"Показать последние {digits} цифр: {masked}")
|
||||||
|
|
||||||
|
# Тест поиска по счёту
|
||||||
|
print("\n🔍 Тестирование поиска по номеру счёта:")
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Полный номер
|
||||||
|
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34-56")
|
||||||
|
if user:
|
||||||
|
print(f"✅ Найден пользователь по полному номеру: {user.first_name}")
|
||||||
|
|
||||||
|
# Поиск по части номера
|
||||||
|
users = await UserService.search_by_account(session, "12-34")
|
||||||
|
print(f"🔍 Найдено пользователей по шаблону '12-34': {len(users)}")
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if user.account_number:
|
||||||
|
masked = mask_account_number(user.account_number, show_last_digits=6)
|
||||||
|
print(f" • {user.first_name}: {masked}")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_display_types():
|
||||||
|
"""Тест различных типов отображения победителей"""
|
||||||
|
print("\n🎨 Тестирование типов отображения победителей")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Создаём розыгрыш
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="Тест отображения победителей",
|
||||||
|
description="Розыгрыш для тестирования различных типов отображения",
|
||||||
|
prizes=["Первый приз", "Второй приз"],
|
||||||
|
creator_id=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем пользователя со счётом
|
||||||
|
user = await UserService.get_user_by_account(session, "12-34-56-78-90-12-34-56")
|
||||||
|
|
||||||
|
if user and lottery:
|
||||||
|
print(f"👤 Тестируем отображение для пользователя: {user.first_name}")
|
||||||
|
print(f"💳 Номер счёта: {user.account_number}")
|
||||||
|
|
||||||
|
# Тестируем разные типы отображения
|
||||||
|
display_types = ['username', 'chat_id', 'account_number']
|
||||||
|
|
||||||
|
for display_type in display_types:
|
||||||
|
# Устанавливаем тип отображения
|
||||||
|
success = await LotteryService.set_winner_display_type(
|
||||||
|
session, lottery.id, display_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# Получаем обновлённый розыгрыш
|
||||||
|
updated_lottery = await LotteryService.get_lottery(session, lottery.id)
|
||||||
|
|
||||||
|
# Тестируем отображение
|
||||||
|
public_display = format_winner_display(user, updated_lottery, show_sensitive_data=False)
|
||||||
|
admin_display = format_winner_display(user, updated_lottery, show_sensitive_data=True)
|
||||||
|
|
||||||
|
print(f"\n📺 Тип отображения: {display_type}")
|
||||||
|
print(f" 👥 Публичное: {public_display}")
|
||||||
|
print(f" 🔧 Админское: {admin_display}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка установки типа отображения {display_type}")
|
||||||
|
else:
|
||||||
|
print("❌ Не удалось создать тестовые данные")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_multithreading():
|
||||||
|
"""Тест многопоточности"""
|
||||||
|
print("\n⚡ Тестирование системы многопоточности")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Запускаем менеджер задач
|
||||||
|
await task_manager.start()
|
||||||
|
|
||||||
|
# Функция-заглушка для тестирования
|
||||||
|
async def test_task(task_name: str, duration: float, user_id_arg: int):
|
||||||
|
print(f"🔄 Запуск задачи {task_name} для пользователя {user_id_arg}")
|
||||||
|
await asyncio.sleep(duration)
|
||||||
|
print(f"✅ Завершена задача {task_name} для пользователя {user_id_arg}")
|
||||||
|
return f"Результат {task_name}"
|
||||||
|
|
||||||
|
# Добавляем задачи разных приоритетов
|
||||||
|
tasks = [
|
||||||
|
("critical_task", 2.0, 100, TaskPriority.CRITICAL),
|
||||||
|
("high_task_1", 1.5, 200, TaskPriority.HIGH),
|
||||||
|
("normal_task_1", 1.0, 300, TaskPriority.NORMAL),
|
||||||
|
("normal_task_2", 1.2, 300, TaskPriority.NORMAL),
|
||||||
|
("high_task_2", 0.8, 200, TaskPriority.HIGH),
|
||||||
|
("low_task", 0.5, 400, TaskPriority.LOW)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"📤 Добавляем {len(tasks)} задач...")
|
||||||
|
|
||||||
|
for task_name, duration, user_id, priority in tasks:
|
||||||
|
try:
|
||||||
|
task_id = await task_manager.add_task(
|
||||||
|
task_id=f"{task_name}_{int(time.time())}",
|
||||||
|
user_id=user_id,
|
||||||
|
func=test_task,
|
||||||
|
priority=priority,
|
||||||
|
timeout=10.0,
|
||||||
|
task_name=task_name,
|
||||||
|
duration=duration,
|
||||||
|
user_id_arg=user_id
|
||||||
|
)
|
||||||
|
print(f" ➕ Добавлена задача {task_name} (приоритет: {priority.name})")
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" ❌ Ошибка добавления задачи {task_name}: {e}")
|
||||||
|
|
||||||
|
# Показываем статистику
|
||||||
|
print("\n📊 Начальная статистика:")
|
||||||
|
stats_text = await format_task_stats()
|
||||||
|
print(stats_text)
|
||||||
|
|
||||||
|
# Ждём выполнения задач
|
||||||
|
print("\n⏳ Ожидание выполнения задач...")
|
||||||
|
await asyncio.sleep(5.0)
|
||||||
|
|
||||||
|
# Показываем финальную статистику
|
||||||
|
print("\n📈 Финальная статистика:")
|
||||||
|
final_stats = await format_task_stats()
|
||||||
|
print(final_stats)
|
||||||
|
|
||||||
|
# Тестируем лимиты пользователя
|
||||||
|
print("\n🚧 Тестирование лимитов пользователя:")
|
||||||
|
|
||||||
|
# Пробуем добавить много задач одному пользователю
|
||||||
|
user_id = 999
|
||||||
|
added_tasks = 0
|
||||||
|
|
||||||
|
for i in range(8): # Больше чем лимит (5)
|
||||||
|
try:
|
||||||
|
await task_manager.add_task(
|
||||||
|
task_id=f"limit_test_{i}_{int(time.time())}",
|
||||||
|
user_id=user_id,
|
||||||
|
func=test_task,
|
||||||
|
priority=TaskPriority.NORMAL,
|
||||||
|
task_name=f"limit_test_{i}",
|
||||||
|
duration=0.1,
|
||||||
|
user_id_arg=user_id
|
||||||
|
)
|
||||||
|
added_tasks += 1
|
||||||
|
except ValueError as e:
|
||||||
|
print(f" 🛑 Лимит достигнут после {added_tasks} задач: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Ждём немного и останавливаем менеджер
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
await task_manager.stop()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Главная функция теста"""
|
||||||
|
print("🚀 Запуск тестирования новой функциональности")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Инициализируем базу данных
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Запускаем тесты
|
||||||
|
await test_account_functionality()
|
||||||
|
await test_display_types()
|
||||||
|
await test_multithreading()
|
||||||
|
|
||||||
|
print("\n🎉 Все тесты завершены успешно!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Ошибка во время тестирования: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
124
utils.py
Normal file
124
utils.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Утилиты для управления ботом
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from database import async_session_maker, init_db
|
||||||
|
from services import UserService
|
||||||
|
from config import ADMIN_IDS
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_admin_users():
|
||||||
|
"""Установить права администратора для пользователей из ADMIN_IDS"""
|
||||||
|
if not ADMIN_IDS:
|
||||||
|
print("❌ Список ADMIN_IDS пуст")
|
||||||
|
return
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
for admin_id in ADMIN_IDS:
|
||||||
|
success = await UserService.set_admin(session, admin_id, True)
|
||||||
|
if success:
|
||||||
|
print(f"✅ Права администратора установлены для ID: {admin_id}")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Пользователь с ID {admin_id} не найден в базе")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sample_lottery():
|
||||||
|
"""Создать пример розыгрыша для тестирования"""
|
||||||
|
from services import LotteryService
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Берем первого администратора как создателя
|
||||||
|
if not ADMIN_IDS:
|
||||||
|
print("❌ Нет администраторов для создания розыгрыша")
|
||||||
|
return
|
||||||
|
|
||||||
|
admin_user = await UserService.get_user_by_telegram_id(session, ADMIN_IDS[0])
|
||||||
|
if not admin_user:
|
||||||
|
print("❌ Пользователь-администратор не найден в базе")
|
||||||
|
return
|
||||||
|
|
||||||
|
lottery = await LotteryService.create_lottery(
|
||||||
|
session,
|
||||||
|
title="🎉 Тестовый розыгрыш",
|
||||||
|
description="Это тестовый розыгрыш для демонстрации работы бота",
|
||||||
|
prizes=[
|
||||||
|
"🥇 Главный приз - 10,000 рублей",
|
||||||
|
"🥈 Второй приз - iPhone 15",
|
||||||
|
"🥉 Третий приз - AirPods Pro"
|
||||||
|
],
|
||||||
|
creator_id=admin_user.id
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✅ Создан тестовый розыгрыш с ID: {lottery.id}")
|
||||||
|
print(f"📝 Название: {lottery.title}")
|
||||||
|
|
||||||
|
|
||||||
|
async def init_database():
|
||||||
|
"""Инициализация базы данных"""
|
||||||
|
print("🔄 Инициализация базы данных...")
|
||||||
|
await init_db()
|
||||||
|
print("✅ База данных инициализирована")
|
||||||
|
|
||||||
|
|
||||||
|
async def show_stats():
|
||||||
|
"""Показать статистику бота"""
|
||||||
|
from services import LotteryService, ParticipationService
|
||||||
|
from models import User, Lottery, Participation
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
# Количество пользователей
|
||||||
|
result = await session.execute(select(func.count(User.id)))
|
||||||
|
users_count = result.scalar()
|
||||||
|
|
||||||
|
# Количество розыгрышей
|
||||||
|
result = await session.execute(select(func.count(Lottery.id)))
|
||||||
|
lotteries_count = result.scalar()
|
||||||
|
|
||||||
|
# Количество активных розыгрышей
|
||||||
|
result = await session.execute(
|
||||||
|
select(func.count(Lottery.id))
|
||||||
|
.where(Lottery.is_active == True, Lottery.is_completed == False)
|
||||||
|
)
|
||||||
|
active_lotteries = result.scalar()
|
||||||
|
|
||||||
|
# Количество участий
|
||||||
|
result = await session.execute(select(func.count(Participation.id)))
|
||||||
|
participations_count = result.scalar()
|
||||||
|
|
||||||
|
print("\n📊 Статистика бота:")
|
||||||
|
print(f"👥 Всего пользователей: {users_count}")
|
||||||
|
print(f"🎲 Всего розыгрышей: {lotteries_count}")
|
||||||
|
print(f"🟢 Активных розыгрышей: {active_lotteries}")
|
||||||
|
print(f"🎫 Всего участий: {participations_count}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Главная функция утилиты"""
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Использование:")
|
||||||
|
print(" python utils.py init - Инициализация базы данных")
|
||||||
|
print(" python utils.py setup-admins - Установка прав администратора")
|
||||||
|
print(" python utils.py sample - Создание тестового розыгрыша")
|
||||||
|
print(" python utils.py stats - Показать статистику")
|
||||||
|
return
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
if command == "init":
|
||||||
|
asyncio.run(init_database())
|
||||||
|
elif command == "setup-admins":
|
||||||
|
asyncio.run(setup_admin_users())
|
||||||
|
elif command == "sample":
|
||||||
|
asyncio.run(create_sample_lottery())
|
||||||
|
elif command == "stats":
|
||||||
|
asyncio.run(show_stats())
|
||||||
|
else:
|
||||||
|
print(f"❌ Неизвестная команда: {command}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
116
winner_display.py
Normal file
116
winner_display.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Утилиты для отображения информации о победителях в зависимости от настроек розыгрыша
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from models import User, Lottery
|
||||||
|
from account_utils import mask_account_number
|
||||||
|
|
||||||
|
|
||||||
|
def format_winner_display(user: User, lottery: Lottery, show_sensitive_data: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует отображение победителя в зависимости от настроек розыгрыша
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: Пользователь-победитель
|
||||||
|
lottery: Розыгрыш
|
||||||
|
show_sensitive_data: Показывать ли чувствительные данные (для админов)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка для отображения победителя
|
||||||
|
"""
|
||||||
|
display_type = getattr(lottery, 'winner_display_type', 'username')
|
||||||
|
|
||||||
|
if display_type == 'username':
|
||||||
|
# Отображаем username или имя
|
||||||
|
if user.username:
|
||||||
|
return f"@{user.username}"
|
||||||
|
else:
|
||||||
|
return user.first_name or f"Пользователь {user.id}"
|
||||||
|
|
||||||
|
elif display_type == 'chat_id':
|
||||||
|
# Отображаем Telegram ID
|
||||||
|
return f"ID: {user.telegram_id}"
|
||||||
|
|
||||||
|
elif display_type == 'account_number':
|
||||||
|
# Отображаем номер клиентского счета
|
||||||
|
if not user.account_number:
|
||||||
|
return "Счёт не указан"
|
||||||
|
|
||||||
|
if show_sensitive_data:
|
||||||
|
# Для админов показываем полный номер
|
||||||
|
return f"Счёт: {user.account_number}"
|
||||||
|
else:
|
||||||
|
# Для публичного показа маскируем номер
|
||||||
|
masked = mask_account_number(user.account_number, show_last_digits=4)
|
||||||
|
return f"Счёт: {masked}"
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Fallback к username/имени
|
||||||
|
if user.username:
|
||||||
|
return f"@{user.username}"
|
||||||
|
else:
|
||||||
|
return user.first_name or f"Пользователь {user.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_winner_info(winner_data: Dict[str, Any], show_sensitive_data: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует информацию о победителе из данных розыгрыша
|
||||||
|
|
||||||
|
Args:
|
||||||
|
winner_data: Словарь с данными о победителе
|
||||||
|
show_sensitive_data: Показывать ли чувствительные данные
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Отформатированная строка для отображения
|
||||||
|
"""
|
||||||
|
user = winner_data.get('user')
|
||||||
|
place = winner_data.get('place', 1)
|
||||||
|
prize = winner_data.get('prize', f'Приз {place} места')
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return f"{place}. Победитель не определен"
|
||||||
|
|
||||||
|
# Пробуем получить lottery из winner_data, если есть
|
||||||
|
lottery = winner_data.get('lottery')
|
||||||
|
|
||||||
|
if lottery:
|
||||||
|
winner_display = format_winner_display(user, lottery, show_sensitive_data)
|
||||||
|
else:
|
||||||
|
# Fallback если нет данных о розыгрыше
|
||||||
|
if user.username:
|
||||||
|
winner_display = f"@{user.username}"
|
||||||
|
else:
|
||||||
|
winner_display = user.first_name or f"Пользователь {user.id}"
|
||||||
|
|
||||||
|
return f"{place}. {winner_display}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_type_name(display_type: str) -> str:
|
||||||
|
"""
|
||||||
|
Получить человекочитаемое название типа отображения
|
||||||
|
|
||||||
|
Args:
|
||||||
|
display_type: Тип отображения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Название типа
|
||||||
|
"""
|
||||||
|
types = {
|
||||||
|
'username': 'Username/Имя',
|
||||||
|
'chat_id': 'Telegram ID',
|
||||||
|
'account_number': 'Номер счёта'
|
||||||
|
}
|
||||||
|
return types.get(display_type, 'Неизвестно')
|
||||||
|
|
||||||
|
|
||||||
|
def validate_display_type(display_type: str) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет корректность типа отображения
|
||||||
|
|
||||||
|
Args:
|
||||||
|
display_type: Тип отображения для проверки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если тип корректен
|
||||||
|
"""
|
||||||
|
return display_type in ['username', 'chat_id', 'account_number']
|
||||||
Reference in New Issue
Block a user