231 lines
12 KiB
Python
231 lines
12 KiB
Python
|
|
"""Add shop system with coins, items, inventory, certification
|
||
|
|
|
||
|
|
Revision ID: 023_add_shop_system
|
||
|
|
Revises: 022_add_notification_settings
|
||
|
|
Create Date: 2025-01-05
|
||
|
|
|
||
|
|
"""
|
||
|
|
from typing import Sequence, Union
|
||
|
|
|
||
|
|
from alembic import op
|
||
|
|
import sqlalchemy as sa
|
||
|
|
from sqlalchemy import inspect
|
||
|
|
|
||
|
|
|
||
|
|
# revision identifiers, used by Alembic.
|
||
|
|
revision: str = '023_add_shop_system'
|
||
|
|
down_revision: Union[str, None] = '022_add_notification_settings'
|
||
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
||
|
|
depends_on: Union[str, Sequence[str], None] = None
|
||
|
|
|
||
|
|
|
||
|
|
def column_exists(table_name: str, column_name: str) -> bool:
|
||
|
|
bind = op.get_bind()
|
||
|
|
inspector = inspect(bind)
|
||
|
|
columns = [col['name'] for col in inspector.get_columns(table_name)]
|
||
|
|
return column_name in columns
|
||
|
|
|
||
|
|
|
||
|
|
def table_exists(table_name: str) -> bool:
|
||
|
|
bind = op.get_bind()
|
||
|
|
inspector = inspect(bind)
|
||
|
|
return table_name in inspector.get_table_names()
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
# === 1. Создаём таблицу shop_items ===
|
||
|
|
if not table_exists('shop_items'):
|
||
|
|
op.create_table(
|
||
|
|
'shop_items',
|
||
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
||
|
|
sa.Column('item_type', sa.String(30), nullable=False, index=True),
|
||
|
|
sa.Column('code', sa.String(50), nullable=False, unique=True, index=True),
|
||
|
|
sa.Column('name', sa.String(100), nullable=False),
|
||
|
|
sa.Column('description', sa.Text(), nullable=True),
|
||
|
|
sa.Column('price', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('rarity', sa.String(20), nullable=False, server_default='common'),
|
||
|
|
sa.Column('asset_data', sa.JSON(), nullable=True),
|
||
|
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||
|
|
sa.Column('available_from', sa.DateTime(), nullable=True),
|
||
|
|
sa.Column('available_until', sa.DateTime(), nullable=True),
|
||
|
|
sa.Column('stock_limit', sa.Integer(), nullable=True),
|
||
|
|
sa.Column('stock_remaining', sa.Integer(), nullable=True),
|
||
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||
|
|
)
|
||
|
|
|
||
|
|
# === 2. Создаём таблицу user_inventory ===
|
||
|
|
if not table_exists('user_inventory'):
|
||
|
|
op.create_table(
|
||
|
|
'user_inventory',
|
||
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
||
|
|
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True),
|
||
|
|
sa.Column('item_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='CASCADE'), nullable=False, index=True),
|
||
|
|
sa.Column('quantity', sa.Integer(), nullable=False, server_default='1'),
|
||
|
|
sa.Column('equipped', sa.Boolean(), nullable=False, server_default='false'),
|
||
|
|
sa.Column('purchased_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||
|
|
sa.Column('expires_at', sa.DateTime(), nullable=True),
|
||
|
|
)
|
||
|
|
|
||
|
|
# === 3. Создаём таблицу coin_transactions ===
|
||
|
|
if not table_exists('coin_transactions'):
|
||
|
|
op.create_table(
|
||
|
|
'coin_transactions',
|
||
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
||
|
|
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True),
|
||
|
|
sa.Column('amount', sa.Integer(), nullable=False),
|
||
|
|
sa.Column('transaction_type', sa.String(30), nullable=False),
|
||
|
|
sa.Column('reference_type', sa.String(30), nullable=True),
|
||
|
|
sa.Column('reference_id', sa.Integer(), nullable=True),
|
||
|
|
sa.Column('description', sa.Text(), nullable=True),
|
||
|
|
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||
|
|
)
|
||
|
|
|
||
|
|
# === 4. Создаём таблицу consumable_usages ===
|
||
|
|
if not table_exists('consumable_usages'):
|
||
|
|
op.create_table(
|
||
|
|
'consumable_usages',
|
||
|
|
sa.Column('id', sa.Integer(), primary_key=True),
|
||
|
|
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True),
|
||
|
|
sa.Column('item_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='CASCADE'), nullable=False),
|
||
|
|
sa.Column('marathon_id', sa.Integer(), sa.ForeignKey('marathons.id', ondelete='CASCADE'), nullable=True),
|
||
|
|
sa.Column('assignment_id', sa.Integer(), sa.ForeignKey('assignments.id', ondelete='CASCADE'), nullable=True),
|
||
|
|
sa.Column('used_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
|
||
|
|
sa.Column('effect_data', sa.JSON(), nullable=True),
|
||
|
|
)
|
||
|
|
|
||
|
|
# === 5. Добавляем поля в users ===
|
||
|
|
|
||
|
|
# coins_balance - баланс монет
|
||
|
|
if not column_exists('users', 'coins_balance'):
|
||
|
|
op.add_column('users', sa.Column('coins_balance', sa.Integer(), nullable=False, server_default='0'))
|
||
|
|
|
||
|
|
# equipped_frame_id - экипированная рамка
|
||
|
|
if not column_exists('users', 'equipped_frame_id'):
|
||
|
|
op.add_column('users', sa.Column('equipped_frame_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True))
|
||
|
|
|
||
|
|
# equipped_title_id - экипированный титул
|
||
|
|
if not column_exists('users', 'equipped_title_id'):
|
||
|
|
op.add_column('users', sa.Column('equipped_title_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True))
|
||
|
|
|
||
|
|
# equipped_name_color_id - экипированный цвет ника
|
||
|
|
if not column_exists('users', 'equipped_name_color_id'):
|
||
|
|
op.add_column('users', sa.Column('equipped_name_color_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True))
|
||
|
|
|
||
|
|
# equipped_background_id - экипированный фон
|
||
|
|
if not column_exists('users', 'equipped_background_id'):
|
||
|
|
op.add_column('users', sa.Column('equipped_background_id', sa.Integer(), sa.ForeignKey('shop_items.id', ondelete='SET NULL'), nullable=True))
|
||
|
|
|
||
|
|
# === 6. Добавляем поля сертификации в marathons ===
|
||
|
|
|
||
|
|
# certification_status - статус сертификации
|
||
|
|
if not column_exists('marathons', 'certification_status'):
|
||
|
|
op.add_column('marathons', sa.Column('certification_status', sa.String(20), nullable=False, server_default='none'))
|
||
|
|
|
||
|
|
# certification_requested_at - когда подана заявка
|
||
|
|
if not column_exists('marathons', 'certification_requested_at'):
|
||
|
|
op.add_column('marathons', sa.Column('certification_requested_at', sa.DateTime(), nullable=True))
|
||
|
|
|
||
|
|
# certified_at - когда сертифицирован
|
||
|
|
if not column_exists('marathons', 'certified_at'):
|
||
|
|
op.add_column('marathons', sa.Column('certified_at', sa.DateTime(), nullable=True))
|
||
|
|
|
||
|
|
# certified_by_id - кем сертифицирован
|
||
|
|
if not column_exists('marathons', 'certified_by_id'):
|
||
|
|
op.add_column('marathons', sa.Column('certified_by_id', sa.Integer(), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True))
|
||
|
|
|
||
|
|
# certification_rejection_reason - причина отказа
|
||
|
|
if not column_exists('marathons', 'certification_rejection_reason'):
|
||
|
|
op.add_column('marathons', sa.Column('certification_rejection_reason', sa.Text(), nullable=True))
|
||
|
|
|
||
|
|
# === 7. Добавляем настройки consumables в marathons ===
|
||
|
|
|
||
|
|
# allow_skips - разрешены ли скипы
|
||
|
|
if not column_exists('marathons', 'allow_skips'):
|
||
|
|
op.add_column('marathons', sa.Column('allow_skips', sa.Boolean(), nullable=False, server_default='true'))
|
||
|
|
|
||
|
|
# max_skips_per_participant - лимит скипов на участника
|
||
|
|
if not column_exists('marathons', 'max_skips_per_participant'):
|
||
|
|
op.add_column('marathons', sa.Column('max_skips_per_participant', sa.Integer(), nullable=True))
|
||
|
|
|
||
|
|
# allow_consumables - разрешены ли расходуемые
|
||
|
|
if not column_exists('marathons', 'allow_consumables'):
|
||
|
|
op.add_column('marathons', sa.Column('allow_consumables', sa.Boolean(), nullable=False, server_default='true'))
|
||
|
|
|
||
|
|
# === 8. Добавляем поля в participants ===
|
||
|
|
|
||
|
|
# coins_earned - заработано монет в марафоне
|
||
|
|
if not column_exists('participants', 'coins_earned'):
|
||
|
|
op.add_column('participants', sa.Column('coins_earned', sa.Integer(), nullable=False, server_default='0'))
|
||
|
|
|
||
|
|
# skips_used - использовано скипов
|
||
|
|
if not column_exists('participants', 'skips_used'):
|
||
|
|
op.add_column('participants', sa.Column('skips_used', sa.Integer(), nullable=False, server_default='0'))
|
||
|
|
|
||
|
|
# active_boost_multiplier - активный множитель буста
|
||
|
|
if not column_exists('participants', 'active_boost_multiplier'):
|
||
|
|
op.add_column('participants', sa.Column('active_boost_multiplier', sa.Float(), nullable=True))
|
||
|
|
|
||
|
|
# active_boost_expires_at - когда истекает буст
|
||
|
|
if not column_exists('participants', 'active_boost_expires_at'):
|
||
|
|
op.add_column('participants', sa.Column('active_boost_expires_at', sa.DateTime(), nullable=True))
|
||
|
|
|
||
|
|
# has_shield - есть ли активный щит
|
||
|
|
if not column_exists('participants', 'has_shield'):
|
||
|
|
op.add_column('participants', sa.Column('has_shield', sa.Boolean(), nullable=False, server_default='false'))
|
||
|
|
|
||
|
|
|
||
|
|
def downgrade() -> None:
|
||
|
|
# === Удаляем поля из participants ===
|
||
|
|
if column_exists('participants', 'has_shield'):
|
||
|
|
op.drop_column('participants', 'has_shield')
|
||
|
|
if column_exists('participants', 'active_boost_expires_at'):
|
||
|
|
op.drop_column('participants', 'active_boost_expires_at')
|
||
|
|
if column_exists('participants', 'active_boost_multiplier'):
|
||
|
|
op.drop_column('participants', 'active_boost_multiplier')
|
||
|
|
if column_exists('participants', 'skips_used'):
|
||
|
|
op.drop_column('participants', 'skips_used')
|
||
|
|
if column_exists('participants', 'coins_earned'):
|
||
|
|
op.drop_column('participants', 'coins_earned')
|
||
|
|
|
||
|
|
# === Удаляем поля consumables из marathons ===
|
||
|
|
if column_exists('marathons', 'allow_consumables'):
|
||
|
|
op.drop_column('marathons', 'allow_consumables')
|
||
|
|
if column_exists('marathons', 'max_skips_per_participant'):
|
||
|
|
op.drop_column('marathons', 'max_skips_per_participant')
|
||
|
|
if column_exists('marathons', 'allow_skips'):
|
||
|
|
op.drop_column('marathons', 'allow_skips')
|
||
|
|
|
||
|
|
# === Удаляем поля сертификации из marathons ===
|
||
|
|
if column_exists('marathons', 'certification_rejection_reason'):
|
||
|
|
op.drop_column('marathons', 'certification_rejection_reason')
|
||
|
|
if column_exists('marathons', 'certified_by_id'):
|
||
|
|
op.drop_column('marathons', 'certified_by_id')
|
||
|
|
if column_exists('marathons', 'certified_at'):
|
||
|
|
op.drop_column('marathons', 'certified_at')
|
||
|
|
if column_exists('marathons', 'certification_requested_at'):
|
||
|
|
op.drop_column('marathons', 'certification_requested_at')
|
||
|
|
if column_exists('marathons', 'certification_status'):
|
||
|
|
op.drop_column('marathons', 'certification_status')
|
||
|
|
|
||
|
|
# === Удаляем поля из users ===
|
||
|
|
if column_exists('users', 'equipped_background_id'):
|
||
|
|
op.drop_column('users', 'equipped_background_id')
|
||
|
|
if column_exists('users', 'equipped_name_color_id'):
|
||
|
|
op.drop_column('users', 'equipped_name_color_id')
|
||
|
|
if column_exists('users', 'equipped_title_id'):
|
||
|
|
op.drop_column('users', 'equipped_title_id')
|
||
|
|
if column_exists('users', 'equipped_frame_id'):
|
||
|
|
op.drop_column('users', 'equipped_frame_id')
|
||
|
|
if column_exists('users', 'coins_balance'):
|
||
|
|
op.drop_column('users', 'coins_balance')
|
||
|
|
|
||
|
|
# === Удаляем таблицы ===
|
||
|
|
if table_exists('consumable_usages'):
|
||
|
|
op.drop_table('consumable_usages')
|
||
|
|
if table_exists('coin_transactions'):
|
||
|
|
op.drop_table('coin_transactions')
|
||
|
|
if table_exists('user_inventory'):
|
||
|
|
op.drop_table('user_inventory')
|
||
|
|
if table_exists('shop_items'):
|
||
|
|
op.drop_table('shop_items')
|