main functions fix
This commit is contained in:
332
ACCOUNT_SYSTEM_GUIDE.md
Normal file
332
ACCOUNT_SYSTEM_GUIDE.md
Normal 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
126
COMMAND_FIX.md
Normal 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
|
||||
```
|
||||
|
||||
### Обычный текст (должен игнорироваться):
|
||||
```
|
||||
Привет, как дела?
|
||||
Когда будет розыгрыш?
|
||||
```
|
||||
|
||||
Все должно работать корректно!
|
||||
22
Makefile
22
Makefile
@@ -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
160
POSTGRESQL_MIGRATION.md
Normal 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`!
|
||||
12
README.md
12
README.md
@@ -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
100
UPGRADE_NOTES.md
Normal 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
375
account_handlers.py
Normal 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
287
account_services.py
Normal 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
|
||||
]
|
||||
@@ -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
100
accounts_100.txt
Normal 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
|
||||
127
admin_panel.py
127
admin_panel.py
@@ -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)
|
||||
|
||||
|
||||
# ======================
|
||||
# СТАТИСТИКА
|
||||
# ======================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
10
main.py
@@ -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)
|
||||
|
||||
|
||||
@@ -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 ###
|
||||
28
models.py
28
models.py
@@ -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})>"
|
||||
@@ -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
|
||||
28
services.py
28
services.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user