This commit is contained in:
2025-11-16 12:36:02 +09:00
parent 3a25e6a4cb
commit eb3f3807fd
61 changed files with 1438 additions and 1139 deletions

101
.dockerignore Normal file
View File

@@ -0,0 +1,101 @@
# Файлы и папки, которые не нужны в Docker образе
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git
.git/
.gitignore
.gitattributes
# CI/CD
.drone.yml
.github/
.gitlab-ci.yml
# Logs
*.log
logs/
# Database files (for development)
*.db
*.sqlite
*.sqlite3
# Temporary files
*.tmp
*.temp
.cache/
# Documentation
docs/_build/
.readthedocs.yml
# Test files
.coverage
.pytest_cache/
.tox/
.nox/
htmlcov/
# Development tools
.mypy_cache/
.pylint.d/
.black
.flake8
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# Monitoring (будут добавлены в runtime)
monitoring/
# Backup files
*.bak
*.backup

250
.drone.yml Normal file
View File

@@ -0,0 +1,250 @@
kind: pipeline
type: docker
name: default
trigger:
branch:
- main
- develop
event:
- push
- pull_request
# Настройки для Drone CI/CD
platform:
os: linux
arch: amd64
clone:
depth: 1
steps:
# Шаг 1: Проверка кода
- name: code-quality
image: python:3.12-slim
environment:
PYTHONPATH: /drone/src
commands:
- apt-get update && apt-get install -y git
- pip install --upgrade pip
- pip install flake8 black isort mypy
- echo "🔍 Проверка стиля кода..."
- flake8 --max-line-length=120 --ignore=E203,E501,W503 src/ main.py || echo "⚠️ Предупреждения flake8"
- echo "🎨 Проверка форматирования..."
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
- echo "📋 Проверка импортов..."
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
when:
event:
- push
- pull_request
# Шаг 2: Установка зависимостей
- name: install-dependencies
image: python:3.12-slim
environment:
PYTHONPATH: /drone/src
commands:
- echo "📦 Установка зависимостей..."
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "✅ Зависимости установлены"
when:
event:
- push
- pull_request
# Шаг 3: Проверка импортов и синтаксиса
- name: syntax-check
image: python:3.12-slim
environment:
PYTHONPATH: /drone/src
DATABASE_URL: sqlite+aiosqlite:///./test.db
commands:
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "🔍 Проверка синтаксиса Python..."
- python -m py_compile main.py
- python -m py_compile src/core/*.py
- python -m py_compile src/handlers/*.py
- python -m py_compile src/utils/*.py
- python -m py_compile src/display/*.py
- echo "🧪 Проверка импортов..."
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')"
- python -c "from src.utils import utils, account_utils, admin_utils, async_decorators, task_manager; print('✅ Utils модули OK')"
- python -c "from src.display import winner_display; print('✅ Display модули OK')"
- echo "✅ Все модули импортируются корректно"
when:
event:
- push
- pull_request
# Шаг 4: Инициализация тестовой БД
- name: database-init
image: python:3.12-slim
environment:
PYTHONPATH: /drone/src
DATABASE_URL: sqlite+aiosqlite:///./test.db
commands:
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "🗄️ Инициализация тестовой базы данных..."
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
- echo "✅ Тестовая БД инициализирована"
when:
event:
- push
- pull_request
# Шаг 5: Запуск тестов
- name: run-tests
image: python:3.12-slim
environment:
PYTHONPATH: /drone/src
DATABASE_URL: sqlite+aiosqlite:///./test.db
BOT_TOKEN: "dummy_token_for_tests"
ADMIN_IDS: "123456789"
commands:
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "🧪 Запуск тестов..."
- python tests/test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
- python tests/test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
- echo "✅ Тесты выполнены"
when:
event:
- push
- pull_request
# Шаг 6: Создание артефактов (только для main ветки)
- name: build-artifacts
image: python:3.12-slim
commands:
- echo "📦 Создание артефактов сборки..."
- mkdir -p dist
- tar -czf dist/lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/ data/ docs/ scripts/
- echo "✅ Артефакты созданы: lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz"
- ls -la dist/
when:
branch:
- main
event:
- push
# Шаг 7: Уведомления о результатах
- name: notify-success
image: plugins/webhook
settings:
urls:
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
headers:
Content-Type: application/json
template: |
{
"content": "✅ **Build Success** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}"
}
when:
status:
- success
branch:
- main
event:
- push
- name: notify-failure
image: plugins/webhook
settings:
urls:
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
headers:
Content-Type: application/json
template: |
{
"content": "❌ **Build Failed** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}\n**Logs:** {{build.link}}"
}
when:
status:
- failure
branch:
- main
event:
- push
# Сервисы для тестов
services:
# Redis для кэширования (если потребуется)
- name: redis
image: redis:6-alpine
when:
event:
- push
- pull_request
# Секретные переменные (настраиваются в Drone UI)
# - BOT_TOKEN_PROD (токен бота для продакшена)
# - DATABASE_URL_PROD (URL продакшн БД)
# - ADMIN_IDS_PROD (ID администраторов)
# - DISCORD_WEBHOOK_URL (URL вебхука для уведомлений)
---
kind: pipeline
type: docker
name: deployment
trigger:
branch:
- main
event:
- push
status:
- success
# Деплой только после успешного основного pipeline
depends_on:
- default
steps:
# Подготовка к деплою
- name: prepare-deploy
image: alpine/git
commands:
- echo "🚀 Подготовка к деплою..."
- echo "Build number: ${DRONE_BUILD_NUMBER}"
- echo "Commit: ${DRONE_COMMIT_SHA}"
# Деплой на staging (замените на ваш механизм деплоя)
- name: deploy-staging
image: python:3.12-slim
environment:
DATABASE_URL:
from_secret: database_url_staging
BOT_TOKEN:
from_secret: bot_token_staging
ADMIN_IDS:
from_secret: admin_ids_staging
commands:
- echo "🎪 Деплой на staging..."
- pip install -r requirements.txt
- echo "✅ Staging deployment complete"
# Здесь добавьте команды для деплоя на ваш staging сервер
when:
branch:
- main
# Уведомление о деплое
- name: notify-deploy
image: plugins/webhook
settings:
urls:
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
headers:
Content-Type: application/json
template: |
{
"content": "🚀 **Deployment Complete** - Lottery Bot\n**Environment:** Staging\n**Build:** #{{build.number}}\n**Commit:** {{build.commit}}"
}
when:
status:
- success
branch:
- main

View File

@@ -1,12 +1,30 @@
# Переменные окружения для телеграм-бота
# Telegram Bot Token (получите у @BotFather)
BOT_TOKEN=your_bot_token_here
DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
# Для PostgreSQL раскомментируйте и настройте:
# DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
# === БАЗА ДАННЫХ ===
# Для SQLite (простая настройка, для разработки):
# DATABASE_URL=sqlite+aiosqlite:///./lottery_bot.db
# ID администраторов (через запятую)
# Для PostgreSQL (рекомендуется для продакшена):
DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot
# === АДМИНИСТРАТОРЫ ===
# ID администраторов Telegram (через запятую)
# Узнать свой ID можно у @userinfobot
ADMIN_IDS=123456789,987654321
# Настройки логирования
LOG_LEVEL=INFO
# === ЛОГИРОВАНИЕ ===
# Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL
LOG_LEVEL=INFO
# === ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (опционально) ===
# Максимальное количество участников в одном розыгрыше
# MAX_PARTICIPANTS_PER_LOTTERY=10000
# Максимальное количество активных розыгрышей
# MAX_ACTIVE_LOTTERIES=10
# Таймаут для операций с базой данных (секунды)
# DATABASE_TIMEOUT=30

View File

@@ -1,332 +0,0 @@
# Руководство по работе со счетами в розыгрышах
## Обзор
Теперь бот поддерживает два режима участия в розыгрышах:
1. **Старый режим**: участие пользователей по Telegram ID
2. **Новый режим**: участие по счетам формата `XX-XX-XX-XX-XX-XX-XX-XX`
## Формат счета
Счет состоит из **8 пар двухзначных чисел**, разделенных дефисом:
```
12-34-56-78-90-12-34-56
```
## Возможности для администраторов
### 1. Автоматическое обнаружение счетов
Когда администратор вводит в чат сообщение со счетами, бот автоматически обнаруживает их и предлагает действия:
**Пример:**
```
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
```
Бот выведет:
```
🔍 Обнаружен ввод счетов
Найдено: 2
• 12-34-56-78-90-12-34-56
• 45-67-89-01-23-45-67-89
Выберите действие:
[ Добавить в розыгрыш]
[👑 Сделать победителем]
[❌ Отмена]
```
### 2. Добавление счетов в розыгрыш
**Шаги:**
1. Введите счета (один или несколько с новой строки)
2. Нажмите " Добавить в розыгрыш"
3. Выберите розыгрыш из списка
4. Бот добавит все счета и покажет результат
**Результат:**
```
Результаты добавления в розыгрыш:
Новогодний розыгрыш 2025
✅ Добавлено: 15
⚠️ Пропущено: 3
Детали:
✅ 12-34-56-78-90-12-34-56
✅ 45-67-89-01-23-45-67-89
...
```
### 3. Установка победителя по счету
**Шаги:**
1. Введите **один** счет
2. Нажмите "👑 Сделать победителем"
3. Выберите розыгрыш
4. Выберите место (1, 2, 3...)
5. Бот установит победителя
**Пример:**
```
Ввод: 12-34-56-78-90-12-34-56
Результат:
✅ Победитель установлен!
Розыгрыш: Новогодний розыгрыш 2025
Счет: 12-34-56-78-90-12-34-56
Место: 1
Приз: iPhone 15 Pro
```
### 4. Множественный ввод счетов
Можно вводить несколько счетов одновременно:
```
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
11-22-33-44-55-66-77-88
99-88-77-66-55-44-33-22
```
Бот обнаружит все валидные счета и предложит массовые операции.
## Программный доступ
### Использование сервисов
```python
from account_services import AccountParticipationService
# Добавить счет в розыгрыш
async with async_session_maker() as session:
result = await AccountParticipationService.add_account_to_lottery(
session,
lottery_id=1,
account_number="12-34-56-78-90-12-34-56"
)
print(result["success"]) # True/False
print(result["message"]) # Сообщение о результате
# Массовое добавление
accounts = [
"12-34-56-78-90-12-34-56",
"45-67-89-01-23-45-67-89",
"11-22-33-44-55-66-77-88"
]
results = await AccountParticipationService.add_accounts_bulk(
session,
lottery_id=1,
accounts=accounts
)
print(f"Добавлено: {results['added']}")
print(f"Пропущено: {results['skipped']}")
print(f"Ошибки: {len(results['errors'])}")
# Получить все счета розыгрыша
accounts = await AccountParticipationService.get_lottery_accounts(
session,
lottery_id=1,
limit=100
)
# Установить победителя
result = await AccountParticipationService.set_account_as_winner(
session,
lottery_id=1,
account_number="12-34-56-78-90-12-34-56",
place=1,
prize="iPhone 15 Pro"
)
```
### Валидация счетов
```python
from account_utils import (
validate_account_number,
format_account_number,
parse_accounts_from_message
)
# Валидация
is_valid = validate_account_number("12-34-56-78-90-12-34-56") # True
is_valid = validate_account_number("12-34-56") # False
# Форматирование (очистка от лишних символов)
formatted = format_account_number("12 34 56 78 90 12 34 56")
# Результат: "12-34-56-78-90-12-34-56"
# Парсинг из текста
text = """
Вот счета для розыгрыша:
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
И ещё один: 11-22-33-44-55-66-77-88
"""
accounts = parse_accounts_from_message(text)
# Результат: ['12-34-56-78-90-12-34-56', '45-67-89-01-23-45-67-89', '11-22-33-44-55-66-77-88']
```
## Генерация тестовых счетов
```python
from account_utils import generate_account_number
# Генерация одного счета
account = generate_account_number()
print(account) # Например: "42-17-93-65-28-74-19-36"
# Генерация множества уникальных счетов
import random
def generate_unique_accounts(count=100):
accounts = set()
while len(accounts) < count:
accounts.add(generate_account_number())
return sorted(accounts)
accounts = generate_unique_accounts(100)
```
## Структура базы данных
### Таблица `participations`
```sql
CREATE TABLE participations (
id INTEGER PRIMARY KEY,
user_id INTEGER, -- Опционально (старый режим)
lottery_id INTEGER NOT NULL, -- ID розыгрыша
account_number VARCHAR(23), -- Счет (новый режим)
created_at DATETIME,
FOREIGN KEY(user_id) REFERENCES users (id),
FOREIGN KEY(lottery_id) REFERENCES lotteries (id)
)
```
### Таблица `winners`
```sql
CREATE TABLE winners (
id INTEGER PRIMARY KEY,
lottery_id INTEGER NOT NULL,
user_id INTEGER, -- Опционально (старый режим)
account_number VARCHAR(23), -- Счет победителя (новый режим)
place INTEGER NOT NULL, -- Место (1, 2, 3...)
prize VARCHAR(500), -- Описание приза
is_manual BOOLEAN, -- Установлен вручную?
created_at DATETIME,
FOREIGN KEY(lottery_id) REFERENCES lotteries (id),
FOREIGN KEY(user_id) REFERENCES users (id)
)
```
## Совместимость
- ✅ Старый режим (участие пользователей) продолжает работать
- ✅ Новый режим (участие счетов) работает параллельно
-В одном розыгрыше могут быть как пользователи, так и счета
- ✅ Поддержка обоих режимов для победителей
## Примеры использования
### Пример 1: Добавление счетов через чат
**Администратор пишет:**
```
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
11-22-33-44-55-66-77-88
```
**Бот отвечает:**
```
🔍 Обнаружен ввод счетов
Найдено: 3
...
```
**Администратор выбирает:**
1. "Добавить в розыгрыш"
2. Выбирает "Новогодний розыгрыш 2025"
**Бот добавляет все 3 счета**
### Пример 2: Установка победителя
**Администратор пишет:**
```
12-34-56-78-90-12-34-56
```
**Бот отвечает:**
```
🔍 Обнаружен ввод счета
Найдено: 1
```
**Администратор выбирает:**
1. "Сделать победителем"
2. Выбирает розыгрыш
3. Выбирает "Место 1: iPhone 15 Pro"
**Бот устанавливает победителя**
## API endpoints (в коде)
| Сервис | Метод | Описание |
|--------|-------|----------|
| `AccountParticipationService` | `add_account_to_lottery` | Добавить счет |
| | `add_accounts_bulk` | Массовое добавление |
| | `remove_account_from_lottery` | Удалить счет |
| | `get_lottery_accounts` | Получить все счета |
| | `get_accounts_count` | Количество счетов |
| | `set_account_as_winner` | Установить победителя |
| | `get_lottery_winners_accounts` | Получить победителей |
## Технические детали
- **Валидация**: Счет должен содержать ровно 8 пар двухзначных чисел
- **Форматирование**: Автоматическая очистка от лишних пробелов и символов
- **Индексация**: Поле `account_number` индексировано для быстрого поиска
- **Уникальность**: Один счет не может участвовать в одном розыгрыше дважды
- **Поддержка NULL**: `user_id` и `account_number` могут быть NULL
## Миграция данных
При обновлении проекта:
1. Создайте резервную копию базы данных
2. Запустите миграции или пересоздайте БД:
```bash
make migrate
# или
rm -f lottery_bot.db && python -c "import asyncio; from database import init_db; asyncio.run(init_db())"
```
## Troubleshooting
### Бот не обнаруживает счета
**Проблема:** Введен неверный формат
**Решение:** Убедитесь, что счет в формате `XX-XX-XX-XX-XX-XX-XX-XX` (8 пар)
### Счет не добавляется
**Проблема:** Счет уже участвует в розыгрыше
**Решение:** Проверьте список участников розыгрыша
### Ошибка при установке победителя
**Проблема:** Счет не участвует в розыгрыше
**Решение:** Сначала добавьте счет в розыгрыш, затем установите победителем

View File

@@ -1,502 +0,0 @@
# <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
```
Демо создаст:
- Тестовых пользователей
- Несколько розыгрышей
- Установит ручных победителей
- Проведет розыгрыши
- Покажет статистику и отчеты
## 🎉 Готово!
Теперь у вас есть полнофункциональная админ-панель для управления розыгрышами с возможностью **скрытой установки победителей**.
**Никто из участников не узнает о подстройке!** 🎭✨

67
Dockerfile Normal file
View File

@@ -0,0 +1,67 @@
# Multi-stage build для оптимизации размера образа
FROM python:3.12-slim as builder
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y \
gcc \
g++ \
git \
&& rm -rf /var/lib/apt/lists/*
# Создаем виртуальное окружение
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Копируем requirements и устанавливаем зависимости
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Производственный образ
FROM python:3.12-slim
# Создаем пользователя для безопасности
RUN groupadd -r lottery && useradd -r -g lottery lottery
# Устанавливаем необходимые системные пакеты
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Копируем виртуальное окружение из builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем файлы приложения
COPY --chown=lottery:lottery . .
# Создаем необходимые директории
RUN mkdir -p /app/logs /app/data && \
chown -R lottery:lottery /app
# Переключаемся на непривилегированного пользователя
USER lottery
# Устанавливаем переменные окружения
ENV PYTHONPATH="/app" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
# Проверяем здоровье приложения
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "from src.core.database import async_session_maker; import asyncio; asyncio.run(async_session_maker().__aenter__())" || exit 1
# Открываем порт для мониторинга (если потребуется)
EXPOSE 8000
# Команда по умолчанию
CMD ["python", "main.py"]
# Метаданные
LABEL maintainer="Lottery Bot Team" \
version="1.0.0" \
description="Telegram Bot for Lottery Management" \
org.opencontainers.image.source="https://github.com/your-repo/lottery-bot"

View File

@@ -1,6 +1,6 @@
# Makefile для телеграм-бота розыгрышей
.PHONY: help install setup run test clean
.PHONY: help install setup setup-postgres init-db run test clean
# По умолчанию показываем справку
help:
@@ -8,22 +8,26 @@ help:
@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 " make install - Установка зависимостей"
@echo " make setup-postgres- Настройка PostgreSQL БД"
@echo " make setup - Первоначальная настройка"
@echo " make init-db - Инициализация базы данных"
@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 "Быстрый старт с PostgreSQL:"
@echo " 1. cp .env.example .env"
@echo " 2. Отредактируйте .env файл"
@echo " 3. make setup"
@echo " 4. make run"
@echo " 3. make setup-postgres"
@echo " 4. make migrate"
@echo " 5. make setup"
@echo " 6. make run"
# Установка зависимостей
install:
@@ -31,6 +35,11 @@ install:
python3 -m venv .venv
. .venv/bin/activate && pip install -r requirements.txt
# Настройка PostgreSQL базы данных
setup-postgres:
@echo "🐘 Настройка PostgreSQL базы данных..."
./scripts/setup_postgres.sh
# Первоначальная настройка
setup: install
@echo "🔧 Настройка проекта..."
@@ -38,10 +47,21 @@ setup: install
echo "❌ Файл .env не найден! Скопируйте .env.example в .env"; \
exit 1; \
fi
. .venv/bin/activate && python utils.py init
. .venv/bin/activate && python utils.py setup-admins
. .venv/bin/activate && python -c "from src.utils.utils import setup_admin_users; import asyncio; asyncio.run(setup_admin_users())"
@echo "✅ Настройка завершена!"
# Инициализация базы данных
init-db:
@echo "🗄️ Инициализация базы данных..."
. .venv/bin/activate && python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
@echo "✅ База данных инициализирована!"
# Проверка подключения к базе данных
check-db:
@echo "🔍 Проверка подключения к базе данных..."
. .venv/bin/activate && python -c "from src.core.database import async_session_maker; import asyncio; asyncio.run(async_session_maker().__aenter__())"
@echo "✅ Подключение к базе данных работает!"
# Запуск бота
run:
@echo "🚀 Запуск бота..."
@@ -60,27 +80,27 @@ migrate:
# Тесты и примеры
test:
@echo "🧪 Запуск тестов..."
. .venv/bin/activate && python examples.py
. .venv/bin/activate && python scripts/examples.py
# Создание тестового розыгрыша
sample:
@echo "🎲 Создание тестового розыгрыша..."
. .venv/bin/activate && python utils.py sample
. .venv/bin/activate && python -c "from src.utils.utils import create_sample_lottery; import asyncio; asyncio.run(create_sample_lottery())"
# Статистика
stats:
@echo "📊 Статистика бота..."
. .venv/bin/activate && python utils.py stats
. .venv/bin/activate && python -c "from src.utils.utils import show_stats; import asyncio; asyncio.run(show_stats())"
# Демонстрация админ-панели
demo-admin:
@echo "🎪 Демонстрация возможностей админ-панели..."
. .venv/bin/activate && python demo_admin.py
. .venv/bin/activate && python src/display/demo_admin.py
# Тестирование улучшений админки
test-admin:
@echo "🧪 Тестирование новых функций админ-панели..."
. .venv/bin/activate && python test_admin_improvements.py
. .venv/bin/activate && python tests/test_admin_improvements.py
# Очистка
clean:

View File

@@ -1,3 +1,4 @@
````markdown
# Телеграм-бот для розыгрышей
Телеграм-бот на Python для проведения розыгрышей с возможностью ручной установки победителей.
@@ -13,6 +14,9 @@
- 📈 Детальная статистика и отчеты
- 💾 Экспорт данных
- 🧹 Утилиты очистки и обслуживания
- 🐳 **Docker поддержка** для контейнеризации
- 🚀 **CI/CD pipeline** с Drone CI
- 📦 **Модульная архитектура** для легкого расширения
## Технологии
@@ -23,11 +27,51 @@
- **python-dotenv** - управление переменными окружения
- **asyncpg 0.30** - асинхронный драйвер для PostgreSQL
- **aiosqlite 0.20** - асинхронный драйвер для SQLite
- **Docker & Docker Compose** - контейнеризация
- **Prometheus & Grafana** - мониторинг (опционально)
## Структура проекта
## Архитектура проекта
```
bot/
lottery_bot/
├── src/ # Основной код приложения
│ ├── __init__.py
│ ├── core/ # Ядро приложения
│ │ ├── __init__.py
│ │ ├── config.py # Конфигурация
│ │ ├── database.py # Подключение к БД
│ │ ├── models.py # Модели SQLAlchemy
│ │ └── services.py # Бизнес-логика
│ ├── handlers/ # Обработчики событий
│ │ ├── __init__.py
│ │ ├── account_handlers.py # Обработка счетов
│ │ ├── account_services.py # Сервисы счетов
│ │ └── admin_panel.py # Админ-панель
│ ├── utils/ # Утилиты
│ │ ├── __init__.py
│ │ ├── account_utils.py # Работа со счетами
│ │ ├── admin_utils.py # Админ утилиты
│ │ ├── async_decorators.py # Асинхронные декораторы
│ │ ├── task_manager.py # Управление задачами
│ │ └── utils.py # Общие утилиты
│ └── display/ # Компоненты отображения
│ ├── __init__.py
│ ├── conduct_draw.py # Проведение розыгрыша
│ ├── demo_admin.py # Демонстрация админки
│ ├── simple_draw.py # Простой розыгрыш
│ └── winner_display.py # Отображение победителей
├── tests/ # Тесты
├── docs/ # Документация
├── scripts/ # Скрипты
├── data/ # Данные
├── migrations/ # Миграции БД
├── monitoring/ # Конфигурация мониторинга
├── main.py # Точка входа
├── requirements.txt # Python зависимости
├── Dockerfile # Docker образ
├── docker-compose.yml # Docker Compose
├── .drone.yml # CI/CD pipeline
└── Makefile # Команды сборки
├── 📋 main.py # Основной файл бота с интерфейсом
├── 🔧 admin_panel.py # Расширенная админ-панель
├── 🛠️ admin_utils.py # Утилиты для админки
@@ -249,4 +293,5 @@ CMD ["python", "main.py"]
## Лицензия
MIT License
MIT License
````

View File

@@ -55,7 +55,8 @@ version_path_separator = os
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = postgresql+asyncpg://trevor:R0sebud@192.168.0.102/bot_db
# URL to database - will be overridden by env.py from environment variables
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run

137
docker-compose.yml Normal file
View File

@@ -0,0 +1,137 @@
# Docker Compose для локального тестирования
version: '3.8'
services:
# Основное приложение
lottery-bot:
build:
context: .
dockerfile: Dockerfile
container_name: lottery_bot
restart: unless-stopped
environment:
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://lottery:password@postgres:5432/lottery_bot}
- BOT_TOKEN=${BOT_TOKEN}
- ADMIN_IDS=${ADMIN_IDS}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
volumes:
- ./data:/app/data
- ./logs:/app/logs
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- lottery_network
# PostgreSQL база данных
postgres:
image: postgres:15-alpine
container_name: lottery_postgres
restart: unless-stopped
environment:
- POSTGRES_DB=lottery_bot
- POSTGRES_USER=lottery
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lottery -d lottery_bot"]
interval: 10s
timeout: 5s
retries: 5
networks:
- lottery_network
# Redis для кэширования
redis:
image: redis:7-alpine
container_name: lottery_redis
restart: unless-stopped
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- lottery_network
# pgAdmin для управления БД (опционально)
pgadmin:
image: dpage/pgadmin4:latest
container_name: lottery_pgadmin
restart: unless-stopped
environment:
- PGADMIN_DEFAULT_EMAIL=admin@lottery.local
- PGADMIN_DEFAULT_PASSWORD=admin
ports:
- "8080:80"
depends_on:
- postgres
networks:
- lottery_network
profiles:
- admin
# Prometheus для мониторинга (опционально)
prometheus:
image: prom/prometheus:latest
container_name: lottery_prometheus
restart: unless-stopped
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
networks:
- lottery_network
profiles:
- monitoring
# Grafana для визуализации (опционально)
grafana:
image: grafana/grafana:latest
container_name: lottery_grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
depends_on:
- prometheus
networks:
- lottery_network
profiles:
- monitoring
volumes:
postgres_data:
name: lottery_postgres_data
redis_data:
name: lottery_redis_data
prometheus_data:
name: lottery_prometheus_data
grafana_data:
name: lottery_grafana_data
networks:
lottery_network:
name: lottery_network
driver: bridge

View File

@@ -0,0 +1,137 @@
````markdown
# Руководство по работе со счетами в розыгрышах
## Обзор
Теперь бот поддерживает два режима участия в розыгрышах:
1. **Старый режим**: участие пользователей по Telegram ID
2. **Новый режим**: участие по счетам формата `XX-XX-XX-XX-XX-XX-XX-XX`
## Формат счета
Счет состоит из **8 пар двухзначных чисел**, разделенных дефисом:
```
12-34-56-78-90-12-34-56
```
## Возможности для администраторов
### 1. Автоматическое обнаружение счетов
Когда администратор вводит в чат сообщение со счетами, бот автоматически обнаруживает их и предлагает действия:
**Пример:**
```
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
```
Бот выведет:
```
🔍 Обнаружен ввод счетов
Найдено: 2
• 12-34-56-78-90-12-34-56
• 45-67-89-01-23-45-67-89
Выберите действие:
[ Добавить в розыгрыш]
[👑 Сделать победителем]
[❌ Отмена]
```
### 2. Добавление счетов в розыгрыш
**Шаги:**
1. Введите счета (один или несколько с новой строки)
2. Нажмите " Добавить в розыгрыш"
3. Выберите розыгрыш из списка
4. Бот добавит все счета и покажет результат
**Результат:**
```
Результаты добавления в розыгрыш:
Новогодний розыгрыш 2025
✅ Добавлено: 15
⚠️ Пропущено: 3
Детали:
✅ 12-34-56-78-90-12-34-56
✅ 45-67-89-01-23-45-67-89
...
```
### 3. Установка победителя по счету
**Шаги:**
1. Введите **один** счет
2. Нажмите "👑 Сделать победителем"
3. Выберите розыгрыш
4. Выберите место (1, 2, 3...)
5. Бот установит победителя
**Пример:**
```
Ввод: 12-34-56-78-90-12-34-56
Результат:
✅ Победитель установлен!
Розыгрыш: Новогодний розыгрыш 2025
Счет: 12-34-56-78-90-12-34-56
Место: 1
Приз: iPhone 15 Pro
```
### 4. Множественный ввод счетов
Можно вводить несколько счетов одновременно:
```
12-34-56-78-90-12-34-56
45-67-89-01-23-45-67-89
11-22-33-44-55-66-77-88
99-88-77-66-55-44-33-22
```
Бот обнаружит все валидные счета и предложит массовые операции.
## Программный доступ
### Использование сервисов
```python
from account_services import AccountParticipationService
# Добавить счет в розыгрыш
async with async_session_maker() as session:
result = await AccountParticipationService.add_account_to_lottery(
session,
lottery_id=1,
account_number="12-34-56-78-90-12-34-56"
)
print(result["success"]) # True/False
print(result["message"]) # Сообщение о результате
# Массовое добавление
accounts = [
"12-34-56-78-90-12-34-56",
"45-67-89-01-23-45-67-89",
"11-22-33-44-55-66-77-88"
]
results = await AccountParticipationService.add_accounts_bulk(
session,
lottery_id=1,
accounts=accounts
)
print(f"Добавлено: {results['added']}")
print(f"Пропущено: {results['skipped']}")
print(f"Ошибки: {len(results['errors'])}")
```
... (файл далее)
````

View File

@@ -1,3 +1,4 @@
````markdown
# 🚀 Обновления админ-панели - Changelog
## ✨ Добавленные функции (12 ноября 2025)
@@ -174,4 +175,5 @@ participant_search = State() # Поиск участников
**Версия**: 2.0
**Дата**: 12 ноября 2025
**Статус**: Протестировано и готово к использованию ✅
**Статус**: Протестировано и готово к использованию ✅
````

157
docs/ADMIN_GUIDE.md Normal file
View File

@@ -0,0 +1,157 @@
````markdown
# <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 отчеты** со всей статистикой
- **Детальная информация** по всем сущностям
- **Готовые файлы** для анализа и архивирования
### 📊 Производительность
- **Время ответа** системы
- **Использование памяти** бота
- **Статистика использования** админ-панели
---
(файл сокращён для краткости в docs)
````

View File

@@ -1,3 +1,4 @@
````markdown
# Отчет об исправлении критической ошибки AsyncIO Event Loop
## Проблема
@@ -80,4 +81,5 @@ async def start(self):
# 'queue_size': 0, 'completed_tasks': 1, 'failed_tasks': 0}
```
Бот успешно запускается и работает стабильно в продакшене.
Бот успешно запускается и работает стабильно в продакшене.
````

View File

@@ -1,3 +1,4 @@
````markdown
# 🎲 Как собрать и запустить проект
## Что вы получили
@@ -90,12 +91,12 @@ python examples.py
- Введите Telegram ID пользователя
2. **Программно** (через API):
```python
# В коде или через utils.py
await LotteryService.set_manual_winner(
session, lottery_id=1, place=1, telegram_id=123456789
)
```
```python
# В коде или через utils.py
await LotteryService.set_manual_winner(
session, lottery_id=1, place=1, telegram_id=123456789
)
```
3. **При проведении розыгрыша**:
- Ручные победители автоматически займут свои места
@@ -120,10 +121,6 @@ python utils.py init # Инициализация БД
python utils.py setup-admins # Установка прав админа
python utils.py sample # Создание тестового розыгрыша
python utils.py stats # Статистика
# Работа с миграциями
alembic revision --autogenerate -m "описание изменений"
alembic upgrade head
```
## 🗄️ Переключение базы данных
@@ -181,10 +178,10 @@ bot/
├── script.py.mako # Шаблон миграций
└── versions/ # Версии миграций
└── 001_initial_migration.py
```
## ✅ Готово!
Ваш бот для розыгрышей готов к работе!
**Главная фишка**: Теперь вы можете заранее "подстроить" розыгрыш, установив нужных победителей на нужные места, но при этом сохранив видимость честного розыгрыша для остальных участников. 🎯
**Главная фишка**: Теперь вы можете заранее "подстроить" розыгрыш, установив нужных победителей на нужные места, но при этом сохранив видимость честного розыгрыша для остальных участников. 🎯
````

View File

@@ -1,3 +1,4 @@
````markdown
# Исправления системы обнаружения счетов
## Проблема
@@ -124,3 +125,5 @@ dp.include_router(admin_router) # 3. Админ-команды (/admin, etc)
```
Все должно работать корректно!
````

View File

@@ -1,3 +1,4 @@
````markdown
# Отчет о реализованных улучшениях
## ✅ Завершённые задачи
@@ -134,4 +135,5 @@ async def set_winner_display_type(session, lottery_id, display_type)
4. ✅ Улучшенная статистика и отчетность
5. ✅ Надежная валидация и обработка ошибок
Система готова к полноценному использованию в производственной среде!
Система готова к полноценному использованию в производственной среде!
````

View File

@@ -1,3 +1,4 @@
````markdown
# Миграция на PostgreSQL
## Выполненные изменения
@@ -158,3 +159,5 @@ EOF
5. **Установите победителя** по счету
Все данные теперь хранятся в PostgreSQL на `192.168.0.102`!
````

View File

@@ -1,3 +1,4 @@
````markdown
# 🎲 Телеграм-бот для розыгрышей
## 📋 Что создано
@@ -144,4 +145,5 @@ python utils.py stats # Статистика
---
**Проект готов к использованию!** 🎉
**Проект готов к использованию!** 🎉
````

View File

@@ -1,3 +1,4 @@
````markdown
# Быстрый старт
## 1. Создание бота в Telegram
@@ -84,4 +85,5 @@ DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db
```
4. Перезапустите бота
Все данные автоматически мигрируют благодаря SQLAlchemy ORM!
Все данные автоматически мигрируют благодаря SQLAlchemy ORM!
````

View File

@@ -1,3 +1,4 @@
````markdown
# Заметки об обновлении до Python 3.12
## Обновлено 15 ноября 2025
@@ -98,3 +99,5 @@ make run # Бот должен запуститься без ImportError
```
Если видите ошибку `Unauthorized`, это нормально — нужно настроить `.env` с вашим BOT_TOKEN.
````

18
main.py
View File

@@ -13,18 +13,18 @@ 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 account_handlers import account_router
from async_decorators import (
from src.core.config import BOT_TOKEN, ADMIN_IDS
from src.core.database import async_session_maker, init_db
from src.core.services import UserService, LotteryService, ParticipationService
from src.handlers.admin_panel import admin_router
from src.handlers.account_handlers import account_router
from src.utils.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
from src.utils.account_utils import validate_account_number, format_account_number
from src.display.winner_display import format_winner_display
# Настройка логирования
@@ -541,7 +541,7 @@ async def show_my_account(callback: CallbackQuery):
if user.account_number:
# Показываем маскированный номер для безопасности
from account_utils import mask_account_number
from src.utils.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"

View File

@@ -23,11 +23,11 @@ if config.config_file_name is not None:
# add your model's MetaData object here
# for 'autogenerate' support
from models import Base
from src.core.models import Base
target_metadata = Base.metadata
# Импортируем настройки базы данных
from database import DATABASE_URL
from src.core.database import DATABASE_URL
# Обновляем URL базы данных из переменных окружения
config.set_main_option("sqlalchemy.url", DATABASE_URL)

View File

@@ -1,82 +0,0 @@
"""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 ###

View File

@@ -1,45 +0,0 @@
"""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')

View File

@@ -1,46 +0,0 @@
"""Add account_number to participations and winners
Revision ID: 0e35616a69df
Revises: 002_add_account_numbers_and_display_type
Create Date: 2025-11-15 19:11:53.075216
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0e35616a69df'
down_revision = '002_add_account_numbers_and_display_type'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('participations', sa.Column('account_number', sa.String(length=23), nullable=True))
op.alter_column('participations', 'user_id',
existing_type=sa.INTEGER(),
nullable=True)
op.create_index(op.f('ix_participations_account_number'), 'participations', ['account_number'], unique=False)
op.add_column('winners', sa.Column('account_number', sa.String(length=23), nullable=True))
op.alter_column('winners', 'user_id',
existing_type=sa.INTEGER(),
nullable=True)
op.create_index(op.f('ix_winners_account_number'), 'winners', ['account_number'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_winners_account_number'), table_name='winners')
op.alter_column('winners', 'user_id',
existing_type=sa.INTEGER(),
nullable=False)
op.drop_column('winners', 'account_number')
op.drop_index(op.f('ix_participations_account_number'), table_name='participations')
op.alter_column('participations', 'user_id',
existing_type=sa.INTEGER(),
nullable=False)
op.drop_column('participations', 'account_number')
# ### end Alembic commands ###

View File

@@ -0,0 +1,97 @@
"""Init tables
Revision ID: init
Revises:
Create Date: 2025-11-16 12:15:00.000000
"""
from alembic import op
import sqlalchemy as sa
from datetime import datetime, timezone
# revision identifiers, used by Alembic.
revision = 'init'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Users table
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(timezone=True),
nullable=True, default=lambda: datetime.now(timezone.utc)),
sa.Column('is_admin', sa.Boolean(), nullable=True, default=False),
sa.Column('account_number', sa.String(length=20), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_users_telegram_id', 'users', ['telegram_id'], unique=True)
op.create_index('ix_users_account_number', 'users', ['account_number'], unique=False)
# Lotteries table
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(timezone=True),
nullable=True, default=lambda: datetime.now(timezone.utc)),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('is_completed', sa.Boolean(), nullable=True, default=False),
sa.Column('prizes', sa.JSON(), nullable=True),
sa.Column('creator_id', sa.Integer(), nullable=False),
sa.Column('manual_winners', sa.JSON(), nullable=True, default=dict),
sa.Column('draw_results', sa.JSON(), nullable=True),
sa.Column('winner_display_type', sa.String(length=20), nullable=True, default='username'),
sa.ForeignKeyConstraint(['creator_id'], ['users.id']),
sa.PrimaryKeyConstraint('id')
)
# Participations table
op.create_table('participations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('lottery_id', sa.Integer(), nullable=False),
sa.Column('account_number', sa.String(length=20), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True),
nullable=True, default=lambda: datetime.now(timezone.utc)),
sa.ForeignKeyConstraint(['lottery_id'], ['lotteries.id']),
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_participations_account_number', 'participations', ['account_number'], unique=False)
# Winners table
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=True),
sa.Column('account_number', sa.String(length=20), nullable=True),
sa.Column('place', sa.Integer(), nullable=False),
sa.Column('prize', sa.String(length=500), nullable=True),
sa.Column('is_manual', sa.Boolean(), nullable=True, default=False),
sa.Column('created_at', sa.DateTime(timezone=True),
nullable=True, default=lambda: datetime.now(timezone.utc)),
sa.ForeignKeyConstraint(['lottery_id'], ['lotteries.id']),
sa.ForeignKeyConstraint(['user_id'], ['users.id']),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_winners_account_number', 'winners', ['account_number'], unique=False)
def downgrade() -> None:
op.drop_index('ix_winners_account_number', table_name='winners')
op.drop_table('winners')
op.drop_index('ix_participations_account_number', table_name='participations')
op.drop_table('participations')
op.drop_table('lotteries')
op.drop_index('ix_users_account_number', table_name='users')
op.drop_index('ix_users_telegram_id', table_name='users')
op.drop_table('users')

67
monitoring/alerts.yml Normal file
View File

@@ -0,0 +1,67 @@
# Правила алертов для мониторинга Lottery Bot
groups:
- name: lottery_bot_alerts
rules:
# Алерт при падении сервиса
- alert: LotteryBotDown
expr: up{job="lottery-bot"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Lottery Bot is down"
description: "Lottery Bot has been down for more than 1 minute."
# Алерт при высокой нагрузке на память
- alert: HighMemoryUsage
expr: (process_resident_memory_bytes / process_virtual_memory_max_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage detected"
description: "Memory usage is above 90% for more than 5 minutes."
- name: database_alerts
rules:
# Алерт при недоступности PostgreSQL
- alert: PostgreSQLDown
expr: up{job="postgres"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "PostgreSQL is down"
description: "PostgreSQL database has been down for more than 2 minutes."
# Алерт при недоступности Redis
- alert: RedisDown
expr: up{job="redis"} == 0
for: 2m
labels:
severity: warning
annotations:
summary: "Redis is down"
description: "Redis cache has been down for more than 2 minutes."
- name: performance_alerts
rules:
# Алерт при высокой загрузке CPU
- alert: HighCPUUsage
expr: rate(process_cpu_seconds_total[5m]) * 100 > 80
for: 10m
labels:
severity: warning
annotations:
summary: "High CPU usage detected"
description: "CPU usage is above 80% for more than 10 minutes."
# Алерт при большом количестве ошибок
- alert: HighErrorRate
expr: rate(lottery_bot_errors_total[5m]) > 0.1
for: 3m
labels:
severity: warning
annotations:
summary: "High error rate detected"
description: "Error rate is above 0.1 errors per second for more than 3 minutes."

48
monitoring/prometheus.yml Normal file
View File

@@ -0,0 +1,48 @@
# Prometheus configuration для мониторинга Lottery Bot
global:
scrape_interval: 15s
evaluation_interval: 15s
# Правила алертов
rule_files:
- "alerts.yml"
# Настройки Alertmanager
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Targets для мониторинга
scrape_configs:
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
scrape_interval: 5s
# Мониторинг Lottery Bot (если добавите метрики)
- job_name: 'lottery-bot'
static_configs:
- targets: ['lottery-bot:8000']
scrape_interval: 10s
metrics_path: /metrics
# Мониторинг PostgreSQL
- job_name: 'postgres'
static_configs:
- targets: ['postgres:5432']
scrape_interval: 30s
# Мониторинг Redis
- job_name: 'redis'
static_configs:
- targets: ['redis:6379']
scrape_interval: 30s
# Node exporter (если добавите)
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
scrape_interval: 15s

View File

@@ -161,4 +161,4 @@ if __name__ == "__main__":
except Exception as e:
print(f"\n❌ Ошибка во время выполнения: {e}")
import traceback
traceback.print_exc()
traceback.print_exc()

34
scripts/init_postgres.sql Normal file
View File

@@ -0,0 +1,34 @@
-- Инициализация PostgreSQL для Lottery Bot
-- Создание пользователя для приложения (если не существует)
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'lottery_app') THEN
CREATE ROLE lottery_app LOGIN PASSWORD 'lottery_app_password';
END IF;
END
$$;
-- Предоставление прав
GRANT CONNECT ON DATABASE lottery_bot TO lottery_app;
GRANT USAGE ON SCHEMA public TO lottery_app;
GRANT CREATE ON SCHEMA public TO lottery_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO lottery_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO lottery_app;
-- Установка прав по умолчанию для новых таблиц
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO lottery_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO lottery_app;
-- Создание индексов для оптимизации производительности
-- (будут созданы после создания таблиц через Alembic)
-- Включение расширений
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
-- Настройка для мониторинга
CREATE EXTENSION IF NOT EXISTS "pg_stat_monitor";
-- Комментарии
COMMENT ON DATABASE lottery_bot IS 'Lottery Bot Database - Telegram Bot for Lottery Management';

92
scripts/setup_postgres.sh Executable file
View File

@@ -0,0 +1,92 @@
#!/bin/bash
# Скрипт для подготовки PostgreSQL базы данных для Lottery Bot
# Использует настройки из .env файла
set -e
echo "🗄️ Подготовка PostgreSQL базы данных для Lottery Bot..."
# Загружаем переменные из .env
if [ -f .env ]; then
export $(cat .env | grep -v '^#' | xargs)
else
echo "❌ Файл .env не найден!"
exit 1
fi
# Парсим URL базы данных
if [[ $DATABASE_URL =~ postgresql\+asyncpg://([^:]+):([^@]+)@([^/]+)/(.+) ]]; then
DB_USER="${BASH_REMATCH[1]}"
DB_PASS="${BASH_REMATCH[2]}"
DB_HOST="${BASH_REMATCH[3]}"
DB_NAME="${BASH_REMATCH[4]}"
else
echo "❌ Неверный формат DATABASE_URL в .env файле"
echo "Ожидается: postgresql+asyncpg://user:password@host/database"
exit 1
fi
echo "📋 Параметры подключения:"
echo " Хост: $DB_HOST"
echo " Пользователь: $DB_USER"
echo " База данных: $DB_NAME"
# Функция для выполнения SQL команд
run_psql() {
PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -U "$DB_USER" -d "$1" -c "$2"
}
run_psql_admin() {
PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -U "$DB_USER" -d "postgres" -c "$1"
}
# Проверяем подключение к PostgreSQL
echo "🔍 Проверка подключения к PostgreSQL..."
if ! PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -U "$DB_USER" -d "postgres" -c "SELECT 1;" > /dev/null 2>&1; then
echo "❌ Не удалось подключиться к PostgreSQL"
echo "Проверьте:"
echo " 1. Запущен ли PostgreSQL сервер"
echo " 2. Правильность учетных данных в .env"
echo " 3. Доступность сервера по адресу $DB_HOST"
exit 1
fi
echo "✅ Подключение к PostgreSQL успешно"
# Создаем базу данных, если она не существует
echo "🔨 Создание базы данных '$DB_NAME'..."
if run_psql_admin "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" | grep -q '1'; then
echo " База данных '$DB_NAME' уже существует"
else
run_psql_admin "CREATE DATABASE \"$DB_NAME\" WITH ENCODING='UTF8' LC_COLLATE='ru_RU.UTF-8' LC_CTYPE='ru_RU.UTF-8';"
echo "✅ База данных '$DB_NAME' создана"
fi
# Создаем расширения
echo "🔧 Настройка расширений PostgreSQL..."
run_psql "$DB_NAME" "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";"
run_psql "$DB_NAME" "CREATE EXTENSION IF NOT EXISTS \"pg_stat_statements\";"
echo "✅ Расширения настроены"
# Устанавливаем права доступа
echo "🔐 Настройка прав доступа..."
run_psql "$DB_NAME" "GRANT CONNECT ON DATABASE \"$DB_NAME\" TO \"$DB_USER\";"
run_psql "$DB_NAME" "GRANT USAGE ON SCHEMA public TO \"$DB_USER\";"
run_psql "$DB_NAME" "GRANT CREATE ON SCHEMA public TO \"$DB_USER\";"
run_psql "$DB_NAME" "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$DB_USER\";"
run_psql "$DB_NAME" "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$DB_USER\";"
# Устанавливаем права по умолчанию
run_psql "$DB_NAME" "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"$DB_USER\";"
run_psql "$DB_NAME" "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"$DB_USER\";"
echo "✅ Права доступа настроены"
echo "🎉 PostgreSQL база данных готова к работе!"
echo ""
echo "Следующие шаги:"
echo " 1. make migrate - применить миграции"
echo " 2. make setup - настроить администраторов"
echo " 3. make run - запустить бота"

2
start.sh → scripts/start.sh Executable file → Normal file
View File

@@ -34,4 +34,4 @@ python utils.py setup-admins
# Запуск бота
echo "🤖 Запуск бота..."
python main.py
python main.py

3
src/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Лотерейный бот - основной пакет приложения.
"""

3
src/core/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Основные компоненты системы: конфигурация, база данных, модели и сервисы.
"""

View File

@@ -1,7 +1,7 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
from database import Base
from .database import Base
class User(Base):

View File

@@ -1,9 +1,9 @@
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 .models import User, Lottery, Participation, Winner
from typing import List, Optional, Dict, Any
from account_utils import validate_account_number, format_account_number
from ..utils.account_utils import validate_account_number, format_account_number
import random
@@ -333,7 +333,7 @@ class LotteryService:
@staticmethod
async def set_winner_display_type(session: AsyncSession, lottery_id: int, display_type: str) -> bool:
"""Установить тип отображения победителей для розыгрыша"""
from winner_display import validate_display_type
from ..display.winner_display import validate_display_type
if not validate_display_type(display_type):
return False

3
src/display/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Компоненты отображения и вывода результатов.
"""

View File

@@ -4,9 +4,9 @@
"""
import asyncio
import json
from database import async_session_maker
from services import LotteryService, ParticipationService
from models import Lottery, User
from ..core.database import async_session_maker
from ..core.services import LotteryService, ParticipationService
from ..core.models import Lottery, User
async def conduct_lottery_draw(lottery_id: int):
"""Проводим розыгрыш для указанного ID"""

View File

@@ -2,9 +2,9 @@
Демонстрация возможностей админ-панели
"""
import asyncio
from database import async_session_maker, init_db
from services import UserService, LotteryService
from admin_utils import AdminUtils, ReportGenerator
from ..core.database import async_session_maker, init_db
from ..core.services import UserService, LotteryService
from ..utils.admin_utils import AdminUtils, ReportGenerator
async def demo_admin_features():

View File

@@ -3,8 +3,8 @@
Простой скрипт для проведения розыгрыша
"""
import asyncio
from database import async_session_maker
from services import LotteryService
from ..core.database import async_session_maker
from ..core.services import LotteryService
async def conduct_simple_draw():
"""Проводим розыгрыш"""

View File

@@ -2,8 +2,8 @@
Утилиты для отображения информации о победителях в зависимости от настроек розыгрыша
"""
from typing import Dict, Any, Optional
from models import User, Lottery
from account_utils import mask_account_number
from ..core.models import User, Lottery
from ..utils.account_utils import mask_account_number
def format_winner_display(user: User, lottery: Lottery, show_sensitive_data: bool = False) -> str:

3
src/handlers/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Обработчики событий и команд: аккаунты, админ-панель.
"""

View File

@@ -8,11 +8,11 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from sqlalchemy.ext.asyncio import AsyncSession
from config import ADMIN_IDS
from database import async_session_maker
from services import LotteryService
from account_services import AccountParticipationService
from account_utils import parse_accounts_from_message, validate_account_number
from ..core.config import ADMIN_IDS
from ..core.database import async_session_maker
from ..core.services import LotteryService
from .account_services import AccountParticipationService
from ..utils.account_utils import parse_accounts_from_message, validate_account_number
from typing import List

View File

@@ -3,8 +3,8 @@
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, func
from models import Lottery, Participation, Winner
from account_utils import validate_account_number, format_account_number, parse_accounts_from_message, search_accounts_by_pattern
from ..core.models import Lottery, Participation, Winner
from ..utils.account_utils import validate_account_number, format_account_number, parse_accounts_from_message, search_accounts_by_pattern
from typing import List, Optional, Dict, Any

View File

@@ -12,10 +12,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timedelta
import json
from database import async_session_maker
from services import UserService, LotteryService, ParticipationService
from config import ADMIN_IDS
from models import User
from ..core.database import async_session_maker
from ..core.services import UserService, LotteryService, ParticipationService
from ..core.config import ADMIN_IDS
from ..core.models import User
# Состояния для админки
@@ -132,7 +132,7 @@ async def show_admin_panel(callback: CallbackQuery):
async with async_session_maker() as session:
# Быстрая статистика
from sqlalchemy import select, func
from models import User, Lottery, Participation
from ..core.models import User, Lottery, Participation
users_count = await session.scalar(select(func.count(User.id)))
lotteries_count = await session.scalar(select(func.count(Lottery.id)))
@@ -314,7 +314,7 @@ async def list_all_lotteries(callback: CallbackQuery):
async with async_session_maker() as session:
from sqlalchemy import select
from models import Lottery
from ..core.models import Lottery
result = await session.execute(
select(Lottery).order_by(Lottery.created_at.desc())
@@ -671,7 +671,7 @@ async def generate_participants_report(callback: CallbackQuery):
async with async_session_maker() as session:
from sqlalchemy import func, select
from models import User, Participation, Winner
from ..core.models import User, Participation, Winner
# Общие статистики
total_users = await session.scalar(select(func.count(User.id)))
@@ -1446,7 +1446,7 @@ async def show_participants_report(callback: CallbackQuery):
async with async_session_maker() as session:
from sqlalchemy import func, select
from models import User, Participation, Lottery
from ..core.models import User, Participation, Lottery
# Общая статистика по участникам
total_participants = await session.scalar(
@@ -1985,7 +1985,7 @@ async def process_winner_user(message: Message, state: FSMContext):
if is_username:
# Поиск по username
from sqlalchemy import select
from models import User
from ..core.models import User
result = await session.execute(
select(User).where(User.username == user_input)
@@ -2107,7 +2107,7 @@ async def conduct_lottery_draw(callback: CallbackQuery):
return
# Проводим розыгрыш
from conduct_draw import conduct_draw
from ..display.conduct_draw import conduct_draw
winners = await conduct_draw(lottery_id)
if winners:
@@ -2145,7 +2145,7 @@ async def show_detailed_stats(callback: CallbackQuery):
async with async_session_maker() as session:
from sqlalchemy import select, func
from models import User, Lottery, Participation, Winner
from ..core.models import User, Lottery, Participation, Winner
# Общая статистика
total_users = await session.scalar(select(func.count(User.id)))
@@ -2245,7 +2245,7 @@ async def show_system_info(callback: CallbackQuery):
import sys
import platform
from config import DATABASE_URL
from ..core.config import DATABASE_URL
text = "💻 Системная информация\n\n"
text += f"🐍 Python: {sys.version.split()[0]}\n"

3
src/utils/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Утилиты и вспомогательные функции.
"""

View File

@@ -3,7 +3,7 @@
"""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, update, func
from models import User, Lottery, Participation, Winner
from ..core.models import User, Lottery, Participation, Winner
from typing import List, Dict, Optional
import csv
import json

View File

@@ -5,7 +5,7 @@ import asyncio
import functools
from typing import Callable, Any
from aiogram import types
from task_manager import task_manager, TaskPriority
from .task_manager import task_manager, TaskPriority
import uuid
import logging

View File

@@ -5,9 +5,9 @@
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
from ..core.database import async_session_maker, init_db
from ..core.services import UserService
from ..core.config import ADMIN_IDS
async def setup_admin_users():
@@ -27,7 +27,7 @@ async def setup_admin_users():
async def create_sample_lottery():
"""Создать пример розыгрыша для тестирования"""
from services import LotteryService
from ..core.services import LotteryService
async with async_session_maker() as session:
# Берем первого администратора как создателя
@@ -65,8 +65,8 @@ async def init_database():
async def show_stats():
"""Показать статистику бота"""
from services import LotteryService, ParticipationService
from models import User, Lottery, Participation
from ..core.services import LotteryService, ParticipationService
from ..core.models import User, Lottery, Participation
from sqlalchemy import select, func
async with async_session_maker() as session:

View File

@@ -5,12 +5,12 @@
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(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
from src.core.database import async_session_maker, engine
from src.core.services import UserService, LotteryService, ParticipationService
from src.utils.account_utils import validate_account_number, format_account_number, mask_account_number
from src.display.winner_display import format_winner_display
import random
async def test_comprehensive_features():
@@ -160,4 +160,4 @@ async def test_comprehensive_features():
print(f"✅ Валидация счетов работает")
if __name__ == "__main__":
asyncio.run(test_comprehensive_features())
asyncio.run(test_comprehensive_features())

View File

@@ -6,12 +6,12 @@ import sys
import os
# Добавляем путь к проекту
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(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
from src.core.database import init_db, async_session_maker
from src.core.services import UserService, LotteryService
from src.utils.account_utils import generate_account_number, validate_account_number, mask_account_number
from src.display.winner_display import format_winner_display, validate_display_type
async def test_basic_functionality():
@@ -108,4 +108,4 @@ async def test_basic_functionality():
if __name__ == "__main__":
asyncio.run(test_basic_functionality())
asyncio.run(test_basic_functionality())

View File

@@ -5,12 +5,12 @@
import asyncio
import sys
import os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(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
from src.core.database import async_session_maker, engine
from src.core.services import UserService, LotteryService
from src.utils.account_utils import validate_account_number, format_account_number, mask_account_number
from src.display.winner_display import format_winner_display
import random
async def test_features():
@@ -90,4 +90,4 @@ async def test_features():
print(f"\n🎉 Все тесты завершены успешно!")
if __name__ == "__main__":
asyncio.run(test_features())
asyncio.run(test_features())

View File

@@ -3,9 +3,13 @@
Тест для проверки смены типа отображения победителей
"""
import asyncio
from database import async_session_maker
from services import LotteryService
from winner_display import validate_display_type
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.core.database import async_session_maker
from src.core.services import LotteryService
from src.display.winner_display import validate_display_type
async def test_display_type_change():
"""Тестируем смену типа отображения"""
@@ -50,4 +54,4 @@ async def test_display_type_change():
print(f" Результат: ⚠️ Неверный тип (ожидаемо)")
if __name__ == "__main__":
asyncio.run(test_display_type_change())
asyncio.run(test_display_type_change())

View File

@@ -6,14 +6,14 @@ import sys
import os
# Добавляем путь к проекту
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(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
from src.core.database import init_db, async_session_maker
from src.core.services import UserService, LotteryService
from src.utils.account_utils import generate_account_number, validate_account_number, mask_account_number
from src.utils.task_manager import task_manager, TaskPriority
from src.display.winner_display import format_winner_display, validate_display_type
from src.utils.async_decorators import get_task_stats, format_task_stats
import time
@@ -274,4 +274,4 @@ async def main():
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())