feat(admin): add admin panel and reminder system

This commit is contained in:
2026-07-30 19:46:16 +05:00
parent ecc006287b
commit e79624a13a
17 changed files with 582 additions and 148 deletions
+2
View File
@@ -1,5 +1,7 @@
from .db_pending import PendingModel, PendingRepository
from .db_settings import SettingsModel, SettingsRepository
from .db_users import UserModel, UsersRepository
ModelSettings = SettingsModel
ModelUsers = UserModel
ModelPending = PendingModel
+53
View File
@@ -0,0 +1,53 @@
# - *- coding: utf- 8 - *-
from sqlalchemy import BigInteger, Integer
from sqlalchemy import update as sqlalchemy_update
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.orm import Mapped, mapped_column
from tgbot.database.core import Base, session_scope
from tgbot.database.repository import BaseRepository
from tgbot.utils.const_functions import get_unix
# Модель для хранения пользователей, которым бот отправил приветствие
# и которые ещё не вышли из очереди напоминаний
class PendingModel(Base):
__tablename__ = "storage_pending"
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
joined_at: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
last_remind_at: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
ModelBase = PendingModel
BaseModel = PendingModel
# Репозиторий для управления очередью напоминаний
class PendingRepository(BaseRepository[PendingModel]):
def __init__(self):
super().__init__()
self.table_model = PendingModel
self.storage_name = PendingModel.__tablename__
# Добавляем пользователя в очередь, если его ещё нет
async def add(self, user_id: int) -> None:
statement = insert(PendingModel).values(
user_id=user_id,
joined_at=get_unix(),
last_remind_at=0,
)
statement = statement.on_conflict_do_nothing(index_elements=[PendingModel.user_id])
async with session_scope() as session:
await session.execute(statement)
# Обновляем время последнего напоминания
async def update_remind_at(self, user_id: int) -> None:
async with session_scope() as session:
await session.execute(
sqlalchemy_update(PendingModel)
.where(PendingModel.user_id == user_id)
.values(last_remind_at=get_unix())
)
+7
View File
@@ -18,6 +18,10 @@ class SettingsModel(Base):
status_notify: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
misc_message: Mapped[str] = mapped_column(String(4096), nullable=True, default=None)
misc_message_media: Mapped[str] = mapped_column(
String(255), nullable=True, default=None
)
misc_finl_text: Mapped[str] = mapped_column(
String(255), nullable=True, default=None
)
@@ -25,6 +29,9 @@ class SettingsModel(Base):
String(255), nullable=True, default=None
)
# Интервал напоминаний в часах: 0 = выключены, допустимые значения: 5, 12, 24
remind_interval: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
ModelBase = SettingsModel
BaseModel = SettingsModel
+29 -3
View File
@@ -87,20 +87,46 @@ class BaseRepository(Generic[ModelTranslator]):
)
# Готовим подключение к БД. Сами миграции запускаются отдельно через migrate.py.
# Готовим подключение к БД: создаём таблицы и накатываем недостающие колонки.
async def prepare_database() -> None:
database_path.parent.mkdir(parents=True, exist_ok=True)
# Импортируем модели, чтобы репозитории работали с уже загруженными таблицами
# Импортируем модели, чтобы Base.metadata знала обо всех таблицах
import tgbot.database # noqa: F401
from tgbot.database.core import Base, engine
from tgbot.database.db_settings import SettingsRepository
# Создаём все таблицы (если их ещё нет)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Мягкая миграция: добавляем колонку remind_interval если её нет
await _migrate_settings_columns()
try:
await SettingsRepository().ensure_default()
except SQLAlchemyError as error:
raise RuntimeError(
"База данных не готова. Запусти миграции командой: python migrate.py"
"База данных не готова — проверь подключение к SQLite"
) from error
bot_logger.info("База данных готова")
async def _migrate_settings_columns() -> None:
"""Добавляет новые колонки в storage_settings для существующих БД."""
from sqlalchemy import text
from tgbot.database.core import engine
migrations = [
"ALTER TABLE storage_settings ADD COLUMN remind_interval INTEGER NOT NULL DEFAULT 0",
]
async with engine.begin() as conn:
for sql in migrations:
try:
await conn.execute(text(sql))
except Exception:
# Колонка уже существует — игнорируем
pass