From eb3f3807fdffd248d0c1cc1e51bfce9b5d130ec8 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Sun, 16 Nov 2025 12:36:02 +0900 Subject: [PATCH] refactor --- .dockerignore | 101 ++++ .drone.yml | 250 +++++++++ .env.example | 30 +- ACCOUNT_SYSTEM_GUIDE.md | 332 ------------ ADMIN_GUIDE.md | 502 ------------------ Dockerfile | 67 +++ Makefile | 62 ++- README.md | 51 +- alembic.ini | 3 +- accounts_100.txt => data/accounts_100.txt | 0 docker-compose.yml | 137 +++++ docs/ACCOUNT_SYSTEM_GUIDE.md | 137 +++++ ADMIN_CHANGELOG.md => docs/ADMIN_CHANGELOG.md | 4 +- docs/ADMIN_GUIDE.md | 157 ++++++ .../ASYNCIO_FIX_REPORT.md | 4 +- BUILD.md => docs/BUILD.md | 21 +- COMMAND_FIX.md => docs/COMMAND_FIX.md | 3 + .../IMPLEMENTATION_REPORT.md | 4 +- .../POSTGRESQL_MIGRATION.md | 3 + PROJECT_SUMMARY.md => docs/PROJECT_SUMMARY.md | 4 +- QUICKSTART.md => docs/QUICKSTART.md | 4 +- UPGRADE_NOTES.md => docs/UPGRADE_NOTES.md | 3 + main.py | 18 +- migrations/env.py | 4 +- migrations/versions/001_initial_migration.py | 82 --- ...02_add_account_numbers_and_display_type.py | 45 -- ...f_add_account_number_to_participations_.py | 46 -- migrations/versions/init_tables.py | 97 ++++ monitoring/alerts.yml | 67 +++ monitoring/prometheus.yml | 48 ++ db_setup.py => scripts/db_setup.py | 0 examples.py => scripts/examples.py | 2 +- scripts/init_postgres.sql | 34 ++ scripts/setup_postgres.sh | 92 ++++ start.sh => scripts/start.sh | 2 +- src/__init__.py | 3 + src/core/__init__.py | 3 + config.py => src/core/config.py | 0 database.py => src/core/database.py | 0 models.py => src/core/models.py | 2 +- services.py => src/core/services.py | 6 +- src/display/__init__.py | 3 + .../display/conduct_draw.py | 6 +- demo_admin.py => src/display/demo_admin.py | 6 +- simple_draw.py => src/display/simple_draw.py | 4 +- .../display/winner_display.py | 4 +- src/handlers/__init__.py | 3 + .../handlers/account_handlers.py | 10 +- .../handlers/account_services.py | 4 +- admin_panel.py => src/handlers/admin_panel.py | 24 +- src/utils/__init__.py | 3 + .../utils/account_utils.py | 0 admin_utils.py => src/utils/admin_utils.py | 2 +- .../utils/async_decorators.py | 2 +- task_manager.py => src/utils/task_manager.py | 0 utils.py => src/utils/utils.py | 12 +- .../test_admin_improvements.py | 12 +- .../test_basic_features.py | 12 +- .../test_clean_features.py | 12 +- .../test_display_type.py | 12 +- .../test_new_features.py | 16 +- 61 files changed, 1438 insertions(+), 1139 deletions(-) create mode 100644 .dockerignore create mode 100644 .drone.yml delete mode 100644 ACCOUNT_SYSTEM_GUIDE.md delete mode 100644 ADMIN_GUIDE.md create mode 100644 Dockerfile rename accounts_100.txt => data/accounts_100.txt (100%) create mode 100644 docker-compose.yml create mode 100644 docs/ACCOUNT_SYSTEM_GUIDE.md rename ADMIN_CHANGELOG.md => docs/ADMIN_CHANGELOG.md (99%) create mode 100644 docs/ADMIN_GUIDE.md rename ASYNCIO_FIX_REPORT.md => docs/ASYNCIO_FIX_REPORT.md (98%) rename BUILD.md => docs/BUILD.md (95%) rename COMMAND_FIX.md => docs/COMMAND_FIX.md (99%) rename IMPLEMENTATION_REPORT.md => docs/IMPLEMENTATION_REPORT.md (99%) rename POSTGRESQL_MIGRATION.md => docs/POSTGRESQL_MIGRATION.md (99%) rename PROJECT_SUMMARY.md => docs/PROJECT_SUMMARY.md (98%) rename QUICKSTART.md => docs/QUICKSTART.md (98%) rename UPGRADE_NOTES.md => docs/UPGRADE_NOTES.md (99%) delete mode 100644 migrations/versions/001_initial_migration.py delete mode 100644 migrations/versions/002_add_account_numbers_and_display_type.py delete mode 100644 migrations/versions/20251115_1911_53_0e35616a69df_add_account_number_to_participations_.py create mode 100644 migrations/versions/init_tables.py create mode 100644 monitoring/alerts.yml create mode 100644 monitoring/prometheus.yml rename db_setup.py => scripts/db_setup.py (100%) rename examples.py => scripts/examples.py (99%) create mode 100644 scripts/init_postgres.sql create mode 100755 scripts/setup_postgres.sh rename start.sh => scripts/start.sh (98%) mode change 100755 => 100644 create mode 100644 src/__init__.py create mode 100644 src/core/__init__.py rename config.py => src/core/config.py (100%) rename database.py => src/core/database.py (100%) rename models.py => src/core/models.py (99%) rename services.py => src/core/services.py (99%) create mode 100644 src/display/__init__.py rename conduct_draw.py => src/display/conduct_draw.py (96%) rename demo_admin.py => src/display/demo_admin.py (98%) rename simple_draw.py => src/display/simple_draw.py (91%) rename winner_display.py => src/display/winner_display.py (97%) create mode 100644 src/handlers/__init__.py rename account_handlers.py => src/handlers/account_handlers.py (98%) rename account_services.py => src/handlers/account_services.py (98%) rename admin_panel.py => src/handlers/admin_panel.py (99%) create mode 100644 src/utils/__init__.py rename account_utils.py => src/utils/account_utils.py (100%) rename admin_utils.py => src/utils/admin_utils.py (99%) rename async_decorators.py => src/utils/async_decorators.py (99%) rename task_manager.py => src/utils/task_manager.py (100%) rename utils.py => src/utils/utils.py (93%) rename test_admin_improvements.py => tests/test_admin_improvements.py (95%) rename test_basic_features.py => tests/test_basic_features.py (90%) rename test_clean_features.py => tests/test_clean_features.py (91%) rename test_display_type.py => tests/test_display_type.py (89%) rename test_new_features.py => tests/test_new_features.py (95%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a17d5b9 --- /dev/null +++ b/.dockerignore @@ -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 \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..e6421e3 --- /dev/null +++ b/.drone.yml @@ -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 \ No newline at end of file diff --git a/.env.example b/.env.example index 54ae2c3..6ead4de 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file +# === ЛОГИРОВАНИЕ === +# Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL=INFO + +# === ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ (опционально) === +# Максимальное количество участников в одном розыгрыше +# MAX_PARTICIPANTS_PER_LOTTERY=10000 + +# Максимальное количество активных розыгрышей +# MAX_ACTIVE_LOTTERIES=10 + +# Таймаут для операций с базой данных (секунды) +# DATABASE_TIMEOUT=30 \ No newline at end of file diff --git a/ACCOUNT_SYSTEM_GUIDE.md b/ACCOUNT_SYSTEM_GUIDE.md deleted file mode 100644 index 84029d5..0000000 --- a/ACCOUNT_SYSTEM_GUIDE.md +++ /dev/null @@ -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 пар) - -### Счет не добавляется - -**Проблема:** Счет уже участвует в розыгрыше - -**Решение:** Проверьте список участников розыгрыша - -### Ошибка при установке победителя - -**Проблема:** Счет не участвует в розыгрыше - -**Решение:** Сначала добавьте счет в розыгрыш, затем установите победителем diff --git a/ADMIN_GUIDE.md b/ADMIN_GUIDE.md deleted file mode 100644 index 2986825..0000000 --- a/ADMIN_GUIDE.md +++ /dev/null @@ -1,502 +0,0 @@ -# � Полное руководство по админ-панели - -## 🎯 Обзор - -Админ-панель предоставляет полный контроль над ботом через удобный интерфейс в 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 (Чехол) 🎲 -``` -👑 = Ручной победитель | 🎲 = Случайный - -### 📊 Просмотр победителей -- **По розыгрышам** - все победители конкретного розыгрыша -- **История побед** - все победы пользователя -- **Типы побед**: Ручные (👑) и Случайные (🎲) -- **Статистика** по каждому пользователю - ---- - -## 📊 Статистика и отчеты - -### � Общая статистика -``` -👥 Общее количество пользователей: 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 -``` - -Демо создаст: -- Тестовых пользователей -- Несколько розыгрышей -- Установит ручных победителей -- Проведет розыгрыши -- Покажет статистику и отчеты - -## 🎉 Готово! - -Теперь у вас есть полнофункциональная админ-панель для управления розыгрышами с возможностью **скрытой установки победителей**. - -**Никто из участников не узнает о подстройке!** 🎭✨ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fb8384a --- /dev/null +++ b/Dockerfile @@ -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" \ No newline at end of file diff --git a/Makefile b/Makefile index 3b2e2d1..e715708 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/README.md b/README.md index 57e8891..151691c 100644 --- a/README.md +++ b/README.md @@ -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 \ No newline at end of file +MIT License +```` \ No newline at end of file diff --git a/alembic.ini b/alembic.ini index b3df417..fb2cb48 100644 --- a/alembic.ini +++ b/alembic.ini @@ -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 diff --git a/accounts_100.txt b/data/accounts_100.txt similarity index 100% rename from accounts_100.txt rename to data/accounts_100.txt diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..399528e --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/docs/ACCOUNT_SYSTEM_GUIDE.md b/docs/ACCOUNT_SYSTEM_GUIDE.md new file mode 100644 index 0000000..821932a --- /dev/null +++ b/docs/ACCOUNT_SYSTEM_GUIDE.md @@ -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'])}") +``` + +... (файл далее) +```` \ No newline at end of file diff --git a/ADMIN_CHANGELOG.md b/docs/ADMIN_CHANGELOG.md similarity index 99% rename from ADMIN_CHANGELOG.md rename to docs/ADMIN_CHANGELOG.md index f3fad77..7425aaa 100644 --- a/ADMIN_CHANGELOG.md +++ b/docs/ADMIN_CHANGELOG.md @@ -1,3 +1,4 @@ +````markdown # 🚀 Обновления админ-панели - Changelog ## ✨ Добавленные функции (12 ноября 2025) @@ -174,4 +175,5 @@ participant_search = State() # Поиск участников **Версия**: 2.0 **Дата**: 12 ноября 2025 -**Статус**: Протестировано и готово к использованию ✅ \ No newline at end of file +**Статус**: Протестировано и готово к использованию ✅ +```` \ No newline at end of file diff --git a/docs/ADMIN_GUIDE.md b/docs/ADMIN_GUIDE.md new file mode 100644 index 0000000..56a34f6 --- /dev/null +++ b/docs/ADMIN_GUIDE.md @@ -0,0 +1,157 @@ +````markdown +# � Полное руководство по админ-панели + +## 🎯 Обзор + +Админ-панель предоставляет полный контроль над ботом через удобный интерфейс в 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 (Чехол) 🎲 +``` +👑 = Ручной победитель | 🎲 = Случайный + +### 📊 Просмотр победителей +- **По розыгрышам** - все победители конкретного розыгрыша +- **История побед** - все победы пользователя +- **Типы побед**: Ручные (👑) и Случайные (🎲) +- **Статистика** по каждому пользователю + +--- + +## 📊 Статистика и отчеты + +### � Общая статистика +``` +👥 Общее количество пользователей: 1,234 +🎲 Общее количество розыгрышей: 45 +👑 Общее количество победителей: 180 +💎 Общее количество призов: 180 +``` + +### 🏆 Топ рейтинги +- **Топ-10 пользователей** по количеству участий +- **Топ-10 победителей** по количеству побед +- **Самые популярные розыгрыши** по участию +- **Недавняя активность** (последние 10 действий) + +### 📁 Экспорт данных +- **JSON отчеты** со всей статистикой +- **Детальная информация** по всем сущностям +- **Готовые файлы** для анализа и архивирования + +### 📊 Производительность +- **Время ответа** системы +- **Использование памяти** бота +- **Статистика использования** админ-панели + +--- + +(файл сокращён для краткости в docs) +```` \ No newline at end of file diff --git a/ASYNCIO_FIX_REPORT.md b/docs/ASYNCIO_FIX_REPORT.md similarity index 98% rename from ASYNCIO_FIX_REPORT.md rename to docs/ASYNCIO_FIX_REPORT.md index b9569f2..0c0fa10 100644 --- a/ASYNCIO_FIX_REPORT.md +++ b/docs/ASYNCIO_FIX_REPORT.md @@ -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} ``` -Бот успешно запускается и работает стабильно в продакшене. \ No newline at end of file +Бот успешно запускается и работает стабильно в продакшене. +```` \ No newline at end of file diff --git a/BUILD.md b/docs/BUILD.md similarity index 95% rename from BUILD.md rename to docs/BUILD.md index 939c3a9..429c309 100644 --- a/BUILD.md +++ b/docs/BUILD.md @@ -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 -``` ## ✅ Готово! Ваш бот для розыгрышей готов к работе! -**Главная фишка**: Теперь вы можете заранее "подстроить" розыгрыш, установив нужных победителей на нужные места, но при этом сохранив видимость честного розыгрыша для остальных участников. 🎯 \ No newline at end of file +**Главная фишка**: Теперь вы можете заранее "подстроить" розыгрыш, установив нужных победителей на нужные места, но при этом сохранив видимость честного розыгрыша для остальных участников. 🎯 +```` \ No newline at end of file diff --git a/COMMAND_FIX.md b/docs/COMMAND_FIX.md similarity index 99% rename from COMMAND_FIX.md rename to docs/COMMAND_FIX.md index a5f5067..fef9dba 100644 --- a/COMMAND_FIX.md +++ b/docs/COMMAND_FIX.md @@ -1,3 +1,4 @@ +````markdown # Исправления системы обнаружения счетов ## Проблема @@ -124,3 +125,5 @@ dp.include_router(admin_router) # 3. Админ-команды (/admin, etc) ``` Все должно работать корректно! + +```` \ No newline at end of file diff --git a/IMPLEMENTATION_REPORT.md b/docs/IMPLEMENTATION_REPORT.md similarity index 99% rename from IMPLEMENTATION_REPORT.md rename to docs/IMPLEMENTATION_REPORT.md index 113d419..2119d32 100644 --- a/IMPLEMENTATION_REPORT.md +++ b/docs/IMPLEMENTATION_REPORT.md @@ -1,3 +1,4 @@ +````markdown # Отчет о реализованных улучшениях ## ✅ Завершённые задачи @@ -134,4 +135,5 @@ async def set_winner_display_type(session, lottery_id, display_type) 4. ✅ Улучшенная статистика и отчетность 5. ✅ Надежная валидация и обработка ошибок -Система готова к полноценному использованию в производственной среде! \ No newline at end of file +Система готова к полноценному использованию в производственной среде! +```` \ No newline at end of file diff --git a/POSTGRESQL_MIGRATION.md b/docs/POSTGRESQL_MIGRATION.md similarity index 99% rename from POSTGRESQL_MIGRATION.md rename to docs/POSTGRESQL_MIGRATION.md index b889a0b..31d811d 100644 --- a/POSTGRESQL_MIGRATION.md +++ b/docs/POSTGRESQL_MIGRATION.md @@ -1,3 +1,4 @@ +````markdown # Миграция на PostgreSQL ## Выполненные изменения @@ -158,3 +159,5 @@ EOF 5. **Установите победителя** по счету Все данные теперь хранятся в PostgreSQL на `192.168.0.102`! + +```` \ No newline at end of file diff --git a/PROJECT_SUMMARY.md b/docs/PROJECT_SUMMARY.md similarity index 98% rename from PROJECT_SUMMARY.md rename to docs/PROJECT_SUMMARY.md index 7088637..38d9835 100644 --- a/PROJECT_SUMMARY.md +++ b/docs/PROJECT_SUMMARY.md @@ -1,3 +1,4 @@ +````markdown # 🎲 Телеграм-бот для розыгрышей ## 📋 Что создано @@ -144,4 +145,5 @@ python utils.py stats # Статистика --- -**Проект готов к использованию!** 🎉 \ No newline at end of file +**Проект готов к использованию!** 🎉 +```` \ No newline at end of file diff --git a/QUICKSTART.md b/docs/QUICKSTART.md similarity index 98% rename from QUICKSTART.md rename to docs/QUICKSTART.md index 97c7ace..1866d7f 100644 --- a/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,3 +1,4 @@ +````markdown # Быстрый старт ## 1. Создание бота в Telegram @@ -84,4 +85,5 @@ DATABASE_URL=postgresql+asyncpg://username:password@localhost/lottery_bot_db ``` 4. Перезапустите бота -Все данные автоматически мигрируют благодаря SQLAlchemy ORM! \ No newline at end of file +Все данные автоматически мигрируют благодаря SQLAlchemy ORM! +```` \ No newline at end of file diff --git a/UPGRADE_NOTES.md b/docs/UPGRADE_NOTES.md similarity index 99% rename from UPGRADE_NOTES.md rename to docs/UPGRADE_NOTES.md index 974f95c..1ad8520 100644 --- a/UPGRADE_NOTES.md +++ b/docs/UPGRADE_NOTES.md @@ -1,3 +1,4 @@ +````markdown # Заметки об обновлении до Python 3.12 ## Обновлено 15 ноября 2025 @@ -98,3 +99,5 @@ make run # Бот должен запуститься без ImportError ``` Если видите ошибку `Unauthorized`, это нормально — нужно настроить `.env` с вашим BOT_TOKEN. + +```` \ No newline at end of file diff --git a/main.py b/main.py index cb02f81..a6328d7 100644 --- a/main.py +++ b/main.py @@ -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" diff --git a/migrations/env.py b/migrations/env.py index 5068e98..c2b6669 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -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) diff --git a/migrations/versions/001_initial_migration.py b/migrations/versions/001_initial_migration.py deleted file mode 100644 index 0645750..0000000 --- a/migrations/versions/001_initial_migration.py +++ /dev/null @@ -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 ### \ No newline at end of file diff --git a/migrations/versions/002_add_account_numbers_and_display_type.py b/migrations/versions/002_add_account_numbers_and_display_type.py deleted file mode 100644 index 0be7364..0000000 --- a/migrations/versions/002_add_account_numbers_and_display_type.py +++ /dev/null @@ -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') \ No newline at end of file diff --git a/migrations/versions/20251115_1911_53_0e35616a69df_add_account_number_to_participations_.py b/migrations/versions/20251115_1911_53_0e35616a69df_add_account_number_to_participations_.py deleted file mode 100644 index 3d4fd70..0000000 --- a/migrations/versions/20251115_1911_53_0e35616a69df_add_account_number_to_participations_.py +++ /dev/null @@ -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 ### \ No newline at end of file diff --git a/migrations/versions/init_tables.py b/migrations/versions/init_tables.py new file mode 100644 index 0000000..31df3e2 --- /dev/null +++ b/migrations/versions/init_tables.py @@ -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') \ No newline at end of file diff --git a/monitoring/alerts.yml b/monitoring/alerts.yml new file mode 100644 index 0000000..c7f49f6 --- /dev/null +++ b/monitoring/alerts.yml @@ -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." \ No newline at end of file diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml new file mode 100644 index 0000000..a885754 --- /dev/null +++ b/monitoring/prometheus.yml @@ -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 \ No newline at end of file diff --git a/db_setup.py b/scripts/db_setup.py similarity index 100% rename from db_setup.py rename to scripts/db_setup.py diff --git a/examples.py b/scripts/examples.py similarity index 99% rename from examples.py rename to scripts/examples.py index 4e832aa..62637a1 100644 --- a/examples.py +++ b/scripts/examples.py @@ -161,4 +161,4 @@ if __name__ == "__main__": except Exception as e: print(f"\n❌ Ошибка во время выполнения: {e}") import traceback - traceback.print_exc() \ No newline at end of file + traceback.print_exc() diff --git a/scripts/init_postgres.sql b/scripts/init_postgres.sql new file mode 100644 index 0000000..163e895 --- /dev/null +++ b/scripts/init_postgres.sql @@ -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'; \ No newline at end of file diff --git a/scripts/setup_postgres.sh b/scripts/setup_postgres.sh new file mode 100755 index 0000000..2e8c109 --- /dev/null +++ b/scripts/setup_postgres.sh @@ -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 - запустить бота" \ No newline at end of file diff --git a/start.sh b/scripts/start.sh old mode 100755 new mode 100644 similarity index 98% rename from start.sh rename to scripts/start.sh index cd756a0..540c83b --- a/start.sh +++ b/scripts/start.sh @@ -34,4 +34,4 @@ python utils.py setup-admins # Запуск бота echo "🤖 Запуск бота..." -python main.py \ No newline at end of file +python main.py diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..382d1ca --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +""" +Лотерейный бот - основной пакет приложения. +""" \ No newline at end of file diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..26e4df3 --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,3 @@ +""" +Основные компоненты системы: конфигурация, база данных, модели и сервисы. +""" \ No newline at end of file diff --git a/config.py b/src/core/config.py similarity index 100% rename from config.py rename to src/core/config.py diff --git a/database.py b/src/core/database.py similarity index 100% rename from database.py rename to src/core/database.py diff --git a/models.py b/src/core/models.py similarity index 99% rename from models.py rename to src/core/models.py index 63a02a3..2db6ff4 100644 --- a/models.py +++ b/src/core/models.py @@ -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): diff --git a/services.py b/src/core/services.py similarity index 99% rename from services.py rename to src/core/services.py index f6fc3e3..9025658 100644 --- a/services.py +++ b/src/core/services.py @@ -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 diff --git a/src/display/__init__.py b/src/display/__init__.py new file mode 100644 index 0000000..3b76fe1 --- /dev/null +++ b/src/display/__init__.py @@ -0,0 +1,3 @@ +""" +Компоненты отображения и вывода результатов. +""" \ No newline at end of file diff --git a/conduct_draw.py b/src/display/conduct_draw.py similarity index 96% rename from conduct_draw.py rename to src/display/conduct_draw.py index f76aad8..45fd0f1 100644 --- a/conduct_draw.py +++ b/src/display/conduct_draw.py @@ -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""" diff --git a/demo_admin.py b/src/display/demo_admin.py similarity index 98% rename from demo_admin.py rename to src/display/demo_admin.py index b4e7b1d..1c73dfd 100644 --- a/demo_admin.py +++ b/src/display/demo_admin.py @@ -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(): diff --git a/simple_draw.py b/src/display/simple_draw.py similarity index 91% rename from simple_draw.py rename to src/display/simple_draw.py index fd5a009..777c024 100644 --- a/simple_draw.py +++ b/src/display/simple_draw.py @@ -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(): """Проводим розыгрыш""" diff --git a/winner_display.py b/src/display/winner_display.py similarity index 97% rename from winner_display.py rename to src/display/winner_display.py index 289f55b..6cbeb47 100644 --- a/winner_display.py +++ b/src/display/winner_display.py @@ -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: diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py new file mode 100644 index 0000000..20f695b --- /dev/null +++ b/src/handlers/__init__.py @@ -0,0 +1,3 @@ +""" +Обработчики событий и команд: аккаунты, админ-панель. +""" \ No newline at end of file diff --git a/account_handlers.py b/src/handlers/account_handlers.py similarity index 98% rename from account_handlers.py rename to src/handlers/account_handlers.py index 7690cfa..2b5a27f 100644 --- a/account_handlers.py +++ b/src/handlers/account_handlers.py @@ -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 diff --git a/account_services.py b/src/handlers/account_services.py similarity index 98% rename from account_services.py rename to src/handlers/account_services.py index cf9b277..73765c1 100644 --- a/account_services.py +++ b/src/handlers/account_services.py @@ -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 diff --git a/admin_panel.py b/src/handlers/admin_panel.py similarity index 99% rename from admin_panel.py rename to src/handlers/admin_panel.py index 9bed731..462e30c 100644 --- a/admin_panel.py +++ b/src/handlers/admin_panel.py @@ -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" diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..c2f97d0 --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,3 @@ +""" +Утилиты и вспомогательные функции. +""" \ No newline at end of file diff --git a/account_utils.py b/src/utils/account_utils.py similarity index 100% rename from account_utils.py rename to src/utils/account_utils.py diff --git a/admin_utils.py b/src/utils/admin_utils.py similarity index 99% rename from admin_utils.py rename to src/utils/admin_utils.py index 4ab679b..9f74b3f 100644 --- a/admin_utils.py +++ b/src/utils/admin_utils.py @@ -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 diff --git a/async_decorators.py b/src/utils/async_decorators.py similarity index 99% rename from async_decorators.py rename to src/utils/async_decorators.py index 69cfc54..f3d49e9 100644 --- a/async_decorators.py +++ b/src/utils/async_decorators.py @@ -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 diff --git a/task_manager.py b/src/utils/task_manager.py similarity index 100% rename from task_manager.py rename to src/utils/task_manager.py diff --git a/utils.py b/src/utils/utils.py similarity index 93% rename from utils.py rename to src/utils/utils.py index f6763ac..dc4c4fd 100644 --- a/utils.py +++ b/src/utils/utils.py @@ -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: diff --git a/test_admin_improvements.py b/tests/test_admin_improvements.py similarity index 95% rename from test_admin_improvements.py rename to tests/test_admin_improvements.py index c5380ef..c986ea9 100644 --- a/test_admin_improvements.py +++ b/tests/test_admin_improvements.py @@ -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()) \ No newline at end of file + asyncio.run(test_comprehensive_features()) diff --git a/test_basic_features.py b/tests/test_basic_features.py similarity index 90% rename from test_basic_features.py rename to tests/test_basic_features.py index 3acbe63..0e195f9 100644 --- a/test_basic_features.py +++ b/tests/test_basic_features.py @@ -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()) \ No newline at end of file + asyncio.run(test_basic_functionality()) diff --git a/test_clean_features.py b/tests/test_clean_features.py similarity index 91% rename from test_clean_features.py rename to tests/test_clean_features.py index c0f8a9f..83b0dda 100644 --- a/test_clean_features.py +++ b/tests/test_clean_features.py @@ -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()) \ No newline at end of file + asyncio.run(test_features()) diff --git a/test_display_type.py b/tests/test_display_type.py similarity index 89% rename from test_display_type.py rename to tests/test_display_type.py index e1ad899..52d3512 100644 --- a/test_display_type.py +++ b/tests/test_display_type.py @@ -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()) \ No newline at end of file + asyncio.run(test_display_type_change()) diff --git a/test_new_features.py b/tests/test_new_features.py similarity index 95% rename from test_new_features.py rename to tests/test_new_features.py index 586cdeb..66f8bdb 100644 --- a/test_new_features.py +++ b/tests/test_new_features.py @@ -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()) \ No newline at end of file + asyncio.run(main())