from __future__ import annotations from datetime import datetime, timezone from typing import Iterable from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.enums.upload_resource import UploadResourceEventEnum, UploadResourceFileDeleteStatusEnum from app.models.upload_resource import UploadResource from app.models.user import User from app.schemas.upload_resource import ( UploadResourceCleanupOut, UploadResourceHistoryBatchDeleteOut, ) from app.services.upload_resource.accounting_service import release_upload_resource_capacity from app.services.upload_resource.file_cleanup_service import cleanup_upload_resource_files_after_commit from app.services.upload_resource.history_service import assert_upload_resource_history_ids_visible from app.services.upload_resource.log_service import log_upload_resource_event MAX_UPLOAD_RESOURCE_BATCH_DELETE_COUNT = 30 def normalize_upload_resource_ids(values: Iterable[str | None]) -> list[str]: ids = [str(value).strip() for value in values if str(value or "").strip()] if not ids: raise HTTPException(status_code=400, detail="resource_ids 不能为空") if len(ids) > MAX_UPLOAD_RESOURCE_BATCH_DELETE_COUNT: raise HTTPException(status_code=400, detail=f"单次最多删除 {MAX_UPLOAD_RESOURCE_BATCH_DELETE_COUNT} 条上传素材") if len(ids) != len(set(ids)): raise HTTPException(status_code=400, detail="resource_ids 不允许重复") return ids def _cleanup_out(stats: dict[str, int] | None) -> UploadResourceCleanupOut: stats = stats or {} return UploadResourceCleanupOut( matched=int(stats.get("matched", 0) or 0), deleted=int(stats.get("deleted", 0) or 0), missing=int(stats.get("missing", 0) or 0), failed=int(stats.get("failed", 0) or 0), legacy_deleted=int(stats.get("legacy_deleted", 0) or 0), legacy_missing=int(stats.get("legacy_missing", 0) or 0), legacy_failed=int(stats.get("legacy_failed", 0) or 0), ) async def mark_upload_resource_history_deleted( db: AsyncSession, *, current_user: User, resource_ids: Iterable[str | None], ) -> UploadResourceHistoryBatchDeleteOut: """主事务内软删上传历史素材并释放容量,不删除真实文件。""" ids = normalize_upload_resource_ids(resource_ids) user_id = str(current_user.id) log_upload_resource_event( event_type=UploadResourceEventEnum.DELETE_BATCH_START.value, user_id=user_id, detail={"requested_ids": ids, "requested_count": len(ids)}, ) resources = await assert_upload_resource_history_ids_visible(db, user_id=user_id, resource_ids=ids) now = datetime.now(timezone.utc) # 先抽 primitive 快照,commit/rollback 后日志不再碰 ORM,规避懒加载失效风险。 snapshots = [ { "id": resource.id, "user_id": resource.user_id, "module": resource.module, "resource_type": resource.resource_type, "resource_url": resource.resource_url, "storage_path": resource.storage_path, "file_size_bytes": int(resource.file_size_bytes or 0), } for resource in resources ] released_size = 0 released_ids: list[str] = [] for resource in resources: resource.deleted_at = now resource.file_delete_status = UploadResourceFileDeleteStatusEnum.PENDING_DELETE.value resource.file_delete_error = None released = await release_upload_resource_capacity(db, resource, released_at=now) if released: released_size += int(resource.file_size_bytes or 0) released_ids.append(resource.id) await db.flush() log_upload_resource_event( event_type=UploadResourceEventEnum.DELETE_BATCH_MARKED_PENDING.value, user_id=user_id, detail={ "requested_ids": ids, "deleted_ids": ids, "released_ids": released_ids, "released_size_bytes": released_size, "snapshots": snapshots, "file_delete_status": UploadResourceFileDeleteStatusEnum.PENDING_DELETE.value, }, ) return UploadResourceHistoryBatchDeleteOut( message="删除成功", requested_count=len(ids), deleted_count=len(ids), requested_ids=ids, deleted_ids=ids, released_size_bytes=released_size, cleanup=UploadResourceCleanupOut(), ) async def cleanup_upload_resource_history_files( db: AsyncSession, *, result: UploadResourceHistoryBatchDeleteOut, current_user: User, ) -> UploadResourceHistoryBatchDeleteOut: """主事务 commit 成功后清理真实文件,失败不回滚主删除。""" stats = await cleanup_upload_resource_files_after_commit(db, resource_ids=result.deleted_ids) result.cleanup = _cleanup_out(stats) log_upload_resource_event( event_type=UploadResourceEventEnum.DELETE_BATCH_CLEANUP_SUCCESS.value, user_id=str(current_user.id), detail={ "requested_ids": result.requested_ids, "deleted_ids": result.deleted_ids, "cleanup": result.cleanup.model_dump(), }, ) return result