"""Title library use cases.""" from __future__ import annotations import uuid from typing import List, Optional from packages.adapters.sqlalchemy_impl.title_library_repository import SQLAlchemyTitleLibraryRepository from packages.application.title_library.commands import ( CreateTitleLibraryCommand, IncrementTitleUsageCommand, PickTitleCommand, UpdateTitleLibraryCommand, ) from packages.domain.quota import QuotaDimension, quota_checker from packages.domain.title_library import TitleLibraryItem class ListTitleLibraryUseCase: def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute( self, user_id: str, *, category: Optional[str] = None, skip: int = 0, limit: int = 50, ) -> List[TitleLibraryItem]: return self.repository.list_by_user(user_id, category=category, skip=skip, limit=limit) class GetTitleLibraryUseCase: def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, title_id: str, user_id: str) -> Optional[TitleLibraryItem]: return self.repository.get(title_id, user_id) class CreateTitleLibraryUseCase: def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, command: CreateTitleLibraryCommand, plan_name: str = "free") -> TitleLibraryItem: # Quota check current_count = self.repository.count_by_user(command.user_id) result = quota_checker.check(plan_name, QuotaDimension.MAX_TITLES.value, current_count) if not result.allowed: raise QuotaExceededError( dimension=QuotaDimension.MAX_TITLES.value, limit=result.limit, used=result.used, ) item = TitleLibraryItem( id=uuid.uuid4().hex, user_id=command.user_id, name=command.name, text=command.text, category=command.category, description=command.description, tags=command.tags, metadata_=command.metadata_, ) return self.repository.create(item) class UpdateTitleLibraryUseCase: def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, command: UpdateTitleLibraryCommand) -> TitleLibraryItem: existing = self.repository.get(command.title_id, command.user_id) if existing is None: raise NotFoundError(f"Title {command.title_id} not found") if command.name is not None: existing.name = command.name if command.text is not None: existing.text = command.text if command.category is not None: existing.category = command.category if command.description is not None: existing.description = command.description if command.tags is not None: existing.tags = command.tags if command.is_active is not None: existing.is_active = command.is_active if command.metadata_ is not None: existing.metadata_ = command.metadata_ return self.repository.update(existing) class DeleteTitleLibraryUseCase: def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, title_id: str, user_id: str) -> bool: return self.repository.delete(title_id, user_id) class IncrementTitleUsageUseCase: """递增标题使用次数。用于生成视频成功后,更新标题的使用统计。""" def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, command: IncrementTitleUsageCommand) -> bool: if command.increment <= 0: return False return self.repository.increment_usage_count( command.title_id, command.user_id, increment=command.increment, ) class PickTitleUseCase: """智能选择一个标题。 策略: 1. 可选按 category 过滤 2. 排除指定的 title_ids(如本轮已用过的) 3. 按使用次数升序,取最少的前 5 个 4. 从中随机选一个,增加多样性 5. 无可用标题时返回 None """ _CANDIDATE_POOL_SIZE = 5 def __init__(self, repository: SQLAlchemyTitleLibraryRepository) -> None: self.repository = repository def execute(self, command: PickTitleCommand) -> TitleLibraryItem | None: import random # 取该用户所有活跃标题(或指定分类) all_titles = self.repository.list_by_user( command.user_id, category=command.category, is_active=True, skip=0, limit=500, # 取足够多的候选 ) if not all_titles: return None # 排除已使用/指定排除的 exclude_set = set(command.exclude_ids or []) candidates = [t for t in all_titles if t.id not in exclude_set] if not candidates: # 排除后没了,就从全部里选 candidates = all_titles # 按使用次数升序,取最少的前 N 个 candidates.sort(key=lambda t: t.usage_count) pool = candidates[: self._CANDIDATE_POOL_SIZE] # 随机选一个 return random.choice(pool) class QuotaExceededError(Exception): def __init__(self, dimension: str, limit: float, used: float) -> None: self.dimension = dimension self.limit = limit self.used = used super().__init__(f"Quota exceeded for {dimension}: {used}/{limit}") class NotFoundError(Exception): pass