137 lines
5.1 KiB
Python
137 lines
5.1 KiB
Python
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
|