from __future__ import annotations from fastapi import HTTPException from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.enums.private_portrait import ( PRIVATE_PORTRAIT_DEFAULT_ASSET_LIMIT, PRIVATE_PORTRAIT_ENABLED_ASSET_TYPES, PrivatePortraitAssetStatus, PrivatePortraitEventSource, PrivatePortraitEventStatus, PrivatePortraitEventType, PrivatePortraitLibraryType, ) from app.models.private_portrait import PrivatePortraitAsset from app.models.user import User from app.schemas.private_portrait import PrivatePortraitConfigOut from app.services.operation_log_service import log_operation_event DOMAIN = "private_portrait" _COUNTING_STATUSES = { PrivatePortraitAssetStatus.CREATING.value, PrivatePortraitAssetStatus.PROCESSING.value, PrivatePortraitAssetStatus.ACTIVE.value, } _COUNTING_LIBRARY_TYPES = { PrivatePortraitLibraryType.REAL_PERSON.value, PrivatePortraitLibraryType.AIGC_VIRTUAL.value, } async def get_user_or_404(db: AsyncSession, *, user_id: str, for_update: bool = False) -> User: stmt = select(User).where(User.id == user_id).limit(1) if for_update: stmt = stmt.with_for_update() user = (await db.execute(stmt)).scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") return user def get_user_asset_limit_value(user: User) -> int: return int(getattr(user, "private_portrait_asset_limit", PRIVATE_PORTRAIT_DEFAULT_ASSET_LIMIT) or 0) async def count_user_counting_assets(db: AsyncSession, *, user_id: str) -> int: total = ( await db.execute( select(func.count(PrivatePortraitAsset.id)).where( PrivatePortraitAsset.user_id == user_id, PrivatePortraitAsset.library_type.in_(_COUNTING_LIBRARY_TYPES), PrivatePortraitAsset.asset_type.in_(PRIVATE_PORTRAIT_ENABLED_ASSET_TYPES), PrivatePortraitAsset.deleted_at.is_(None), PrivatePortraitAsset.status.in_(_COUNTING_STATUSES), ) ) ).scalar_one() return int(total or 0) async def get_user_private_portrait_config(db: AsyncSession, *, user_id: str) -> PrivatePortraitConfigOut: user = await get_user_or_404(db, user_id=user_id) limit = get_user_asset_limit_value(user) used = await count_user_counting_assets(db, user_id=user_id) remaining = max(0, limit - used) if limit > 0 else 0 return PrivatePortraitConfigOut( enabled=limit > 0, asset_limit=limit, used_asset_count=used, remaining_asset_count=remaining, image_limit=limit, used_image_count=used, remaining_image_count=remaining, ) async def set_user_private_portrait_limit(db: AsyncSession, *, user_id: str, limit: int) -> User: user = await get_user_or_404(db, user_id=user_id) user.private_portrait_asset_limit = max(0, int(limit)) await db.flush() return user async def ensure_private_portrait_asset_quota_available( db: AsyncSession, *, user_id: str, project_id: str | None = None, library_type: str | None = None, asset_type: str | None = None, ) -> tuple[int, int]: user = await get_user_or_404(db, user_id=user_id, for_update=True) limit = get_user_asset_limit_value(user) log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.QUOTA_CHECK_START.value, event_status=PrivatePortraitEventStatus.PENDING.value, source=PrivatePortraitEventSource.SERVICE.value, user_id=user_id, project_id=project_id, detail={"asset_limit": limit, "library_type": library_type, "asset_type": asset_type}, ) if limit <= 0: log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.QUOTA_CHECK_DENY.value, event_status=PrivatePortraitEventStatus.FAILED.value, source=PrivatePortraitEventSource.SERVICE.value, user_id=user_id, project_id=project_id, detail={"asset_limit": limit, "library_type": library_type, "asset_type": asset_type, "reason": "disabled"}, message="用户私域人像素材库未启用", ) raise HTTPException(status_code=403, detail="私域人像素材库未启用") used = await count_user_counting_assets(db, user_id=user_id) if used >= limit: log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.QUOTA_CHECK_DENY.value, event_status=PrivatePortraitEventStatus.FAILED.value, source=PrivatePortraitEventSource.SERVICE.value, user_id=user_id, project_id=project_id, detail={"asset_limit": limit, "used_asset_count": used, "library_type": library_type, "asset_type": asset_type, "reason": "max_limit"}, message="用户私域人像素材总量已达上限", ) raise HTTPException(status_code=400, detail=f"你的私域人像素材库最多可上传 {limit} 个素材,请删除已有素材后再上传") log_operation_event( domain=DOMAIN, event_type=PrivatePortraitEventType.QUOTA_CHECK_PASS.value, event_status=PrivatePortraitEventStatus.SUCCESS.value, source=PrivatePortraitEventSource.SERVICE.value, user_id=user_id, project_id=project_id, detail={"asset_limit": limit, "used_asset_count": used, "remaining_asset_count": max(0, limit - used), "library_type": library_type, "asset_type": asset_type}, ) return limit, used