Merge branch 'dev' into feature/new-tariffs

This commit is contained in:
3252a8
2026-05-07 22:23:04 +03:00
11 changed files with 746 additions and 67 deletions
+57
View File
@@ -12,6 +12,7 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
from ..models import (
User,
UserTelegramAvatar,
Subscription,
Payment,
PromoCodeActivation,
@@ -112,6 +113,45 @@ async def get_user_by_telegram_id(
return result.scalar_one_or_none()
async def get_user_telegram_avatar(
session: AsyncSession,
user_id: int,
) -> Optional[UserTelegramAvatar]:
stmt = select(UserTelegramAvatar).where(UserTelegramAvatar.user_id == user_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def upsert_user_telegram_avatar(
session: AsyncSession,
*,
user_id: int,
file_unique_id: Optional[str],
content_type: str,
image_bytes: bytes,
) -> UserTelegramAvatar:
avatar = await get_user_telegram_avatar(session, user_id)
if avatar is None:
avatar = UserTelegramAvatar(
user_id=user_id,
file_unique_id=file_unique_id,
content_type=content_type,
image_bytes=image_bytes,
size_bytes=len(image_bytes),
updated_at=datetime.now(timezone.utc),
)
session.add(avatar)
else:
avatar.file_unique_id = file_unique_id
avatar.content_type = content_type
avatar.image_bytes = image_bytes
avatar.size_bytes = len(image_bytes)
avatar.updated_at = datetime.now(timezone.utc)
await session.flush()
await session.refresh(avatar)
return avatar
async def get_user_by_panel_uuid(
session: AsyncSession, panel_uuid: str
) -> Optional[User]:
@@ -426,6 +466,22 @@ async def merge_users(
.values(user_id=target_user_id)
)
target_has_avatar = (
await session.execute(
select(UserTelegramAvatar.user_id).where(UserTelegramAvatar.user_id == target_user_id)
)
).scalar_one_or_none()
if target_has_avatar:
await session.execute(
delete(UserTelegramAvatar).where(UserTelegramAvatar.user_id == source_user_id)
)
else:
await session.execute(
update(UserTelegramAvatar)
.where(UserTelegramAvatar.user_id == source_user_id)
.values(user_id=target_user_id)
)
subscription_update_values: Dict[str, Any] = {"user_id": target_user_id}
if panel_uuid_to_keep:
subscription_update_values["panel_user_uuid"] = panel_uuid_to_keep
@@ -684,6 +740,7 @@ async def delete_user_and_relations(session: AsyncSession, user_id: int) -> bool
)
await session.execute(delete(UserBilling).where(UserBilling.user_id == user_id))
await session.execute(delete(AdAttribution).where(AdAttribution.user_id == user_id))
await session.execute(delete(UserTelegramAvatar).where(UserTelegramAvatar.user_id == user_id))
await session.delete(user)
await session.flush()
+33 -3
View File
@@ -303,7 +303,32 @@ def _migration_0010_add_email_magic_token_hash(connection: Connection) -> None:
)
def _migration_0011_add_tariffs_schema(connection: Connection) -> None:
def _migration_0011_add_user_telegram_avatars(connection: Connection) -> None:
connection.execute(
text(
"""
CREATE TABLE IF NOT EXISTS user_telegram_avatars (
user_id BIGINT PRIMARY KEY REFERENCES users(user_id),
file_unique_id VARCHAR,
content_type VARCHAR(64) NOT NULL DEFAULT 'image/jpeg',
image_bytes BYTEA NOT NULL,
size_bytes INTEGER NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"""
)
)
connection.execute(
text(
"""
CREATE INDEX IF NOT EXISTS ix_user_telegram_avatars_file_unique_id
ON user_telegram_avatars (file_unique_id)
"""
)
)
def _migration_0012_add_tariffs_schema(connection: Connection) -> None:
inspector = inspect(connection)
sub_columns: Set[str] = {col["name"] for col in inspector.get_columns("subscriptions")}
@@ -497,9 +522,14 @@ MIGRATIONS: List[Migration] = [
upgrade=_migration_0010_add_email_magic_token_hash,
),
Migration(
id="0011_add_tariffs_schema",
id="0011_add_user_telegram_avatars",
description="Cache compact Telegram profile avatars for WebApp profiles",
upgrade=_migration_0011_add_user_telegram_avatars,
),
Migration(
id="0012_add_tariffs_schema",
description="Add tariff catalog columns and traffic accounting tables",
upgrade=_migration_0011_add_tariffs_schema,
upgrade=_migration_0012_add_tariffs_schema,
),
]
+24 -1
View File
@@ -1,4 +1,4 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Float, ForeignKey, UniqueConstraint, Text, BigInteger, Index, Numeric
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Float, ForeignKey, UniqueConstraint, Text, BigInteger, Index, Numeric, LargeBinary
from sqlalchemy.orm import relationship, DeclarativeBase
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.sql import func
@@ -59,6 +59,29 @@ class User(Base):
return f"<User(user_id={self.user_id}, username='{self.username}')>"
class UserTelegramAvatar(Base):
__tablename__ = "user_telegram_avatars"
user_id = Column(
BigInteger,
ForeignKey("users.user_id"),
primary_key=True,
index=True,
)
file_unique_id = Column(String, nullable=True, index=True)
content_type = Column(String(64), nullable=False, default="image/jpeg")
image_bytes = Column(LargeBinary, nullable=False)
size_bytes = Column(Integer, nullable=False)
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
)
user = relationship("User")
class Subscription(Base):
__tablename__ = "subscriptions"
__table_args__ = (