feat: Система автоматического подтверждения выигрышей с поддержкой множественных счетов
Some checks reported errors
continuous-integration/drone/push Build encountered an error
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:
108
migrations/versions/003_add_registration_and_accounts.py
Normal file
108
migrations/versions/003_add_registration_and_accounts.py
Normal 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')
|
||||
26
migrations/versions/004_add_claimed_at.py
Normal file
26
migrations/versions/004_add_claimed_at.py
Normal 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')
|
||||
Reference in New Issue
Block a user