Files
new_lottery_bot/src/core/models.py
Andrey K. Choi 0fdad07d82
Some checks failed
continuous-integration/drone/pr Build is failing
refactor
2026-02-17 00:22:42 +09:00

316 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, JSON, UniqueConstraint, BigInteger
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
from .database import Base
import secrets
class User(Base):
"""Модель пользователя с регистрацией"""
__tablename__ = "users"
id = Column(Integer, primary_key=True)
telegram_id = Column(BigInteger, unique=True, nullable=False, index=True)
username = Column(String(255))
first_name = Column(String(255))
last_name = Column(String(255))
nickname = Column(String(100), nullable=True) # Никнейм пользователя для чата
phone = Column(String(20), nullable=True) # Телефон для верификации
club_card_number = Column(String(50), unique=True, nullable=True, index=True) # Номер клубной карты
is_registered = Column(Boolean, default=False) # Прошел ли полную регистрацию
is_admin = Column(Boolean, default=False)
is_chat_banned = Column(Boolean, default=False) # Заблокирован ли в чате бота
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
last_activity = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) # Последняя активность
# Секретный код для верификации выигрыша (генерируется при регистрации)
verification_code = Column(String(10), unique=True, nullable=True)
# Связи
accounts = relationship("Account", back_populates="owner", cascade="all, delete-orphan")
participations = relationship("Participation", back_populates="user")
winners = relationship("Winner", back_populates="user")
def __repr__(self):
return f"<User(telegram_id={self.telegram_id}, club_card={self.club_card_number})>"
def generate_verification_code(self):
"""Генерирует уникальный код верификации"""
self.verification_code = secrets.token_hex(4).upper() # 8-символьный код
class Account(Base):
"""Модель счета клиента (может быть несколько у одного пользователя)"""
__tablename__ = "accounts"
id = Column(Integer, primary_key=True)
account_number = Column(String(20), unique=True, nullable=False, index=True) # XX-XX-XX-XX-XX-XX-XX
owner_id = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
is_active = Column(Boolean, default=True) # Активен ли счет
# Связи
owner = relationship("User", back_populates="accounts")
participations = relationship("Participation", back_populates="account")
def __repr__(self):
return f"<Account(number={self.account_number}, owner_id={self.owner_id})>"
class WinnerVerification(Base):
"""Модель верификации победителя"""
__tablename__ = "winner_verifications"
id = Column(Integer, primary_key=True)
winner_id = Column(Integer, ForeignKey("winners.id"), nullable=False, unique=True)
verification_token = Column(String(32), unique=True, nullable=False) # Токен для подтверждения
is_verified = Column(Boolean, default=False)
verified_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
expires_at = Column(DateTime(timezone=True), nullable=False) # Срок действия токена
# Связи
winner = relationship("Winner", back_populates="verification")
def __repr__(self):
return f"<WinnerVerification(winner_id={self.winner_id}, verified={self.is_verified})>"
@staticmethod
def generate_token():
"""Генерирует уникальный токен верификации"""
return secrets.token_urlsafe(24)
class Lottery(Base):
"""Модель розыгрыша"""
__tablename__ = "lotteries"
id = Column(Integer, primary_key=True)
title = Column(String(500), nullable=False)
description = Column(Text)
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
creator_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Настройки для ручного управления победителями
manual_winners = Column(JSON, default=lambda: {}) # {место: telegram_id}
draw_results = Column(JSON) # Результаты розыгрыша
# Тип отображения победителей: "username", "chat_id", "account_number"
winner_display_type = Column(String(20), default="username")
# Связи
creator = relationship("User")
participations = relationship("Participation", back_populates="lottery")
def __repr__(self):
return f"<Lottery(id={self.id}, title={self.title})>"
class Participation(Base):
"""Модель участия в розыгрыше"""
__tablename__ = "participations"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
lottery_id = Column(Integer, ForeignKey("lotteries.id"), nullable=False)
account_id = Column(Integer, ForeignKey("accounts.id"), nullable=True) # Привязка к счету
account_number = Column(String(20), nullable=True, index=True) # Дублируем для быстрого доступа
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Связи
user = relationship("User", back_populates="participations")
lottery = relationship("Lottery", back_populates="participations")
account = relationship("Account", 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})>"
class Winner(Base):
"""Модель победителя розыгрыша"""
__tablename__ = "winners"
id = Column(Integer, primary_key=True)
lottery_id = Column(Integer, ForeignKey("lotteries.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)
prize = Column(String(500))
is_manual = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Статус подтверждения выигрыша
is_notified = Column(Boolean, default=False) # Отправлено ли уведомление
is_claimed = Column(Boolean, default=False) # Подтвердил ли победитель
claimed_at = Column(DateTime(timezone=True), nullable=True) # Время подтверждения
# Связи
user = relationship("User", back_populates="winners")
lottery = relationship("Lottery")
verification = relationship("WinnerVerification", back_populates="winner", uselist=False)
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})>"
class ChatSettings(Base):
"""Настройки системы чата"""
__tablename__ = "chat_settings"
id = Column(Integer, primary_key=True)
mode = Column(String(20), nullable=False, default='broadcast') # broadcast или forward
forward_chat_id = Column(String(50), nullable=True) # ID группы/канала для пересылки
global_ban = Column(Boolean, default=False) # Глобальный бан чата
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
def __repr__(self):
return f"<ChatSettings(mode={self.mode}, global_ban={self.global_ban})>"
class BannedUser(Base):
"""Забаненные пользователи (не могут отправлять сообщения)"""
__tablename__ = "banned_users"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
telegram_id = Column(BigInteger, nullable=False, index=True)
banned_by = Column(Integer, ForeignKey("users.id"), nullable=False)
reason = Column(Text, nullable=True)
banned_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
is_active = Column(Boolean, default=True, index=True) # Активен ли бан
# Связи
user = relationship("User", foreign_keys=[user_id])
admin = relationship("User", foreign_keys=[banned_by])
def __repr__(self):
return f"<BannedUser(telegram_id={self.telegram_id}, is_active={self.is_active})>"
class ChatMessage(Base):
"""История сообщений чата (для модерации)"""
__tablename__ = "chat_messages"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
telegram_message_id = Column(Integer, nullable=False)
message_type = Column(String(20), nullable=False) # text, photo, video, document, animation, sticker, voice, etc.
text = Column(Text, nullable=True) # Текст сообщения
file_id = Column(String(255), nullable=True) # ID файла в Telegram
forwarded_message_ids = Column(JSON, nullable=True) # Список telegram_message_id пересланных сообщений {"user_telegram_id": message_id}
is_deleted = Column(Boolean, default=False, index=True)
deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True)
deleted_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
# Связи
sender = relationship("User", foreign_keys=[user_id])
moderator = relationship("User", foreign_keys=[deleted_by])
def __repr__(self):
return f"<ChatMessage(id={self.id}, user_id={self.user_id}, type={self.message_type})>"
class P2PMessage(Base):
"""P2P сообщения между пользователями"""
__tablename__ = "p2p_messages"
id = Column(Integer, primary_key=True)
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
recipient_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
message_type = Column(String(20), nullable=False) # text, photo, video, etc.
text = Column(Text, nullable=True)
file_id = Column(String(255), nullable=True)
sender_message_id = Column(Integer, nullable=False) # ID сообщения у отправителя
recipient_message_id = Column(Integer, nullable=True) # ID сообщения у получателя
is_read = Column(Boolean, default=False, index=True)
read_at = Column(DateTime(timezone=True), nullable=True)
reply_to_id = Column(Integer, ForeignKey("p2p_messages.id"), nullable=True) # Ответ на сообщение
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True)
# Связи
sender = relationship("User", foreign_keys=[sender_id], backref="sent_p2p_messages")
recipient = relationship("User", foreign_keys=[recipient_id], backref="received_p2p_messages")
reply_to = relationship("P2PMessage", remote_side=[id], backref="replies")
def __repr__(self):
return f"<P2PMessage(id={self.id}, from={self.sender_id}, to={self.recipient_id})>"
class BroadcastChannel(Base):
"""Каналы и группы для рассылки"""
__tablename__ = "broadcast_channels"
id = Column(Integer, primary_key=True)
chat_id = Column(BigInteger, nullable=False, unique=True, index=True) # ID канала или группы
chat_type = Column(String(20), nullable=False) # 'channel' или 'group'
title = Column(String(255), nullable=False) # Название
username = Column(String(255), nullable=True) # Username (если есть)
description = Column(Text, nullable=True) # Описание
is_active = Column(Boolean, default=True, index=True) # Активен ли для рассылок
added_by = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
# Связи
admin = relationship("User")
def __repr__(self):
return f"<BroadcastChannel(id={self.id}, title={self.title}, type={self.chat_type})>"
class BlockedUser(Base):
"""Пользователи, которые заблокировали бота или недоступны"""
__tablename__ = "blocked_users"
id = Column(Integer, primary_key=True)
telegram_id = Column(BigInteger, nullable=False, unique=True, index=True)
error_type = Column(String(100), nullable=False) # тип ошибки (blocked, deleted, deactivated, etc.)
error_message = Column(Text, nullable=True) # Полное сообщение об ошибке
first_blocked_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
last_attempt_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
attempt_count = Column(Integer, default=1) # Количество неудачных попыток
is_active = Column(Boolean, default=True, index=True) # Активна ли блокировка
def __repr__(self):
return f"<BlockedUser(telegram_id={self.telegram_id}, error={self.error_type})>"
class BroadcastLog(Base):
"""История рассылок"""
__tablename__ = "broadcast_logs"
id = Column(Integer, primary_key=True)
broadcast_type = Column(String(20), nullable=False, index=True) # 'direct', 'channel', 'group'
target_id = Column(BigInteger, nullable=True) # ID канала/группы (null для direct)
message_type = Column(String(20), nullable=False) # text, photo, video, etc.
message_text = Column(Text, nullable=True) # Текст сообщения
file_id = Column(String(255), nullable=True) # ID файла (если есть)
# Статистика
total_recipients = Column(Integer, default=0) # Всего получателей
success_count = Column(Integer, default=0) # Успешно доставлено
failed_count = Column(Integer, default=0) # Не доставлено
blocked_count = Column(Integer, default=0) # Заблокировали бота
# Метаданные
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
started_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
completed_at = Column(DateTime(timezone=True), nullable=True)
status = Column(String(20), default='pending', index=True) # pending, in_progress, completed, failed
# Связи
admin = relationship("User")
def __repr__(self):
return f"<BroadcastLog(id={self.id}, type={self.broadcast_type}, status={self.status})>"