подписки сразу отслеживаются

This commit is contained in:
Artem Tsyrulnikov
2026-01-28 11:42:09 +03:00
parent 2d35588a85
commit 0b25566ffb
9 changed files with 205 additions and 47 deletions

View File

@@ -0,0 +1,149 @@
# ruff: noqa
# mypy: ignore-errors
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
-- 1. Drop old constraints
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "uid_subscriptio_placeme_aba67c";
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_f35fb6bb";
-- 2. Add new column for placement_id
ALTER TABLE "subscription" ADD COLUMN "placement_id" UUID;
-- 3. Populate placement_id from placement_post table
UPDATE "subscription" s
SET placement_id = pp.placement_id
FROM "placement_post" pp
WHERE s.placement_post_id = pp.id;
-- 4. Make NOT NULL (all subscriptions should have a placement via placement_post)
ALTER TABLE "subscription" ALTER COLUMN "placement_id" SET NOT NULL;
-- 5. Add FK constraint to placement table
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_0b5d8f15"
FOREIGN KEY ("placement_id") REFERENCES "placement" ("id") ON DELETE CASCADE;
-- 6. Create unique index
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_75b40b"
ON "subscription" ("placement_id", "telegram_user_id");
-- 7. Drop old column
ALTER TABLE "subscription" DROP COLUMN "placement_post_id";
"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
-- 1. Drop new constraints
DROP INDEX IF EXISTS "uid_subscriptio_placeme_75b40b";
ALTER TABLE "subscription" DROP CONSTRAINT IF EXISTS "fk_subscrip_placemen_0b5d8f15";
-- 2. Add back placement_post_id column
ALTER TABLE "subscription" ADD COLUMN "placement_post_id" UUID;
-- 3. Populate from placement_post (best effort - takes first placement_post for the placement)
UPDATE "subscription" s
SET placement_post_id = pp.id
FROM "placement_post" pp
WHERE s.placement_id = pp.placement_id;
-- 4. Make NOT NULL (may fail if no matching placement_post exists)
ALTER TABLE "subscription" ALTER COLUMN "placement_post_id" SET NOT NULL;
-- 5. Add FK constraint back to placement_post
ALTER TABLE "subscription" ADD CONSTRAINT "fk_subscrip_placemen_f35fb6bb"
FOREIGN KEY ("placement_post_id") REFERENCES "placement_post" ("id") ON DELETE CASCADE;
-- 6. Restore unique index
CREATE UNIQUE INDEX IF NOT EXISTS "uid_subscriptio_placeme_aba67c"
ON "subscription" ("placement_post_id", "telegram_user_id");
-- 7. Drop new column
ALTER TABLE "subscription" DROP COLUMN "placement_id";
"""
MODELS_STATE = (
"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"
"al"
)

View File

@@ -21,7 +21,7 @@ async def get_views_history(
to_date: datetime.datetime | None = None, to_date: datetime.datetime | None = None,
) -> Page[dto.PostViewsHistoryOutput]: ) -> Page[dto.PostViewsHistoryOutput]:
input_data = dto.GetViewsHistoryInput( input_data = dto.GetViewsHistoryInput(
placement_post_id=placement_id, placement_id=placement_id,
user_id=current_user.user_id, user_id=current_user.user_id,
workspace_id=workspace_id, workspace_id=workspace_id,
from_date=from_date, from_date=from_date,

View File

@@ -7,7 +7,7 @@ from tortoise import fields
from .base import TimestampedModel from .base import TimestampedModel
if TYPE_CHECKING: if TYPE_CHECKING:
from .placement_post import PlacementPost from .placement import Placement
from .telegram_user import TelegramUser from .telegram_user import TelegramUser
@@ -21,17 +21,17 @@ class Subscription(TimestampedModel):
status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE) status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE)
unsubscribed_at = fields.DatetimeField(null=True) unsubscribed_at = fields.DatetimeField(null=True)
placement_post: fields.ForeignKeyRelation['PlacementPost'] = fields.ForeignKeyField( placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
'models.PlacementPost', related_name='subscriptions', on_delete=fields.CASCADE, index=True 'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
) )
telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField( telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField(
'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True 'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True
) )
if TYPE_CHECKING: if TYPE_CHECKING:
placement_post_id: UUID placement_id: UUID
telegram_user_id: UUID telegram_user_id: UUID
class Meta: class Meta:
table = 'subscription' table = 'subscription'
unique_together = (('placement_post_id', 'telegram_user_id'),) unique_together = (('placement_id', 'telegram_user_id'),)

View File

@@ -15,9 +15,9 @@ class PostViewsHistoryOutput(pydantic.BaseModel):
class GetViewsHistoryInput(pydantic.BaseModel): class GetViewsHistoryInput(pydantic.BaseModel):
"""Получить историю просмотров для публикации.""" """Получить историю просмотров для placement."""
placement_post_id: uuid.UUID placement_id: uuid.UUID
user_id: uuid.UUID user_id: uuid.UUID
workspace_id: uuid.UUID workspace_id: uuid.UUID
from_date: datetime.datetime | None = None # Фильтр: с какой даты from_date: datetime.datetime | None = None # Фильтр: с какой даты

View File

@@ -74,8 +74,8 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
placement_id=placement.id, placement_id=placement.id,
include_archived=True, include_archived=True,
) )
placement_post_ids = [post.id for post in placement_posts] # Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post] post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post]
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
@@ -89,7 +89,7 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
views_count = views_map.get(placement_post.post.id, (None,))[0] views_count = views_map.get(placement_post.post.id, (None,))[0]
placement_post_output = _build_placement_post_output( placement_post_output = _build_placement_post_output(
placement_post, placement_post,
subscriptions_counts.get(placement_post.id, 0), subscriptions_count,
views_count, views_count,
) )
if placement_post_output is not None: if placement_post_output is not None:

View File

@@ -34,8 +34,8 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
placement_ids, placement_ids,
include_archived=True, include_archived=True,
) )
placement_post_ids = [post.id for post in placement_posts] # Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) subscriptions_counts = await self.database.count_subscriptions_by_placement_batch(placement_ids)
post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post] post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post]
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
@@ -46,7 +46,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
views_count = views_map.get(placement_post.post.id, (None,))[0] views_count = views_map.get(placement_post.post.id, (None,))[0]
placement_post_output = _build_placement_post_output( placement_post_output = _build_placement_post_output(
placement_post, placement_post,
subscriptions_counts.get(placement_post.id, 0), subscriptions_counts.get(placement_post.placement_id, 0),
views_count, views_count,
) )
if placement_post_output is None: if placement_post_output is None:

View File

@@ -17,13 +17,9 @@ async def handle_subscription(
first_name: str | None = None, first_name: str | None = None,
last_name: str | None = None, last_name: str | None = None,
) -> None: ) -> None:
placement_post = await self.database.get_placement_post_by_invite_link(invite_link) placement = await self.database.get_placement_by_invite_link(invite_link)
if not placement_post: if not placement:
log.warning('PlacementPost not found for invite_link: %s', invite_link) log.warning('Placement not found for invite_link: %s', invite_link)
return
if not placement_post.placement:
log.error('PlacementPost %s has no placement', placement_post.id)
return return
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id) subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
@@ -42,27 +38,27 @@ async def handle_subscription(
await self.database.update_telegram_user(subscriber) await self.database.update_telegram_user(subscriber)
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project( active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
subscriber.id, placement_post.placement.project_id subscriber.id, placement.project_id
) )
if active_subscription: if active_subscription:
# Пользователь уже подписан на канал через другую публикацию # Пользователь уже подписан на канал через другой placement
# Это не должно случиться (Telegram не даст подписаться дважды), # Это не должно случиться (Telegram не даст подписаться дважды),
# но если случилось - логируем и игнорируем # но если случилось - логируем и игнорируем
log.warning( log.warning(
'User %s (telegram_id: %s) already has active subscription to channel %s via placement_post %s, ' 'User %s (telegram_id: %s) already has active subscription to channel %s via placement %s, '
'ignoring new subscription attempt via placement_post %s', 'ignoring new subscription attempt via placement %s',
subscriber.id, subscriber.id,
user_telegram_id, user_telegram_id,
placement_post.placement.project_id, placement.project_id,
active_subscription.placement_post_id, active_subscription.placement_id,
placement_post.id, placement.id,
) )
return return
# Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации) # Проверяем, была ли раньше подписка через ЭТОТ placement (для реактивации)
existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post( existing_sub = await self.database.get_subscription_by_subscriber_and_placement(
subscriber.id, placement_post.id subscriber.id, placement.id
) )
if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED: if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED:
@@ -71,24 +67,24 @@ async def handle_subscription(
await self.database.update_subscription(existing_sub) await self.database.update_subscription(existing_sub)
log.info( log.info(
'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement_post %s', 'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement %s',
subscriber.id, subscriber.id,
user_telegram_id, user_telegram_id,
placement_post.id, placement.id,
) )
return return
subscription = domain.Subscription( subscription = domain.Subscription(
placement_post_id=placement_post.id, placement_id=placement.id,
telegram_user_id=subscriber.id, telegram_user_id=subscriber.id,
invite_link=invite_link, invite_link=invite_link,
) )
await self.database.create_subscription(subscription) await self.database.create_subscription(subscription)
log.info( log.info(
'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement_post %s (invite_link: %s)', 'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement %s (invite_link: %s)',
subscriber.id, subscriber.id,
user_telegram_id, user_telegram_id,
placement_post.id, placement.id,
invite_link, invite_link,
) )

View File

@@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_
await self.database.update_subscription(subscription) await self.database.update_subscription(subscription)
log.info( log.info(
'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement_post %s', 'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement %s',
subscriber.id, subscriber.id,
user_telegram_id, user_telegram_id,
subscription.placement_post_id, subscription.placement_id,
) )

View File

@@ -14,18 +14,31 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
) )
placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id) placement = await self.database.get_placement(input.workspace_id, input.placement_id)
if not placement_post: if not placement:
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id) log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
raise domain.PlacementPostNotFound(input.placement_post_id) raise domain.PlacementNotFound(input.placement_id)
if not placement_post.placement: context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
log.error('PlacementPost %s has no placement', placement_post.id)
raise domain.PlacementNotFound(placement_post.placement_id)
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement_post.placement.project_id) # Получаем PlacementPost для этого Placement (для получения связанного Post)
placement_posts = await self.database.get_workspace_placement_posts(
input.workspace_id,
placement_id=placement.id,
include_archived=True,
)
if not placement_post.post: if not placement_posts:
return []
# Берём первый PlacementPost с постом
placement_post = None
for pp in placement_posts:
if pp.post:
placement_post = pp
break
if not placement_post or not placement_post.post:
return [] return []
histories = await self.database.get_views_history( histories = await self.database.get_views_history(