feat: Система автоматического подтверждения выигрышей с поддержкой множественных счетов
Some checks reported errors
continuous-integration/drone/push Build encountered an error

Основные изменения:

 Новые функции:
- Система регистрации пользователей с множественными счетами
- Автоматическое подтверждение выигрышей через inline-кнопки
- Механизм переигровки для неподтвержденных выигрышей (24 часа)
- Подтверждение на уровне счетов (каждый счет подтверждается отдельно)
- Скрипт полной очистки базы данных

🔧 Технические улучшения:
- Исправлена ошибка MissingGreenlet при lazy loading (добавлен joinedload/selectinload)
- Добавлено поле claimed_at для отслеживания времени подтверждения
- Пакетное добавление счетов с выбором розыгрыша
- Проверка владения конкретным счетом при подтверждении

📚 Документация:
- docs/AUTO_CONFIRM_SYSTEM.md - Полная документация системы подтверждения
- docs/ACCOUNT_BASED_CONFIRMATION.md - Подтверждение на уровне счетов
- docs/REGISTRATION_SYSTEM.md - Система регистрации
- docs/ADMIN_COMMANDS.md - Команды администратора
- docs/CLEAR_DATABASE.md - Очистка БД
- docs/QUICK_GUIDE.md - Быстрое начало
- docs/UPDATE_LOG.md - Журнал обновлений

🗄️ База данных:
- Миграция 003: Таблицы accounts, winner_verifications
- Миграция 004: Поле claimed_at в таблице winners
- Скрипт scripts/clear_database.py для полной очистки

🎮 Новые команды:
Админские:
- /check_unclaimed <lottery_id> - Проверка неподтвержденных выигрышей
- /redraw <lottery_id> - Повторный розыгрыш
- /add_accounts - Пакетное добавление счетов
- /list_accounts <telegram_id> - Список счетов пользователя

Пользовательские:
- /register - Регистрация с вводом данных
- /my_account - Просмотр своих счетов
- Callback confirm_win_{id} - Подтверждение выигрыша

🛠️ Makefile:
- make clear-db - Очистка всех данных из БД (с подтверждением)

🔒 Безопасность:
- Проверка владения счетом при подтверждении
- Защита от подтверждения чужих счетов
- Независимое подтверждение каждого выигрышного счета

📊 Логика работы:
1. Пользователь регистрируется и добавляет счета
2. Счета участвуют в розыгрыше
3. Победители получают уведомление с кнопкой подтверждения
4. Каждый счет подтверждается отдельно (24 часа на подтверждение)
5. Неподтвержденные выигрыши переигрываются через /redraw
This commit is contained in:
2025-11-16 14:01:30 +09:00
parent 31c4c5382a
commit 505d26f0e9
21 changed files with 4217 additions and 68 deletions

View File

@@ -0,0 +1,108 @@
"""Add registration and account management
Revision ID: 003
Revises: init
Create Date: 2025-11-16 13:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '003'
down_revision = 'init'
branch_labels = None
depends_on = None
def upgrade() -> None:
# Добавляем новые поля в users
op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))
op.add_column('users', sa.Column('club_card_number', sa.String(50), nullable=True))
op.add_column('users', sa.Column('is_registered', sa.Boolean(), server_default='false', nullable=False))
op.add_column('users', sa.Column('verification_code', sa.String(10), nullable=True))
# Создаем индексы для users
op.create_index('ix_users_club_card_number', 'users', ['club_card_number'], unique=True)
op.create_index('ix_users_verification_code', 'users', ['verification_code'], unique=True)
# Удаляем старое поле account_number из users (оно теперь в отдельной таблице)
# Сначала удаляем индекс
op.drop_index('ix_users_account_number', table_name='users')
op.drop_column('users', 'account_number')
# Создаем таблицу accounts
op.create_table(
'accounts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('account_number', sa.String(20), nullable=False),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_accounts_account_number', 'accounts', ['account_number'], unique=True)
op.create_index('ix_accounts_owner_id', 'accounts', ['owner_id'])
# Добавляем поле account_id в participations
op.add_column('participations', sa.Column('account_id', sa.Integer(), nullable=True))
op.create_foreign_key(
'fk_participations_account_id',
'participations', 'accounts',
['account_id'], ['id'],
ondelete='SET NULL'
)
# Добавляем поля в winners для отслеживания статуса
op.add_column('winners', sa.Column('is_notified', sa.Boolean(), server_default='false', nullable=False))
op.add_column('winners', sa.Column('is_claimed', sa.Boolean(), server_default='false', nullable=False))
# Создаем таблицу winner_verifications
op.create_table(
'winner_verifications',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('winner_id', sa.Integer(), nullable=False),
sa.Column('verification_token', sa.String(32), nullable=False),
sa.Column('is_verified', sa.Boolean(), server_default='false', nullable=False),
sa.Column('verified_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['winner_id'], ['winners.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_winner_verifications_winner_id', 'winner_verifications', ['winner_id'], unique=True)
op.create_index('ix_winner_verifications_token', 'winner_verifications', ['verification_token'], unique=True)
def downgrade() -> None:
# Удаляем winner_verifications
op.drop_index('ix_winner_verifications_token', table_name='winner_verifications')
op.drop_index('ix_winner_verifications_winner_id', table_name='winner_verifications')
op.drop_table('winner_verifications')
# Удаляем новые поля из winners
op.drop_column('winners', 'is_claimed')
op.drop_column('winners', 'is_notified')
# Удаляем account_id из participations
op.drop_constraint('fk_participations_account_id', 'participations', type_='foreignkey')
op.drop_column('participations', 'account_id')
# Удаляем таблицу accounts
op.drop_index('ix_accounts_owner_id', table_name='accounts')
op.drop_index('ix_accounts_account_number', table_name='accounts')
op.drop_table('accounts')
# Возвращаем account_number в users
op.add_column('users', sa.Column('account_number', sa.String(20), nullable=True))
op.create_index('ix_users_account_number', 'users', ['account_number'], unique=True)
# Удаляем новые поля из users
op.drop_index('ix_users_verification_code', table_name='users')
op.drop_index('ix_users_club_card_number', table_name='users')
op.drop_column('users', 'verification_code')
op.drop_column('users', 'is_registered')
op.drop_column('users', 'club_card_number')
op.drop_column('users', 'phone')

View File

@@ -0,0 +1,26 @@
"""Add claimed_at field to winners
Revision ID: 004
Revises: 003
Create Date: 2025-11-16
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '004'
down_revision = '003'
branch_labels = None
depends_on = None
def upgrade():
"""Add claimed_at timestamp to winners table"""
op.add_column('winners', sa.Column('claimed_at', sa.DateTime(timezone=True), nullable=True))
def downgrade():
"""Remove claimed_at field"""
op.drop_column('winners', 'claimed_at')