763 lines
30 KiB
Python
763 lines
30 KiB
Python
"""
|
||
Модели базы данных
|
||
"""
|
||
from datetime import datetime
|
||
from sqlalchemy import (
|
||
Column, Integer, BigInteger, String, Boolean, DateTime,
|
||
Text, Float, ForeignKey, Index
|
||
)
|
||
from sqlalchemy.orm import relationship, declarative_base
|
||
|
||
Base = declarative_base()
|
||
|
||
|
||
def get_level_by_rating(rating: int) -> str:
|
||
"""Определить уровень по рейтингу"""
|
||
if rating >= 1000:
|
||
return 'Лидер'
|
||
elif rating >= 500:
|
||
return 'Активист'
|
||
elif rating >= 100:
|
||
return 'Сосед'
|
||
else:
|
||
return 'Новичок'
|
||
|
||
|
||
class User(Base):
|
||
"""Пользователи (жильцы)"""
|
||
__tablename__ = 'users'
|
||
|
||
user_id = Column(BigInteger, primary_key=True)
|
||
username = Column(String(255), nullable=True)
|
||
first_name = Column(String(255), nullable=True)
|
||
last_name = Column(String(255), nullable=True)
|
||
apartment = Column(String(10), nullable=True) # Номер квартиры
|
||
phone = Column(String(20), nullable=True) # Телефон жильца
|
||
verified = Column(Boolean, default=False) # Прошёл верификацию
|
||
verification_date = Column(DateTime, nullable=True)
|
||
join_date = Column(DateTime, default=datetime.utcnow)
|
||
message_count = Column(Integer, default=0)
|
||
is_banned = Column(Boolean, default=False)
|
||
is_admin = Column(Boolean, default=False)
|
||
last_seen = Column(DateTime, nullable=True)
|
||
|
||
# Рейтинг активности
|
||
rating = Column(Integer, default=0) # Баллы активности
|
||
level = Column(String(50), default='Новичок') # Новичок, Сосед, Активист, Лидер
|
||
|
||
# Для детекта шпионов
|
||
spy_score = Column(Integer, default=0) # 0-100
|
||
spy_flags = Column(Text, nullable=True) # JSON список флагов
|
||
|
||
# Для анти-спама
|
||
spam_warning_count = Column(Integer, default=0) # Количество предупреждений за спам
|
||
|
||
# Для анти-спам лимитов
|
||
is_muted = Column(Boolean, default=False) # Замучен ли
|
||
mute_until = Column(DateTime, nullable=True) # До когда мут
|
||
rate_limit_warnings = Column(Integer, default=0) # Предупреждения за лимиты
|
||
|
||
# Для детекта мультиаккаунтов
|
||
multicount_score = Column(Integer, default=0) # Баллы подозрительности
|
||
suspected_device_id = Column(String(100), nullable=True) # ID устройства (если есть)
|
||
suspected_alt_accounts = Column(Text, nullable=True) # JSON список подозрительных аккаунтов
|
||
|
||
# Для рейтинга токсичности
|
||
toxicity_score = Column(Integer, default=0) # Средний балл токсичности
|
||
toxic_messages_count = Column(Integer, default=0) # Количество токсичных сообщений
|
||
complaints_count = Column(Integer, default=0) # Количество жалоб
|
||
|
||
# Связи
|
||
messages = relationship('Message', back_populates='user', lazy='dynamic')
|
||
|
||
__table_args__ = (
|
||
Index('ix_users_apartment', 'apartment'),
|
||
Index('ix_users_verified', 'verified'),
|
||
Index('ix_users_spy_score', 'spy_score'),
|
||
Index('ix_users_rating', 'rating'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<User {self.user_id} ({self.apartment})>"
|
||
|
||
@property
|
||
def full_name(self):
|
||
parts = [self.first_name, self.last_name]
|
||
return ' '.join(filter(None, parts)) or self.username or f'user{self.user_id}'
|
||
|
||
def update_level(self):
|
||
"""Обновить уровень на основе рейтинга"""
|
||
self.level = get_level_by_rating(self.rating)
|
||
|
||
|
||
class Message(Base):
|
||
"""Сообщения в чате"""
|
||
__tablename__ = 'messages'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
message_id = Column(BigInteger)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'))
|
||
chat_id = Column(BigInteger)
|
||
text = Column(Text, nullable=True)
|
||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||
reply_to_message_id = Column(BigInteger, nullable=True)
|
||
forward_from = Column(String(255), nullable=True)
|
||
topic = Column(String(50), nullable=True) # Тема сообщения (лифты, парковка, уборка...)
|
||
|
||
# Связи
|
||
user = relationship('User', back_populates='messages')
|
||
|
||
__table_args__ = (
|
||
Index('ix_messages_chat_time', 'chat_id', 'timestamp'),
|
||
Index('ix_messages_user_time', 'user_id', 'timestamp'),
|
||
Index('ix_messages_topic', 'topic'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Message {self.message_id} by {self.user_id}>"
|
||
|
||
|
||
class Service(Base):
|
||
"""Контакты служб и мастеров"""
|
||
__tablename__ = 'services'
|
||
|
||
service_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
name = Column(String(255), nullable=False)
|
||
category = Column(String(100), nullable=False) # emergency, plumber, electrician, etc.
|
||
phone = Column(String(20), nullable=False)
|
||
description = Column(Text, nullable=True)
|
||
image_path = Column(String(500), nullable=True) # Путь к картинке
|
||
rating = Column(Float, default=0.0)
|
||
reviews_count = Column(Integer, default=0)
|
||
verified_by_admin = Column(Boolean, default=False)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
__table_args__ = (
|
||
Index('ix_services_category', 'category'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Service {self.name} ({self.category})>"
|
||
|
||
|
||
class VerificationRequest(Base):
|
||
"""Заявки на верификацию"""
|
||
__tablename__ = 'verification_requests'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
apartment = Column(String(10), nullable=False)
|
||
photo_file_id = Column(String(255), nullable=True) # Фото квитанции
|
||
comment = Column(Text, nullable=True)
|
||
status = Column(String(50), default='pending') # pending, approved, rejected
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
reviewed_at = Column(DateTime, nullable=True)
|
||
reviewed_by = Column(BigInteger, nullable=True) # Admin ID
|
||
reject_reason = Column(Text, nullable=True)
|
||
|
||
__table_args__ = (
|
||
Index('ix_verification_status', 'status'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<VerificationRequest {self.user_id} apt={self.apartment}>"
|
||
|
||
|
||
class SpyLog(Base):
|
||
"""Логи детекта шпионов"""
|
||
__tablename__ = 'spy_logs'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
spy_score = Column(Integer, nullable=False)
|
||
flags = Column(Text, nullable=False) # JSON список
|
||
detected_at = Column(DateTime, default=datetime.utcnow)
|
||
action_taken = Column(String(50), nullable=True) # alerted, warned, banned
|
||
|
||
__table_args__ = (
|
||
Index('ix_spy_logs_user_time', 'user_id', 'detected_at'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<SpyLog {self.user_id} score={self.spy_score}>"
|
||
|
||
|
||
class Announcement(Base):
|
||
"""Объявления для жильцов"""
|
||
__tablename__ = 'announcements'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
title = Column(String(255), nullable=False)
|
||
text = Column(Text, nullable=False)
|
||
category = Column(String(50), default='general') # general, emergency, utility
|
||
created_by = Column(BigInteger, nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
is_pinned = Column(Boolean, default=False)
|
||
is_active = Column(Boolean, default=True)
|
||
|
||
__table_args__ = (
|
||
Index('ix_announcements_category', 'category'),
|
||
Index('ix_announcements_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Announcement {self.title}>"
|
||
|
||
|
||
class Poll(Base):
|
||
"""Опросы/голосования жильцов"""
|
||
__tablename__ = 'polls'
|
||
|
||
poll_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
message_id = Column(BigInteger, nullable=False) # ID сообщения с опросом в чате
|
||
chat_id = Column(BigInteger, nullable=False) # ID чата
|
||
question = Column(Text, nullable=False) # Вопрос опроса
|
||
options = Column(Text, nullable=False) # JSON: ["Вариант 1", "Вариант 2", ...]
|
||
votes = Column(Text, nullable=True) # JSON: {"0": [user_id1, user_id2], "1": [user_id3]}
|
||
created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
is_active = Column(Boolean, default=True) # Активен ли опрос
|
||
closed_at = Column(DateTime, nullable=True) # Дата закрытия
|
||
|
||
# Связи
|
||
creator = relationship('User', backref='polls')
|
||
|
||
__table_args__ = (
|
||
Index('ix_polls_chat_active', 'chat_id', 'is_active'),
|
||
Index('ix_polls_created_at', 'created_at'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Poll {self.poll_id}: {self.question[:30]}>"
|
||
|
||
@property
|
||
def total_votes(self) -> int:
|
||
"""Общее количество голосов"""
|
||
import json
|
||
if not self.votes:
|
||
return 0
|
||
votes_data = json.loads(self.votes)
|
||
return sum(len(v) for v in votes_data.values())
|
||
|
||
def get_results(self) -> list:
|
||
"""Получить результаты в формате [(вариант, голоса, процент), ...]"""
|
||
import json
|
||
if not self.votes:
|
||
options_list = json.loads(self.options)
|
||
return [(opt, 0, 0.0) for opt in options_list]
|
||
|
||
options_list = json.loads(self.options)
|
||
votes_data = json.loads(self.votes)
|
||
total = self.total_votes
|
||
|
||
results = []
|
||
for i, option in enumerate(options_list):
|
||
vote_count = len(votes_data.get(str(i), []))
|
||
percent = (vote_count / total * 100) if total > 0 else 0.0
|
||
results.append((option, vote_count, round(percent, 1)))
|
||
|
||
return results
|
||
|
||
|
||
class SpamWarning(Base):
|
||
"""Предупреждения за спам/запрещённые ссылки"""
|
||
__tablename__ = 'spam_warnings'
|
||
|
||
warning_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
message_text = Column(Text, nullable=True) # Текст сообщения
|
||
reason = Column(Text, nullable=True) # Причина предупреждения
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
is_active = Column(Boolean, default=True) # Активно ли предупреждение
|
||
|
||
# Связи
|
||
user = relationship('User', backref='spam_warnings')
|
||
|
||
__table_args__ = (
|
||
Index('ix_spam_warnings_user', 'user_id'),
|
||
Index('ix_spam_warnings_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<SpamWarning {self.user_id}: {self.reason[:30]}>"
|
||
|
||
|
||
class MessageRate(Base):
|
||
"""Счётчик сообщений для rate limiting"""
|
||
__tablename__ = 'message_rates'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
user = relationship('User', backref='message_rates')
|
||
|
||
__table_args__ = (
|
||
Index('ix_message_rates_user', 'user_id'),
|
||
Index('ix_message_rates_time', 'timestamp'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<MessageRate {self.user_id} @ {self.timestamp}>"
|
||
|
||
|
||
class Schedule(Base):
|
||
"""Расписание отключений (вода, свет, отопление)"""
|
||
__tablename__ = 'schedules'
|
||
|
||
schedule_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
schedule_type = Column(String(50), nullable=False) # water, electricity, heating, etc.
|
||
title = Column(String(255), nullable=False)
|
||
description = Column(Text, nullable=True)
|
||
start_time = Column(DateTime, nullable=False)
|
||
end_time = Column(DateTime, nullable=False)
|
||
created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
reminder_sent = Column(Boolean, default=False) # Отправлено ли напоминание
|
||
is_active = Column(Boolean, default=True)
|
||
|
||
# Связи
|
||
creator = relationship('User', backref='schedules')
|
||
|
||
__table_args__ = (
|
||
Index('ix_schedules_type', 'schedule_type'),
|
||
Index('ix_schedules_time', 'start_time', 'end_time'),
|
||
Index('ix_schedules_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Schedule {self.title} ({self.start_time})>"
|
||
|
||
@property
|
||
def duration_hours(self) -> float:
|
||
"""Длительность отключения в часах"""
|
||
if self.start_time and self.end_time:
|
||
delta = self.end_time - self.start_time
|
||
return round(delta.total_seconds() / 3600, 1)
|
||
return 0
|
||
|
||
def get_type_emoji(self) -> str:
|
||
"""Получить emoji типа отключения"""
|
||
emojis = {
|
||
'water': '💧',
|
||
'electricity': '⚡',
|
||
'heating': '🔥',
|
||
'gas': '🔶',
|
||
'elevator': '🛗',
|
||
'general': '📋'
|
||
}
|
||
return emojis.get(self.schedule_type, '📋')
|
||
|
||
|
||
class Ad(Base):
|
||
"""Объявления жильцов (куплю, продам, отдам, найду)"""
|
||
__tablename__ = 'ads'
|
||
|
||
ad_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
category = Column(String(50), nullable=False) # sell, buy, give, lost, found, service
|
||
title = Column(String(255), nullable=False)
|
||
description = Column(Text, nullable=True)
|
||
price = Column(Integer, nullable=True) # Цена в рублях
|
||
phone = Column(String(50), nullable=True) # Контактный телефон
|
||
message_id = Column(BigInteger, nullable=True) # ID сообщения в чате
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
expires_at = Column(DateTime, nullable=True) # Дата истечения (через 7 дней)
|
||
is_active = Column(Boolean, default=True)
|
||
is_moderated = Column(Boolean, default=False) # Проверено админом
|
||
|
||
# Связи
|
||
user = relationship('User', backref='ads')
|
||
|
||
__table_args__ = (
|
||
Index('ix_ads_category', 'category'),
|
||
Index('ix_ads_active', 'is_active'),
|
||
Index('ix_ads_created_at', 'created_at'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Ad {self.title} by {self.user_id}>"
|
||
|
||
@property
|
||
def days_left(self) -> int:
|
||
"""Осталось дней до истечения"""
|
||
if self.expires_at:
|
||
delta = self.expires_at - datetime.utcnow()
|
||
return max(0, delta.days)
|
||
return 7
|
||
|
||
def get_category_emoji(self) -> str:
|
||
"""Получить emoji категории"""
|
||
emojis = {
|
||
'sell': '💰',
|
||
'buy': '🛒',
|
||
'give': '🎁',
|
||
'lost': '😿',
|
||
'found': '😺',
|
||
'service': '🔧'
|
||
}
|
||
return emojis.get(self.category, '📋')
|
||
|
||
def get_category_name(self) -> str:
|
||
"""Получить название категории"""
|
||
names = {
|
||
'sell': 'Продам',
|
||
'buy': 'Куплю',
|
||
'give': 'Отдам',
|
||
'lost': 'Потеряно',
|
||
'found': 'Нашлось',
|
||
'service': 'Услуга'
|
||
}
|
||
return names.get(self.category, 'Общее')
|
||
|
||
|
||
class PaymentReminder(Base):
|
||
"""Напоминания о платежах ЖКХ"""
|
||
__tablename__ = 'payment_reminders'
|
||
|
||
reminder_id = Column(Integer, primary_key=True, autoincrement=True)
|
||
title = Column(String(255), nullable=False) # Название платежа
|
||
amount = Column(Integer, nullable=True) # Сумма в рублях
|
||
due_date = Column(DateTime, nullable=False) # Дата оплаты (число месяца)
|
||
description = Column(Text, nullable=True) # Описание
|
||
payment_link = Column(String(500), nullable=True) # Ссылка на оплату
|
||
is_recurring = Column(Boolean, default=True) # Ежемесячный платёж
|
||
created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
is_active = Column(Boolean, default=True)
|
||
last_reminder_sent = Column(DateTime, nullable=True) # Последнее напоминание
|
||
|
||
# Связи
|
||
creator = relationship('User', backref='payment_reminders')
|
||
|
||
__table_args__ = (
|
||
Index('ix_payment_reminders_due_date', 'due_date'),
|
||
Index('ix_payment_reminders_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<PaymentReminder {self.title} (due: {self.due_date})>"
|
||
|
||
def get_days_until_due(self, current_day: int = None) -> int:
|
||
"""Сколько дней до срока оплаты"""
|
||
from datetime import datetime
|
||
if current_day is None:
|
||
current_day = datetime.utcnow().day
|
||
|
||
due_day = self.due_date.day if hasattr(self.due_date, 'day') else self.due_date
|
||
|
||
if due_day >= current_day:
|
||
return due_day - current_day
|
||
else:
|
||
return (30 - current_day) + due_day
|
||
|
||
|
||
class ProfileHistory(Base):
|
||
"""История изменений профиля пользователя"""
|
||
__tablename__ = 'profile_history'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
field_name = Column(String(50), nullable=False) # username, first_name, last_name, apartment
|
||
old_value = Column(Text, nullable=True) # Старое значение
|
||
new_value = Column(Text, nullable=True) # Новое значение
|
||
changed_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
user = relationship('User', backref='profile_history')
|
||
|
||
__table_args__ = (
|
||
Index('ix_profile_history_user', 'user_id'),
|
||
Index('ix_profile_history_field', 'field_name'),
|
||
Index('ix_profile_history_time', 'changed_at'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<ProfileHistory {self.user_id}: {self.field_name}>"
|
||
|
||
|
||
class Achievement(Base):
|
||
"""Достижения пользователей"""
|
||
__tablename__ = 'achievements'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
achievement_type = Column(String(50), nullable=False) # Тип достижения
|
||
earned_at = Column(DateTime, default=datetime.utcnow) # Когда получено
|
||
achievement_data = Column(Text, nullable=True) # JSON с доп. данными
|
||
|
||
# Связи
|
||
user = relationship('User', backref='achievements_list')
|
||
|
||
__table_args__ = (
|
||
Index('ix_achievements_user', 'user_id'),
|
||
Index('ix_achievements_type', 'achievement_type'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Achievement {self.user_id}: {self.achievement_type}>"
|
||
|
||
|
||
class Thank(Base):
|
||
"""Благодарности пользователей"""
|
||
__tablename__ = 'thanks'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
from_user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) # Кто поблагодарил
|
||
to_user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False) # Кого поблагодарили
|
||
message = Column(Text, nullable=True) # Текст благодарности
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
from_user = relationship('User', foreign_keys=[from_user_id], backref='thanks_given')
|
||
to_user = relationship('User', foreign_keys=[to_user_id], backref='thanks_received')
|
||
|
||
__table_args__ = (
|
||
Index('ix_thanks_from', 'from_user_id'),
|
||
Index('ix_thanks_to', 'to_user_id'),
|
||
Index('ix_thanks_created', 'created_at'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Thank {self.from_user_id} → {self.to_user_id}>"
|
||
|
||
|
||
class Event(Base):
|
||
"""События дома (собрания, субботники, праздники)"""
|
||
__tablename__ = 'events'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
title = Column(String(255), nullable=False) # Название события
|
||
description = Column(Text, nullable=True) # Описание
|
||
event_type = Column(String(50), default='general') # Тип: meeting, subbotnik, holiday
|
||
event_date = Column(DateTime, nullable=False) # Дата и время события
|
||
location = Column(String(255), nullable=True) # Место проведения
|
||
created_by = Column(BigInteger, ForeignKey('users.user_id'), nullable=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
is_active = Column(Boolean, default=True)
|
||
reminder_sent = Column(Boolean, default=False) # Напоминание отправлено
|
||
|
||
# Связи
|
||
creator = relationship('User', backref='created_events')
|
||
|
||
__table_args__ = (
|
||
Index('ix_events_date', 'event_date'),
|
||
Index('ix_events_type', 'event_type'),
|
||
Index('ix_events_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Event {self.title} ({self.event_date})>"
|
||
|
||
|
||
class EventParticipant(Base):
|
||
"""Участники событий"""
|
||
__tablename__ = 'event_participants'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
event_id = Column(Integer, ForeignKey('events.id'), nullable=False)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
joined_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
event = relationship('Event', backref='participants_list')
|
||
user = relationship('User', backref='events_participating')
|
||
|
||
__table_args__ = (
|
||
Index('ix_event_participants_event', 'event_id'),
|
||
Index('ix_event_participants_user', 'user_id'),
|
||
Index('ix_event_participants_unique', 'event_id', 'user_id', unique=True),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<EventParticipant event={self.event_id} user={self.user_id}>"
|
||
|
||
|
||
class InitiativeGroup(Base):
|
||
"""Участники инициативной группы"""
|
||
__tablename__ = 'initiative_group'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False, unique=True)
|
||
role = Column(String(50), default='member') # admin, member, coordinator
|
||
joined_at = Column(DateTime, default=datetime.utcnow)
|
||
is_active = Column(Boolean, default=True)
|
||
notes = Column(Text, nullable=True) # Заметки о роли
|
||
|
||
# Связи
|
||
user = relationship('User', backref='initiative_membership')
|
||
|
||
__table_args__ = (
|
||
Index('ix_initiative_group_user', 'user_id'),
|
||
Index('ix_initiative_group_active', 'is_active'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<InitiativeGroup user={self.user_id} role={self.role}>"
|
||
|
||
|
||
class ScheduledPost(Base):
|
||
"""Запланированные посты для отложенленной публикации"""
|
||
__tablename__ = 'scheduled_posts'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
text = Column(Text, nullable=False) # Текст поста
|
||
photo_file_id = Column(String(255), nullable=True) # ID фото в Telegram
|
||
topic_id = Column(BigInteger, nullable=True) # ID темы форума (message_thread_id)
|
||
topic_name = Column(String(50), nullable=True) # Название темы ('general', 'memes'...)
|
||
recipients = Column(String(50), default='chat_only') # Получатели: 'chat_only', 'all_verified', 'all_and_chat'
|
||
scheduled_time = Column(DateTime, nullable=False) # Когда отправить
|
||
status = Column(String(20), default='pending') # 'pending', 'sent', 'failed', 'cancelled'
|
||
created_by = Column(BigInteger, nullable=True) # ID админа создавшего пост
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
sent_at = Column(DateTime, nullable=True) # Когда фактически отправлено
|
||
error_message = Column(Text, nullable=True) # Текст ошибки если failed
|
||
message_id = Column(BigInteger, nullable=True) # ID отправленного сообщения в Telegram
|
||
|
||
__table_args__ = (
|
||
Index('ix_scheduled_posts_time', 'scheduled_time'),
|
||
Index('ix_scheduled_posts_status', 'status'),
|
||
Index('ix_scheduled_posts_topic', 'topic_name'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<ScheduledPost {self.id} at {self.scheduled_time} status={self.status}>"
|
||
|
||
@property
|
||
def is_due(self) -> bool:
|
||
"""Пора ли отправлять пост"""
|
||
from datetime import datetime
|
||
return datetime.utcnow() >= self.scheduled_time and self.status == 'pending'
|
||
|
||
def get_topic_emoji(self) -> str:
|
||
"""Получить emoji темы"""
|
||
emojis = {
|
||
'general': '💬',
|
||
'ads': '📢',
|
||
'memes': '😂',
|
||
'meetings': '🏛️',
|
||
'kgm': '🗑️',
|
||
'lostfound': '🔍',
|
||
'auto': '🚗',
|
||
'phones': '📞',
|
||
'links': '🔗',
|
||
'uo': '📝',
|
||
}
|
||
return emojis.get(self.topic_name, '📝')
|
||
|
||
def get_status_emoji(self) -> str:
|
||
"""Получить emoji статуса"""
|
||
emojis = {
|
||
'pending': '⏳',
|
||
'sent': '✅',
|
||
'failed': '❌',
|
||
'cancelled': '🚫',
|
||
}
|
||
return emojis.get(self.status, '❓')
|
||
|
||
|
||
class Broadcast(Base):
|
||
"""Умные рассылки с отслеживанием прочтения"""
|
||
__tablename__ = 'broadcasts'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
message_id = Column(BigInteger, nullable=True) # ID сообщения в Telegram (в чате)
|
||
chat_id = Column(BigInteger, nullable=True) # ID чата
|
||
text = Column(Text, nullable=False) # Текст рассылки
|
||
photo_file_id = Column(String(255), nullable=True) # ID фото
|
||
sent_at = Column(DateTime, nullable=True) # Когда отправлено
|
||
sent_by = Column(BigInteger, nullable=True) # ID админа
|
||
total_sent = Column(Integer, default=0) # Сколько получателей
|
||
has_read_button = Column(Boolean, default=True) # Показывать кнопку "Прочитал"
|
||
is_reminder_sent = Column(Boolean, default=False) # Было ли напоминание непрочитавшим
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
broadcast_type = Column(String(50), default='manual') # 'manual', 'digest', 'emergency'
|
||
|
||
__table_args__ = (
|
||
Index('ix_broadcasts_sent_at', 'sent_at'),
|
||
Index('ix_broadcasts_type', 'broadcast_type'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Broadcast {self.id} at {self.sent_at}>"
|
||
|
||
@property
|
||
def read_count(self) -> int:
|
||
"""Сколько прочитали (запрос к БД)"""
|
||
# Это свойство вычисляется через relationship
|
||
return 0
|
||
|
||
@property
|
||
def unread_count(self) -> int:
|
||
"""Сколько НЕ прочитали"""
|
||
return self.total_sent - self.read_count
|
||
|
||
|
||
class BroadcastRead(Base):
|
||
"""Кто прочитал рассылку"""
|
||
__tablename__ = 'broadcast_reads'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
broadcast_id = Column(BigInteger, ForeignKey('broadcasts.id'), nullable=False)
|
||
user_id = Column(BigInteger, ForeignKey('users.user_id'), nullable=False)
|
||
read_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
# Связи
|
||
broadcast = relationship('Broadcast', backref='reads')
|
||
user = relationship('User', backref='broadcast_reads')
|
||
|
||
__table_args__ = (
|
||
Index('ix_broadcast_read_broadcast', 'broadcast_id'),
|
||
Index('ix_broadcast_read_user', 'user_id'),
|
||
Index('ix_broadcast_read_unique', 'broadcast_id', 'user_id', unique=True),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<BroadcastRead broadcast={self.broadcast_id} user={self.user_id}>"
|
||
|
||
|
||
class Digest(Base):
|
||
"""Еженедельные дайджесты"""
|
||
__tablename__ = 'digests'
|
||
|
||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||
period_start = Column(DateTime, nullable=False) # Начало периода
|
||
period_end = Column(DateTime, nullable=False) # Конец периода
|
||
week_number = Column(Integer, nullable=False) # Номер недели
|
||
year = Column(Integer, nullable=False) # Год
|
||
content = Column(Text, nullable=True) # JSON с данными дайджеста
|
||
digest_text = Column(Text, nullable=True) # Сформированный текст
|
||
status = Column(String(50), default='draft') # 'draft', 'pending_approval', 'approved', 'rejected', 'sent'
|
||
message_id = Column(BigInteger, nullable=True) # ID сообщения в Telegram
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
approved_at = Column(DateTime, nullable=True)
|
||
approved_by = Column(BigInteger, nullable=True) # Admin ID
|
||
sent_at = Column(DateTime, nullable=True)
|
||
rejected_by = Column(BigInteger, nullable=True)
|
||
rejection_reason = Column(Text, nullable=True) # Причина отклонения
|
||
|
||
__table_args__ = (
|
||
Index('ix_digests_week', 'year', 'week_number'),
|
||
Index('ix_digests_status', 'status'),
|
||
Index('ix_digests_period', 'period_start', 'period_end'),
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f"<Digest #{self.week_number}/{self.year} status={self.status}>"
|
||
|
||
@property
|
||
def status_emoji(self) -> str:
|
||
emojis = {
|
||
'draft': '📝',
|
||
'pending_approval': '⏳',
|
||
'approved': '✅',
|
||
'rejected': '❌',
|
||
'sent': '📨',
|
||
}
|
||
return emojis.get(self.status, '❓')
|
||
|
||
@property
|
||
def period_label(self) -> str:
|
||
"""Красивая метка периода"""
|
||
return f"{self.period_start.strftime('%d.%m')} - {self.period_end.strftime('%d.%m.%Y')}"
|