main functions fix

This commit is contained in:
2025-11-15 20:03:49 +09:00
parent e0075d91b6
commit 3a25e6a4cb
18 changed files with 1779 additions and 75 deletions

332
ACCOUNT_SYSTEM_GUIDE.md Normal file
View File

@@ -0,0 +1,332 @@
# Руководство по работе со счетами в розыгрышах
## Обзор
Теперь бот поддерживает два режима участия в розыгрышах:
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 пар)
### Счет не добавляется
**Проблема:** Счет уже участвует в розыгрыше
**Решение:** Проверьте список участников розыгрыша
### Ошибка при установке победителя
**Проблема:** Счет не участвует в розыгрыше
**Решение:** Сначала добавьте счет в розыгрыш, затем установите победителем

126
COMMAND_FIX.md Normal file
View File

@@ -0,0 +1,126 @@
# Исправления системы обнаружения счетов
## Проблема
Бот не реагировал на команды `/start` и `/admin`, потому что обработчик `detect_account_input` перехватывал ВСЕ текстовые сообщения, включая команды.
## Решение
### 1. Добавлена проверка команд в `account_handlers.py`
```python
@account_router.message(F.text, StateFilter(None))
async def detect_account_input(message: Message, state: FSMContext):
"""
Обнаружение ввода счетов в сообщении
Активируется только для администраторов
"""
if not is_admin(message.from_user.id):
return
# Игнорируем команды (начинаются с /)
if message.text.startswith('/'):
return # ✅ НОВАЯ ПРОВЕРКА
# Парсим счета из сообщения
accounts = parse_accounts_from_message(message.text)
if not accounts:
return # Счета не обнаружены, пропускаем
```
### 2. Добавлен параметр `limit` в `LotteryService.get_all_lotteries()`
```python
@staticmethod
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
"""Получить список всех розыгрышей"""
query = select(Lottery).order_by(Lottery.created_at.desc())
if limit:
query = query.limit(limit) # ✅ ПОДДЕРЖКА ЛИМИТА
result = await session.execute(query)
return result.scalars().all()
```
## Логика обработки сообщений
Теперь обработчик работает так:
```
Входящее сообщение
Проверка: админ?
НЕТ → игнорировать
↓ ДА
Проверка: команда (начинается с /)?
↓ ДА → пропустить дальше (к обработчикам команд)
НЕТ
Парсинг счетов из текста
Счета найдены?
НЕТ → игнорировать
↓ ДА
Показать inline-меню с действиями
```
## Что теперь работает
**Команды обрабатываются корректно:**
- `/start` → приветствие
- `/admin` → админ-панель
- `/help` → справка
**Обнаружение счетов работает только для текста без `/`:**
- `11-22-33-44-55-66-77-88` → обнаружен счет
- `Вот счета: 11-22-33-44-55-66-77-88` → обнаружен счет
- `/start` → команда, не счет
**Проверка прав:**
- Только администраторы видят меню действий со счетами
- Обычные пользователи получают обычные ответы
## Порядок обработки обновлений
```python
# В main.py
dp.include_router(account_router) # 1. Приоритет - счета (только если не команда)
dp.include_router(router) # 2. Команды пользователя (/start, /join, etc)
dp.include_router(admin_router) # 3. Админ-команды (/admin, etc)
```
Это гарантирует, что:
1. Счета обнаруживаются первыми (но пропускают команды)
2. Команды обрабатываются вторыми
3. Админ-команды обрабатываются последними
## Тестирование
Попробуйте отправить боту:
### Команды (должны работать):
```
/start
/admin
/help
```
### Счета (должны обнаружиться):
```
11-22-33-44-55-66-77-88
```
### Текст со счетами:
```
Вот мои счета для розыгрыша:
11-22-33-44-55-66-77-88
22-33-44-55-66-77-88-99
```
### Обычный текст (должен игнорироваться):
```
Привет, как дела?
Когда будет розыгрыш?
```
Все должно работать корректно!

View File

@@ -29,7 +29,7 @@ help:
install:
@echo "📦 Установка зависимостей..."
python3 -m venv .venv
source .venv/bin/activate && pip install -r requirements.txt
. .venv/bin/activate && pip install -r requirements.txt
# Первоначальная настройка
setup: install
@@ -38,49 +38,49 @@ setup: install
echo "❌ Файл .env не найден! Скопируйте .env.example в .env"; \
exit 1; \
fi
python utils.py init
python utils.py setup-admins
. .venv/bin/activate && python utils.py init
. .venv/bin/activate && python utils.py setup-admins
@echo "✅ Настройка завершена!"
# Запуск бота
run:
@echo "🚀 Запуск бота..."
python main.py
. .venv/bin/activate && python main.py
# Создание миграции
migration:
@echo "📄 Создание новой миграции..."
alembic revision --autogenerate -m "$(MSG)"
. .venv/bin/activate && alembic revision --autogenerate -m "$(MSG)"
# Применение миграций
migrate:
@echo "⬆️ Применение миграций..."
alembic upgrade head
. .venv/bin/activate && alembic upgrade head
# Тесты и примеры
test:
@echo "🧪 Запуск тестов..."
python examples.py
. .venv/bin/activate && python examples.py
# Создание тестового розыгрыша
sample:
@echo "🎲 Создание тестового розыгрыша..."
python utils.py sample
. .venv/bin/activate && python utils.py sample
# Статистика
stats:
@echo "📊 Статистика бота..."
python utils.py stats
. .venv/bin/activate && python utils.py stats
# Демонстрация админ-панели
demo-admin:
@echo "🎪 Демонстрация возможностей админ-панели..."
python demo_admin.py
. .venv/bin/activate && python demo_admin.py
# Тестирование улучшений админки
test-admin:
@echo "🧪 Тестирование новых функций админ-панели..."
python test_admin_improvements.py
. .venv/bin/activate && python test_admin_improvements.py
# Очистка
clean:

160
POSTGRESQL_MIGRATION.md Normal file
View File

@@ -0,0 +1,160 @@
# Миграция на PostgreSQL
## Выполненные изменения
### 1. Настройка подключения
В файле `.env` раскомментирована строка:
```env
DATABASE_URL=postgresql+asyncpg://trevor:R0sebud@192.168.0.102/bot_db
```
### 2. Создание базы данных
```bash
psql -h 192.168.0.102 -U trevor -d postgres -c "CREATE DATABASE bot_db;"
```
### 3. Инициализация таблиц
Вместо Alembic-миграций (из-за проблем с длинными именами версий) используется прямое создание:
```bash
. .venv/bin/activate && python -c "import asyncio; from database import init_db; asyncio.run(init_db())"
```
### 4. Исправление бага в services.py
Добавлен параметр `limit` в метод `LotteryService.get_active_lotteries()`:
```python
@staticmethod
async def get_active_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
"""Получить список активных розыгрышей"""
query = select(Lottery).where(
Lottery.is_active == True,
Lottery.is_completed == False
).order_by(Lottery.created_at.desc())
if limit:
query = query.limit(limit)
result = await session.execute(query)
return result.scalars().all()
```
## Различия SQLite vs PostgreSQL
| Параметр | SQLite | PostgreSQL |
|----------|--------|------------|
| Тип ID | `INTEGER` | `SERIAL` |
| Datetime | `DATETIME` | `TIMESTAMP WITHOUT TIME ZONE` |
| JSON | `JSON` (текст) | `JSON` (нативный тип) |
| Транзакции | Автоматические | Явные BEGIN/COMMIT |
## Проверка подключения
```bash
psql -h 192.168.0.102 -U trevor -d bot_db -c "\dt"
```
## Структура таблиц в PostgreSQL
```sql
-- Пользователи
CREATE TABLE users (
id SERIAL PRIMARY KEY,
telegram_id INTEGER UNIQUE NOT NULL,
username VARCHAR(255),
first_name VARCHAR(255),
last_name VARCHAR(255),
created_at TIMESTAMP,
is_admin BOOLEAN,
account_number VARCHAR(23) UNIQUE
);
-- Розыгрыши
CREATE TABLE lotteries (
id SERIAL PRIMARY KEY,
title VARCHAR(500) NOT NULL,
description TEXT,
created_at TIMESTAMP,
start_date TIMESTAMP,
end_date TIMESTAMP,
is_active BOOLEAN,
is_completed BOOLEAN,
prizes JSON,
creator_id INTEGER REFERENCES users(id),
manual_winners JSON,
draw_results JSON,
winner_display_type VARCHAR(20)
);
-- Участия
CREATE TABLE participations (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
lottery_id INTEGER NOT NULL REFERENCES lotteries(id),
account_number VARCHAR(23),
created_at TIMESTAMP
);
CREATE INDEX ix_participations_account_number ON participations(account_number);
-- Победители
CREATE TABLE winners (
id SERIAL PRIMARY KEY,
lottery_id INTEGER NOT NULL REFERENCES lotteries(id),
user_id INTEGER REFERENCES users(id),
account_number VARCHAR(23),
place INTEGER NOT NULL,
prize VARCHAR(500),
is_manual BOOLEAN,
created_at TIMESTAMP
);
CREATE INDEX ix_winners_account_number ON winners(account_number);
```
## Резервное копирование
### Создание бэкапа
```bash
pg_dump -h 192.168.0.102 -U trevor -d bot_db > backup.sql
```
### Восстановление из бэкапа
```bash
psql -h 192.168.0.102 -U trevor -d bot_db < backup.sql
```
## Сброс базы данных
```bash
psql -h 192.168.0.102 -U trevor -d postgres << 'EOF'
DROP DATABASE IF EXISTS bot_db;
CREATE DATABASE bot_db;
EOF
```
Затем:
```bash
. .venv/bin/activate && python -c "import asyncio; from database import init_db; asyncio.run(init_db())"
```
## Статус миграции
✅ База данных: PostgreSQL
✅ Таблицы созданы
✅ Индексы настроены
✅ Бот запущен и работает
✅ Исправлена ошибка с `limit` параметром
## Тестирование
Теперь бот готов к тестированию:
1. **Создайте тестовый розыгрыш** через админ-панель
2. **Отправьте счет** боту в формате `11-22-33-44-55-66-77-88`
3. **Проверьте inline-меню** - должны появиться кнопки действий
4. **Добавьте счета** в розыгрыш
5. **Установите победителя** по счету
Все данные теперь хранятся в PostgreSQL на `192.168.0.102`!

View File

@@ -16,13 +16,13 @@
## Технологии
- **Python 3.8+**
- **aiogram 3.1+** - для работы с Telegram Bot API
- **SQLAlchemy 2.0** - ORM для работы с базой данных
- **Alembic** - миграции базы данных
- **Python 3.12+** (рекомендуется Python 3.12.3+)
- **aiogram 3.16** - для работы с Telegram Bot API
- **SQLAlchemy 2.0.36** - ORM для работы с базой данных
- **Alembic 1.14** - миграции базы данных
- **python-dotenv** - управление переменными окружения
- **asyncpg** - асинхронный драйвер для PostgreSQL
- **aiosqlite** - асинхронный драйвер для SQLite
- **asyncpg 0.30** - асинхронный драйвер для PostgreSQL
- **aiosqlite 0.20** - асинхронный драйвер для SQLite
## Структура проекта

100
UPGRADE_NOTES.md Normal file
View File

@@ -0,0 +1,100 @@
# Заметки об обновлении до Python 3.12
## Обновлено 15 ноября 2025
### Изменения зависимостей
Все зависимости обновлены для полной совместимости с Python 3.12:
- **aiogram**: 3.1.1 → 3.16.0
- **aiohttp**: 3.8.5 → 3.11.18 (обязательно для Python 3.12)
- **sqlalchemy**: 2.0.23 → 2.0.36
- **alembic**: 1.8.1 → 1.14.0
- **python-dotenv**: 1.0.0 → 1.0.1
- **asyncpg**: 0.28.0 → 0.30.0
- **aiosqlite**: 0.17.0 → 0.20.0
### Изменения кода
#### 1. database.py
- Обновлён импорт: `from sqlalchemy.ext.declarative import declarative_base``from sqlalchemy.orm import declarative_base`
#### 2. models.py
- Заменён устаревший `datetime.utcnow` на `datetime.now(timezone.utc)`
- Все поля `created_at` теперь используют timezone-aware datetime
#### 3. Makefile
- Все команды теперь активируют виртуальное окружение через `. .venv/bin/activate`
- Это обеспечивает использование правильных версий Python-пакетов
### Требования к системе
Для успешной установки требуются следующие системные пакеты:
```bash
sudo apt-get install -y build-essential python3-dev libpq-dev libssl-dev pkg-config
```
### Установка
1. Убедитесь, что используете Python 3.12:
```bash
python3 --version # Должно быть 3.12.x
```
2. Пересоздайте виртуальное окружение:
```bash
rm -rf .venv
python3 -m venv .venv
. .venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
```
3. Примените миграции (если требуется):
```bash
make migrate
```
### Совместимость
- ✅ Полная совместимость с Python 3.12
-Все устаревшие API обновлены
- ✅ Проверено на Ubuntu 24.04 LTS
- ⚠️ Python 3.11 также поддерживается, но рекомендуется 3.12
### Известные проблемы
1. **aiohttp 3.8.x не компилируется на Python 3.12**
- Решение: используйте aiohttp >= 3.11.0
2. **datetime.utcnow deprecated**
- Решение: использован `datetime.now(timezone.utc)`
3. **sqlalchemy.ext.declarative deprecated**
- Решение: используйте `sqlalchemy.orm.declarative_base`
### Откат к старой версии
Если нужно вернуться к Python 3.11:
```bash
# Установите Python 3.11
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.11 python3.11-venv python3.11-dev
# Пересоздайте окружение
rm -rf .venv
python3.11 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
```
### Тестирование
Проверка успешности обновления:
```bash
make run # Бот должен запуститься без ImportError
```
Если видите ошибку `Unauthorized`, это нормально — нужно настроить `.env` с вашим BOT_TOKEN.

375
account_handlers.py Normal file
View File

@@ -0,0 +1,375 @@
"""
Обработчики для работы со счетами в розыгрышах
"""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.filters import StateFilter
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 typing import List
# Состояния FSM для работы со счетами
class AccountStates(StatesGroup):
waiting_for_lottery_choice = State() # Выбор розыгрыша для добавления счетов
waiting_for_winner_lottery = State() # Выбор розыгрыша для установки победителя
waiting_for_winner_place = State() # Выбор места победителя
searching_accounts = State() # Поиск счетов
# Создаем роутер
account_router = Router()
def is_admin(user_id: int) -> bool:
"""Проверка прав администратора"""
return user_id in ADMIN_IDS
@account_router.message(
F.text,
StateFilter(None),
~F.text.startswith('/') # Исключаем команды
)
async def detect_account_input(message: Message, state: FSMContext):
"""
Обнаружение ввода счетов в сообщении
Активируется только для администраторов
"""
if not is_admin(message.from_user.id):
return
# Парсим счета из сообщения
accounts = parse_accounts_from_message(message.text)
if not accounts:
return # Счета не обнаружены, пропускаем
# Сохраняем счета в состоянии
await state.update_data(detected_accounts=accounts)
# Формируем сообщение
accounts_text = "\n".join([f"{acc}" for acc in accounts])
count = len(accounts)
text = (
f"🔍 <b>Обнаружен ввод счет{'а' if count == 1 else 'ов'}</b>\n\n"
f"Найдено: <b>{count}</b>\n\n"
f"{accounts_text}\n\n"
f"Выберите действие:"
)
# Кнопки выбора действия
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=" Добавить в розыгрыш",
callback_data="account_action:add_to_lottery"
)],
[InlineKeyboardButton(
text="👑 Сделать победителем",
callback_data="account_action:set_as_winner"
)],
[InlineKeyboardButton(
text="❌ Отмена",
callback_data="account_action:cancel"
)]
])
await message.answer(text, reply_markup=keyboard, parse_mode="HTML")
@account_router.callback_query(F.data == "account_action:cancel")
async def cancel_account_action(callback: CallbackQuery, state: FSMContext):
"""Отмена действия со счетами"""
await state.clear()
await callback.message.edit_text("❌ Действие отменено")
await callback.answer()
@account_router.callback_query(F.data == "account_action:add_to_lottery")
async def choose_lottery_for_accounts(callback: CallbackQuery, state: FSMContext):
"""Выбор розыгрыша для добавления счетов"""
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
return
async with async_session_maker() as session:
# Получаем активные розыгрыши
lotteries = await LotteryService.get_active_lotteries(session, limit=20)
if not lotteries:
await callback.message.edit_text(
"❌ Нет активных розыгрышей.\n\n"
"Сначала создайте розыгрыш через /admin"
)
await state.clear()
await callback.answer()
return
# Формируем кнопки с розыгрышами
buttons = []
for lottery in lotteries:
buttons.append([InlineKeyboardButton(
text=f"🎲 {lottery.title[:40]}",
callback_data=f"add_accounts_to:{lottery.id}"
)])
buttons.append([InlineKeyboardButton(
text="❌ Отмена",
callback_data="account_action:cancel"
)])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
"📋 <b>Выберите розыгрыш:</b>",
reply_markup=keyboard,
parse_mode="HTML"
)
await state.set_state(AccountStates.waiting_for_lottery_choice)
await callback.answer()
@account_router.callback_query(F.data.startswith("add_accounts_to:"))
async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
"""Добавление счетов в выбранный розыгрыш"""
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
return
lottery_id = int(callback.data.split(":")[1])
# Получаем сохраненные счета
data = await state.get_data()
accounts = data.get("detected_accounts", [])
if not accounts:
await callback.message.edit_text("❌ Счета не найдены")
await state.clear()
await callback.answer()
return
# Показываем процесс
await callback.message.edit_text("⏳ Добавляем счета...")
async with async_session_maker() as session:
# Получаем информацию о розыгрыше
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.message.edit_text("❌ Розыгрыш не найден")
await state.clear()
await callback.answer()
return
# Добавляем счета
results = await AccountParticipationService.add_accounts_bulk(
session, lottery_id, accounts
)
# Формируем результат
text = f"<b>Результаты добавления в розыгрыш:</b>\n<i>{lottery.title}</i>\n\n"
text += f"✅ Добавлено: <b>{results['added']}</b>\n"
text += f"⚠️ Пропущено: <b>{results['skipped']}</b>\n\n"
if results['details']:
text += "<b>Детали:</b>\n"
text += "\n".join(results['details'][:20]) # Показываем первые 20
if len(results['details']) > 20:
text += f"\n... и ещё {len(results['details']) - 20}"
if results['errors']:
text += f"\n\n<b>Ошибки:</b>\n"
text += "\n".join(results['errors'][:10])
await callback.message.edit_text(text, parse_mode="HTML")
await state.clear()
await callback.answer("✅ Готово!")
@account_router.callback_query(F.data == "account_action:set_as_winner")
async def choose_lottery_for_winner(callback: CallbackQuery, state: FSMContext):
"""Выбор розыгрыша для установки победителя"""
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
return
# Проверяем, что у нас только один счет
data = await state.get_data()
accounts = data.get("detected_accounts", [])
if len(accounts) != 1:
await callback.message.edit_text(
"❌ Для установки победителя введите <b>один</b> счет",
parse_mode="HTML"
)
await state.clear()
await callback.answer()
return
async with async_session_maker() as session:
# Получаем все розыгрыши (активные и завершенные)
lotteries = await LotteryService.get_all_lotteries(session, limit=30)
if not lotteries:
await callback.message.edit_text(
"❌ Нет розыгрышей.\n\n"
"Сначала создайте розыгрыш через /admin"
)
await state.clear()
await callback.answer()
return
# Формируем кнопки
buttons = []
for lottery in lotteries:
status = "" if lottery.is_completed else "🎲"
buttons.append([InlineKeyboardButton(
text=f"{status} {lottery.title[:35]}",
callback_data=f"winner_lottery:{lottery.id}"
)])
buttons.append([InlineKeyboardButton(
text="❌ Отмена",
callback_data="account_action:cancel"
)])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
account = accounts[0]
await callback.message.edit_text(
f"👑 <b>Установка победителя</b>\n\n"
f"Счет: <code>{account}</code>\n\n"
f"Выберите розыгрыш:",
reply_markup=keyboard,
parse_mode="HTML"
)
await state.set_state(AccountStates.waiting_for_winner_lottery)
await callback.answer()
@account_router.callback_query(F.data.startswith("winner_lottery:"))
async def choose_winner_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места для победителя"""
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
return
lottery_id = int(callback.data.split(":")[1])
# Сохраняем ID розыгрыша
await state.update_data(winner_lottery_id=lottery_id)
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.message.edit_text("❌ Розыгрыш не найден")
await state.clear()
await callback.answer()
return
# Получаем призы
prizes = lottery.prizes or []
# Формируем кнопки с местами
buttons = []
for i, prize in enumerate(prizes[:10], 1): # Максимум 10 мест
prize_text = prize if isinstance(prize, str) else prize.get('description', f'Приз {i}')
buttons.append([InlineKeyboardButton(
text=f"🏆 Место {i}: {prize_text[:30]}",
callback_data=f"winner_place:{i}"
)])
# Если призов нет, предлагаем места 1-5
if not buttons:
for i in range(1, 6):
buttons.append([InlineKeyboardButton(
text=f"🏆 Место {i}",
callback_data=f"winner_place:{i}"
)])
buttons.append([InlineKeyboardButton(
text="❌ Отмена",
callback_data="account_action:cancel"
)])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
data = await state.get_data()
account = data.get("detected_accounts", [])[0]
await callback.message.edit_text(
f"👑 <b>Установка победителя</b>\n\n"
f"Розыгрыш: <i>{lottery.title}</i>\n"
f"Счет: <code>{account}</code>\n\n"
f"Выберите место:",
reply_markup=keyboard,
parse_mode="HTML"
)
await state.set_state(AccountStates.waiting_for_winner_place)
await callback.answer()
@account_router.callback_query(F.data.startswith("winner_place:"))
async def set_account_winner(callback: CallbackQuery, state: FSMContext):
"""Установка счета как победителя"""
if not is_admin(callback.from_user.id):
await callback.answer("⛔ Доступно только администраторам", show_alert=True)
return
place = int(callback.data.split(":")[1])
# Получаем данные
data = await state.get_data()
account = data.get("detected_accounts", [])[0]
lottery_id = data.get("winner_lottery_id")
if not account or not lottery_id:
await callback.message.edit_text("❌ Ошибка: данные не найдены")
await state.clear()
await callback.answer()
return
# Показываем процесс
await callback.message.edit_text("⏳ Устанавливаем победителя...")
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
# Получаем приз для этого места
prize = None
if lottery.prizes and len(lottery.prizes) >= place:
prize_info = lottery.prizes[place - 1]
prize = prize_info if isinstance(prize_info, str) else prize_info.get('description')
# Устанавливаем победителя
result = await AccountParticipationService.set_account_as_winner(
session, lottery_id, account, place, prize
)
if result["success"]:
text = (
f"✅ <b>Победитель установлен!</b>\n\n"
f"Розыгрыш: <i>{lottery.title}</i>\n"
f"Счет: <code>{account}</code>\n"
f"Место: <b>{place}</b>\n"
)
if prize:
text += f"Приз: <i>{prize}</i>"
await callback.answer("✅ Победитель установлен!", show_alert=True)
else:
text = f"{result['message']}"
await callback.answer("❌ Ошибка", show_alert=True)
await callback.message.edit_text(text, parse_mode="HTML")
await state.clear()

287
account_services.py Normal file
View File

@@ -0,0 +1,287 @@
"""
Сервис для работы с участием счетов в розыгрышах (без привязки к пользователям)
"""
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 typing import List, Optional, Dict, Any
class AccountParticipationService:
"""Сервис для работы с участием счетов в розыгрышах"""
@staticmethod
async def add_account_to_lottery(
session: AsyncSession,
lottery_id: int,
account_number: str
) -> Dict[str, Any]:
"""
Добавить счет в розыгрыш
Returns:
Dict с ключами: success, message, account_number
"""
# Валидируем и форматируем
formatted_account = format_account_number(account_number)
if not formatted_account:
return {
"success": False,
"message": f"Неверный формат счета: {account_number}",
"account_number": account_number
}
# Проверяем существование розыгрыша
lottery = await session.get(Lottery, lottery_id)
if not lottery:
return {
"success": False,
"message": f"Розыгрыш #{lottery_id} не найден",
"account_number": formatted_account
}
# Проверяем, не участвует ли уже этот счет
existing = await session.execute(
select(Participation).where(
Participation.lottery_id == lottery_id,
Participation.account_number == formatted_account
)
)
if existing.scalar_one_or_none():
return {
"success": False,
"message": f"Счет {formatted_account} уже участвует в розыгрыше",
"account_number": formatted_account
}
# Добавляем участие
participation = Participation(
lottery_id=lottery_id,
account_number=formatted_account,
user_id=None # Без привязки к пользователю
)
session.add(participation)
await session.commit()
return {
"success": True,
"message": f"Счет {formatted_account} добавлен в розыгрыш",
"account_number": formatted_account
}
@staticmethod
async def add_accounts_bulk(
session: AsyncSession,
lottery_id: int,
account_numbers: List[str]
) -> Dict[str, Any]:
"""
Массовое добавление счетов в розыгрыш
"""
results = {
"added": 0,
"skipped": 0,
"errors": [],
"details": [],
"added_accounts": [],
"skipped_accounts": []
}
for account in account_numbers:
result = await AccountParticipationService.add_account_to_lottery(
session, lottery_id, account
)
if result["success"]:
results["added"] += 1
results["added_accounts"].append(result["account_number"])
results["details"].append(f"{result['account_number']}")
else:
results["skipped"] += 1
results["skipped_accounts"].append(account)
results["errors"].append(result["message"])
results["details"].append(f"{result['message']}")
return results
@staticmethod
async def remove_account_from_lottery(
session: AsyncSession,
lottery_id: int,
account_number: str
) -> Dict[str, Any]:
"""Удалить счет из розыгрыша"""
formatted_account = format_account_number(account_number)
if not formatted_account:
return {
"success": False,
"message": f"Неверный формат счета: {account_number}"
}
participation = await session.execute(
select(Participation).where(
Participation.lottery_id == lottery_id,
Participation.account_number == formatted_account
)
)
participation = participation.scalar_one_or_none()
if not participation:
return {
"success": False,
"message": f"Счет {formatted_account} не участвует в розыгрыше"
}
await session.delete(participation)
await session.commit()
return {
"success": True,
"message": f"Счет {formatted_account} удален из розыгрыша"
}
@staticmethod
async def get_lottery_accounts(
session: AsyncSession,
lottery_id: int,
limit: Optional[int] = None,
offset: int = 0
) -> List[str]:
"""Получить все счета, участвующие в розыгрыше"""
query = select(Participation.account_number).where(
Participation.lottery_id == lottery_id,
Participation.account_number.isnot(None)
).order_by(Participation.created_at.desc())
if limit:
query = query.offset(offset).limit(limit)
result = await session.execute(query)
return [account for account in result.scalars().all() if account]
@staticmethod
async def get_accounts_count(session: AsyncSession, lottery_id: int) -> int:
"""Получить количество счетов в розыгрыше"""
result = await session.scalar(
select(func.count(Participation.id)).where(
Participation.lottery_id == lottery_id,
Participation.account_number.isnot(None)
)
)
return result or 0
@staticmethod
async def search_accounts_in_lottery(
session: AsyncSession,
lottery_id: int,
pattern: str,
limit: int = 20
) -> List[str]:
"""
Поиск счетов в розыгрыше по частичному совпадению
Args:
lottery_id: ID розыгрыша
pattern: Паттерн поиска (например "11-22" или "33")
limit: Максимальное количество результатов
"""
# Получаем все счета розыгрыша
all_accounts = await AccountParticipationService.get_lottery_accounts(
session, lottery_id
)
# Ищем совпадения
return search_accounts_by_pattern(pattern, all_accounts)[:limit]
@staticmethod
async def set_account_as_winner(
session: AsyncSession,
lottery_id: int,
account_number: str,
place: int,
prize: Optional[str] = None
) -> Dict[str, Any]:
"""
Установить счет как победителя на указанное место
"""
formatted_account = format_account_number(account_number)
if not formatted_account:
return {
"success": False,
"message": f"Неверный формат счета: {account_number}"
}
# Проверяем, участвует ли счет в розыгрыше
participation = await session.execute(
select(Participation).where(
Participation.lottery_id == lottery_id,
Participation.account_number == formatted_account
)
)
if not participation.scalar_one_or_none():
return {
"success": False,
"message": f"Счет {formatted_account} не участвует в розыгрыше"
}
# Проверяем, не занято ли уже это место
existing_winner = await session.execute(
select(Winner).where(
Winner.lottery_id == lottery_id,
Winner.place == place
)
)
existing_winner = existing_winner.scalar_one_or_none()
if existing_winner:
# Обновляем существующего победителя
existing_winner.account_number = formatted_account
existing_winner.user_id = None
existing_winner.is_manual = True
if prize:
existing_winner.prize = prize
else:
# Создаем нового победителя
winner = Winner(
lottery_id=lottery_id,
account_number=formatted_account,
user_id=None,
place=place,
prize=prize,
is_manual=True
)
session.add(winner)
await session.commit()
return {
"success": True,
"message": f"Счет {formatted_account} установлен победителем на место {place}",
"account_number": formatted_account,
"place": place
}
@staticmethod
async def get_lottery_winners_accounts(
session: AsyncSession,
lottery_id: int
) -> List[Dict[str, Any]]:
"""Получить всех победителей розыгрыша (счета)"""
result = await session.execute(
select(Winner).where(
Winner.lottery_id == lottery_id,
Winner.account_number.isnot(None)
).order_by(Winner.place)
)
winners = result.scalars().all()
return [
{
"place": w.place,
"account_number": w.account_number,
"prize": w.prize,
"is_manual": w.is_manual
}
for w in winners
]

View File

@@ -2,13 +2,13 @@
Утилиты для работы с клиентскими счетами
"""
import re
from typing import Optional
from typing import Optional, List
def validate_account_number(account_number: str) -> bool:
"""
Проверяет корректность формата номера клиентского счета
Формат: XX-XX-XX-XX-XX-XX-XX-XX (8 пар цифр через дефис)
Формат: XX-XX-XX-XX-XX-XX-XX (7 пар цифр через дефис)
Args:
account_number: Номер счета для проверки
@@ -19,8 +19,8 @@ def validate_account_number(account_number: str) -> bool:
if not account_number:
return False
# Паттерн для 8 пар цифр через дефис
pattern = r'^\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}$'
# Паттерн для 7 пар цифр через дефис
pattern = r'^\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}$'
return bool(re.match(pattern, account_number))
@@ -41,12 +41,12 @@ def format_account_number(account_number: str) -> Optional[str]:
# Убираем все символы кроме цифр
digits_only = re.sub(r'\D', '', account_number)
# Проверяем что осталось ровно 16 цифр
if len(digits_only) != 16:
# Проверяем что осталось ровно 14 цифр
if len(digits_only) != 14:
return None
# Форматируем как XX-XX-XX-XX-XX-XX-XX-XX
formatted = '-'.join([digits_only[i:i+2] for i in range(0, 16, 2)])
# Форматируем как XX-XX-XX-XX-XX-XX-XX
formatted = '-'.join([digits_only[i:i+2] for i in range(0, 14, 2)])
return formatted
@@ -60,11 +60,11 @@ def generate_account_number() -> str:
"""
import random
# Генерируем 16 случайных цифр
digits = ''.join([str(random.randint(0, 9)) for _ in range(16)])
# Генерируем 14 случайных цифр
digits = ''.join([str(random.randint(0, 9)) for _ in range(14)])
# Форматируем
return '-'.join([digits[i:i+2] for i in range(0, 16, 2)])
return '-'.join([digits[i:i+2] for i in range(0, 14, 2)])
def mask_account_number(account_number: str, show_last_digits: int = 4) -> str:
@@ -82,7 +82,7 @@ def mask_account_number(account_number: str, show_last_digits: int = 4) -> str:
return "Некорректный номер"
if show_last_digits <= 0:
return "**-**-**-**-**-**-**-**"
return "**-**-**-**-**-**-**"
# Убираем дефисы для работы с цифрами
digits = account_number.replace('-', '')
@@ -93,5 +93,59 @@ def mask_account_number(account_number: str, show_last_digits: int = 4) -> str:
# Создаем маску
masked_digits = '*' * (len(digits) - show_digits) + digits[-show_digits:]
# Возвращаем отформатированный результат
return '-'.join([masked_digits[i:i+2] for i in range(0, 16, 2)])
# Возвращаем отформатированный результат (7 пар)
return '-'.join([masked_digits[i:i+2] for i in range(0, 14, 2)])
def parse_accounts_from_message(text: str) -> List[str]:
"""
Извлекает все валидные номера счетов из текста сообщения
Args:
text: Текст сообщения
Returns:
List[str]: Список найденных и отформатированных номеров счетов
"""
if not text:
return []
accounts = []
# Ищем паттерны счетов в тексте (7 пар цифр)
pattern = r'\b\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}[-\s]?\d{2}\b'
matches = re.findall(pattern, text)
for match in matches:
formatted = format_account_number(match)
if formatted and formatted not in accounts:
accounts.append(formatted)
return accounts
def search_accounts_by_pattern(pattern: str, account_list: List[str]) -> List[str]:
"""
Ищет счета по частичному совпадению (1-2 пары цифр)
Args:
pattern: Паттерн для поиска (например "11-22" или "11")
account_list: Список счетов для поиска
Returns:
List[str]: Список найденных счетов
"""
if not pattern or not account_list:
return []
# Убираем лишние символы из паттерна
clean_pattern = re.sub(r'[^\d-]', '', pattern).strip('-')
if not clean_pattern:
return []
results = []
for account in account_list:
if clean_pattern in account:
results.append(account)
return results

100
accounts_100.txt Normal file
View File

@@ -0,0 +1,100 @@
85-84-87-41-83-41-63
03-15-35-94-83-22-40
36-60-34-92-81-48-41
97-66-15-47-35-85-59
16-76-88-84-05-81-72
51-94-46-57-13-01-50
50-73-96-63-73-74-24
94-13-13-89-83-22-75
39-85-17-28-30-43-83
60-72-58-00-79-48-54
29-43-78-41-85-88-89
12-95-36-23-38-10-06
48-64-41-80-09-73-05
23-24-48-78-27-46-23
75-26-85-70-08-44-54
48-06-69-72-17-18-85
90-86-19-06-42-12-59
25-69-98-23-66-87-30
07-42-11-95-24-00-89
01-36-94-83-70-99-72
03-73-60-40-05-98-20
49-09-08-82-43-55-34
42-99-12-21-99-08-03
23-46-32-24-11-78-27
23-03-83-99-03-22-33
48-06-78-22-76-02-51
62-44-30-46-41-65-49
19-29-95-47-06-40-14
15-25-76-63-12-04-30
62-44-62-85-26-11-28
01-52-72-62-41-69-09
15-13-82-39-71-48-08
62-34-87-77-30-28-16
81-21-09-65-26-16-72
50-21-82-08-57-81-17
29-23-02-52-28-27-51
13-88-88-89-68-44-08
29-23-68-44-73-98-87
32-45-19-09-32-21-07
00-07-34-21-79-82-21
71-48-00-71-76-37-60
58-83-40-36-55-92-79
79-21-14-76-38-94-49
80-68-03-20-28-36-87
61-06-20-44-19-50-27
02-71-09-46-02-77-01
97-02-89-39-51-57-45
90-90-25-70-96-57-78
12-31-23-39-22-19-49
05-32-23-84-24-00-09
53-78-44-05-69-82-19
29-77-88-44-31-29-36
34-73-69-69-53-59-25
71-66-51-35-53-29-95
16-95-52-71-19-23-20
38-16-67-97-47-29-82
87-08-91-20-38-46-32
58-74-83-45-82-59-19
48-41-67-61-01-96-92
76-95-03-63-10-18-39
29-32-93-82-25-29-56
39-32-31-37-91-78-45
00-84-92-88-61-09-66
02-61-52-90-79-96-34
52-97-20-79-38-86-51
76-48-21-82-43-43-80
73-21-43-93-39-36-74
16-87-26-27-94-22-46
64-74-00-76-70-33-26
67-41-92-18-56-05-09
13-55-02-86-61-16-95
68-67-72-43-39-48-71
02-20-42-68-50-30-24
81-59-13-84-17-42-96
93-94-95-35-23-68-02
46-88-55-91-39-85-98
34-41-63-45-30-75-63
73-43-03-86-25-51-40
30-76-97-41-02-58-36
27-37-86-88-71-97-99
07-44-36-19-40-72-04
91-55-25-24-73-65-16
74-54-91-40-64-42-94
36-30-21-26-23-48-68
79-83-86-59-11-18-74
25-99-97-49-02-63-90
56-13-47-96-62-62-16
28-52-83-51-16-13-03
14-80-79-79-62-70-67
54-63-36-53-55-69-20
47-84-33-35-58-35-36
68-35-65-98-15-89-52
01-38-28-66-99-84-39
55-97-59-20-47-69-18
99-88-32-71-12-42-94
33-06-14-42-79-98-95
31-19-17-66-90-50-92
77-00-02-95-76-47-68
88-75-41-20-73-22-22
23-18-39-53-89-39-91

View File

@@ -1184,11 +1184,11 @@ async def choose_accounts_bulk_add(callback: CallbackQuery, state: FSMContext):
text = f"🏦 Массовое добавление в: {lottery.title}\n\n"
text += "Введите список номеров счетов через запятую или новую строку:\n\n"
text += "Примеры:\n"
text += "• 12-34-56-78-90-12-34-56\n"
text += "• 98-76-54-32-10-98-76-54, 11-22-33-44-55-66-77-88\n"
text += "• 12345678901234567890 (будет отформатирован)\n\n"
text += "Формат: XX-XX-XX-XX-XX-XX-XX-XX\n"
text += "Всего 8 пар цифр разделенных дефисами"
text += "• 12-34-56-78-90-12-34\n"
text += "• 98-76-54-32-10-98-76, 11-22-33-44-55-66-77\n"
text += "• 12345678901234 (будет отформатирован)\n\n"
text += "Формат: XX-XX-XX-XX-XX-XX-XX\n"
text += "Всего 7 пар цифр разделенных дефисами"
await callback.message.edit_text(
text,
@@ -1314,10 +1314,10 @@ async def choose_accounts_bulk_remove(callback: CallbackQuery, state: FSMContext
text = f"🏦 Массовое удаление из: {lottery.title}\n\n"
text += "Введите список номеров счетов через запятую или новую строку:\n\n"
text += "Примеры:\n"
text += "• 12-34-56-78-90-12-34-56\n"
text += "• 98-76-54-32-10-98-76-54, 11-22-33-44-55-66-77-88\n"
text += "• 12345678901234567890 (будет отформатирован)\n\n"
text += "Формат: XX-XX-XX-XX-XX-XX-XX-XX"
text += "• 12-34-56-78-90-12-34\n"
text += "• 98-76-54-32-10-98-76, 11-22-33-44-55-66-77\n"
text += "• 12345678901234 (будет отформатирован)\n\n"
text += "Формат: XX-XX-XX-XX-XX-XX-XX"
await callback.message.edit_text(
text,
@@ -1868,6 +1868,20 @@ async def start_set_manual_winner(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@admin_router.callback_query(F.data.startswith("admin_set_winner_"))
async def handle_set_winner_from_lottery(callback: CallbackQuery, state: FSMContext):
"""Обработчик для кнопки 'Установить победителя' из карточки розыгрыша"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
lottery_id = int(callback.data.split("_")[-1])
# Перенаправляем на стандартный обработчик
callback.data = f"admin_choose_winner_lottery_{lottery_id}"
await choose_winner_place(callback, state)
@admin_router.callback_query(F.data.startswith("admin_choose_winner_lottery_"))
async def choose_winner_place(callback: CallbackQuery, state: FSMContext):
"""Выбор места для победителя"""
@@ -2023,6 +2037,101 @@ async def process_winner_user(message: Message, state: FSMContext):
)
# ======================
# ПРОВЕДЕНИЕ РОЗЫГРЫША
# ======================
@admin_router.callback_query(F.data == "admin_conduct_draw")
async def choose_lottery_for_draw(callback: CallbackQuery):
"""Выбор розыгрыша для проведения"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
async with async_session_maker() as session:
lotteries = await LotteryService.get_active_lotteries(session, limit=100)
if not lotteries:
await callback.answer("Нет активных розыгрышей", show_alert=True)
return
text = "🎲 Выберите розыгрыш для проведения:\n\n"
buttons = []
for lottery in lotteries:
async with async_session_maker() as session:
participants_count = await ParticipationService.get_participants_count(session, lottery.id)
text += f"🎯 {lottery.title}\n"
text += f" 👥 Участников: {participants_count}\n"
if lottery.is_completed:
text += f" ✅ Завершён\n"
text += "\n"
buttons.append([
InlineKeyboardButton(
text=f"🎲 {lottery.title[:30]}...",
callback_data=f"admin_conduct_{lottery.id}"
)
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_draws")])
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@admin_router.callback_query(F.data.startswith("admin_conduct_"))
async def conduct_lottery_draw(callback: CallbackQuery):
"""Проведение розыгрыша"""
if not is_admin(callback.from_user.id):
await callback.answer("❌ Недостаточно прав", show_alert=True)
return
lottery_id = int(callback.data.split("_")[-1])
async with async_session_maker() as session:
lottery = await LotteryService.get_lottery(session, lottery_id)
if not lottery:
await callback.answer("Розыгрыш не найден", show_alert=True)
return
if lottery.is_completed:
await callback.answer("Розыгрыш уже завершён", show_alert=True)
return
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
if participants_count == 0:
await callback.answer("Нет участников для розыгрыша", show_alert=True)
return
# Проводим розыгрыш
from conduct_draw import conduct_draw
winners = await conduct_draw(lottery_id)
if winners:
text = f"🎉 Розыгрыш '{lottery.title}' завершён!\n\n"
text += "🏆 Победители:\n"
for winner in winners:
if winner.account_number:
text += f"{winner.place} место: {winner.account_number}\n"
elif winner.user:
username = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
text += f"{winner.place} место: {username}\n"
else:
text += f"{winner.place} место: ID {winner.user_id}\n"
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К розыгрышам", callback_data="admin_draws")]
])
)
else:
await callback.answer("Ошибка при проведении розыгрыша", show_alert=True)
# ======================
# СТАТИСТИКА
# ======================

View File

@@ -55,8 +55,7 @@ version_path_separator = os
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///./lottery_bot.db
sqlalchemy.url = postgresql+asyncpg://trevor:R0sebud@192.168.0.102/bot_db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run

View File

@@ -1,6 +1,6 @@
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import declarative_base
from dotenv import load_dotenv
# Загружаем переменные окружения

10
main.py
View File

@@ -17,6 +17,7 @@ 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 (
async_user_action, admin_async_action, db_operation,
TaskManagerMiddleware, shutdown_task_manager,
@@ -575,9 +576,9 @@ async def start_account_setup(callback: CallbackQuery, state: FSMContext):
text = f"💳 **Процедура {action} счёта**\n\n"
text += "Введите номер вашего клиентского счёта в формате:\n"
text += "`12-34-56-78-90-12-34-56`\n\n"
text += "`12-34-56-78-90-12-34`\n\n"
text += "📝 **Требования:**\n"
text += "• Ровно 16 цифр\n"
text += "• Ровно 14 цифр\n"
text += "• Разделены дефисами через каждые 2 цифры\n"
text += "• Номер должен быть уникальным\n\n"
text += "✉️ Отправьте номер счёта в ответном сообщении"
@@ -603,8 +604,8 @@ async def process_account_number(message: Message, state: FSMContext):
if not formatted_number:
await message.answer(
"❌ **Некорректный формат номера счёта**\n\n"
"Номер должен содержать ровно 16 цифр.\n"
"Пример правильного формата: `12-34-56-78-90-12-34-56`\n\n"
"Номер должен содержать ровно 14 цифр.\n"
"Пример правильного формата: `12-34-56-78-90-12-34`\n\n"
"Попробуйте ещё раз:",
parse_mode="Markdown"
)
@@ -699,6 +700,7 @@ async def main():
await set_commands()
# Подключение роутеров
dp.include_router(account_router) # Роутер для работы со счетами (приоритетный)
dp.include_router(router)
dp.include_router(admin_router)

View File

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

View File

@@ -1,6 +1,6 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON
from sqlalchemy.orm import relationship
from datetime import datetime
from datetime import datetime, timezone
from database import Base
@@ -13,10 +13,10 @@ class User(Base):
username = Column(String(255))
first_name = Column(String(255))
last_name = Column(String(255))
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
is_admin = Column(Boolean, default=False)
# Клиентский счет в формате: XX-XX-XX-XX-XX-XX-XX-XX (8 пар цифр через дефис)
account_number = Column(String(23), unique=True, nullable=True, index=True)
# Клиентский счет в формате: XX-XX-XX-XX-XX-XX-XX (7 пар цифр через дефис)
account_number = Column(String(20), unique=True, nullable=True, index=True)
# Связи
participations = relationship("Participation", back_populates="user")
@@ -32,9 +32,9 @@ class Lottery(Base):
id = Column(Integer, primary_key=True)
title = Column(String(500), nullable=False)
description = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
start_date = Column(DateTime)
end_date = Column(DateTime)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
start_date = Column(DateTime(timezone=True))
end_date = Column(DateTime(timezone=True))
is_active = Column(Boolean, default=True)
is_completed = Column(Boolean, default=False)
prizes = Column(JSON) # Список призов в формате JSON
@@ -60,15 +60,18 @@ class Participation(Base):
__tablename__ = "participations"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Опционально
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
account_number = Column(String(20), nullable=True, index=True) # Счет участника (XX-XX-XX-XX-XX-XX-XX)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Связи
user = relationship("User", back_populates="participations")
lottery = relationship("Lottery", back_populates="participations")
def __repr__(self):
if self.account_number:
return f"<Participation(account={self.account_number}, lottery_id={self.lottery_id})>"
return f"<Participation(user_id={self.user_id}, lottery_id={self.lottery_id})>"
@@ -78,15 +81,18 @@ class Winner(Base):
id = Column(Integer, primary_key=True)
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Опционально
account_number = Column(String(20), nullable=True, index=True) # Счет победителя
place = Column(Integer, nullable=False) # Место (1, 2, 3...)
prize = Column(String(500)) # Описание приза
is_manual = Column(Boolean, default=False) # Был ли установлен вручную
created_at = Column(DateTime, default=datetime.utcnow)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Связи
user = relationship("User")
lottery = relationship("Lottery")
def __repr__(self):
if self.account_number:
return f"<Winner(lottery_id={self.lottery_id}, account={self.account_number}, place={self.place})>"
return f"<Winner(lottery_id={self.lottery_id}, user_id={self.user_id}, place={self.place})>"

View File

@@ -1,6 +1,8 @@
aiogram==3.1.1
sqlalchemy==2.0.23
alembic==1.8.1
python-dotenv==1.0.0
asyncpg==0.28.0
aiosqlite==0.17.0
# Updated for Python 3.12 compatibility
aiogram==3.16.0
aiohttp>=3.11.0
sqlalchemy==2.0.36
alembic==1.14.0
python-dotenv==1.0.1
asyncpg==0.30.0
aiosqlite==0.20.0

View File

@@ -198,22 +198,28 @@ class LotteryService:
return result.scalar_one_or_none()
@staticmethod
async def get_active_lotteries(session: AsyncSession) -> List[Lottery]:
async def get_active_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
"""Получить список активных розыгрышей"""
result = await session.execute(
select(Lottery)
.where(Lottery.is_active == True, Lottery.is_completed == False)
.order_by(Lottery.created_at.desc())
)
query = select(Lottery).where(
Lottery.is_active == True,
Lottery.is_completed == False
).order_by(Lottery.created_at.desc())
if limit:
query = query.limit(limit)
result = await session.execute(query)
return result.scalars().all()
@staticmethod
async def get_all_lotteries(session: AsyncSession) -> List[Lottery]:
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
"""Получить список всех розыгрышей"""
result = await session.execute(
select(Lottery)
.order_by(Lottery.created_at.desc())
)
query = select(Lottery).order_by(Lottery.created_at.desc())
if limit:
query = query.limit(limit)
result = await session.execute(query)
return result.scalars().all()
@staticmethod