226 lines
9.6 KiB
Python
226 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_current_user, get_db
|
|
from app.enums.upload_resource import UploadResourceEventEnum
|
|
from app.models.user import User
|
|
from app.schemas.upload_resource import (
|
|
UploadResourceHistoryBatchDeleteOut,
|
|
UploadResourceHistoryBatchDeleteRequest,
|
|
UploadResourceHistoryDayItemsOut,
|
|
UploadResourceHistoryGroupedOut,
|
|
)
|
|
from app.services.upload_resource.delete_service import (
|
|
cleanup_upload_resource_history_files,
|
|
mark_upload_resource_history_deleted,
|
|
)
|
|
from app.services.upload_resource.history_service import (
|
|
list_upload_resource_history_day_items,
|
|
list_upload_resource_history_grouped_days,
|
|
)
|
|
from app.services.upload_resource.log_service import (
|
|
log_upload_resource_exception,
|
|
log_upload_resource_event,
|
|
safe_rollback_with_log,
|
|
)
|
|
|
|
router = APIRouter(
|
|
prefix="/upload-resources",
|
|
tags=["upload-resources"],
|
|
)
|
|
|
|
_HISTORY_DESCRIPTION = (
|
|
"查询当前用户可展示、可复用、可删除的上传素材历史。"
|
|
"只返回 UploadResource 中未删除、未绑定业务记录、bind_status=pending、"
|
|
"delete_policy=user_deletable、resource_type=image/video/audio 的记录。"
|
|
"已绑定爆款开头复刻、拆镜复刻、拆镜切片或其它业务记录的素材不会返回,也不能通过该模块删除。"
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/history",
|
|
response_model=UploadResourceHistoryGroupedOut,
|
|
summary="获取上传历史素材日期分组",
|
|
description=(
|
|
_HISTORY_DESCRIPTION
|
|
+ "返回结构与 /generation-ai/history 接近,generated_date 表示上传日期。"
|
|
"每个日期默认回填前 10 条 items,避免前端首屏再次逐日请求。"
|
|
),
|
|
responses={
|
|
200: {"description": "查询成功,返回上传素材日期分组"},
|
|
400: {"description": "resource_type 不合法或分页参数不合法"},
|
|
401: {"description": "未登录或 Token 无效"},
|
|
},
|
|
)
|
|
async def list_history_grouped_days(
|
|
resource_type: str | None = Query(
|
|
None,
|
|
description="素材类型筛选:image=图片,video=视频,audio=音频;为空表示全部",
|
|
examples=["image"],
|
|
),
|
|
page: int = Query(1, ge=1, description="日期分组页码,从 1 开始"),
|
|
page_size: int = Query(10, ge=1, le=10, description="日期分组每页数量,最大 10"),
|
|
keyword: str | None = Query(None, description="文件名或资源 URL 模糊搜索,可为空"),
|
|
scene: str | None = Query(
|
|
None,
|
|
description="前端使用场景标识:record=素材云展示,picker=AI创作选择器;仅用于日志排查,不影响过滤规则",
|
|
examples=["picker"],
|
|
),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
return await list_upload_resource_history_grouped_days(
|
|
db,
|
|
user_id=current_user.id,
|
|
resource_type=resource_type,
|
|
page=page,
|
|
page_size=page_size,
|
|
keyword=keyword,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
log_upload_resource_exception(
|
|
event_type=UploadResourceEventEnum.HISTORY_LIST_FAILED.value,
|
|
message=f"查询上传历史素材日期分组失败: {exc}",
|
|
user_id=current_user.id,
|
|
detail={"resource_type": resource_type, "page": page, "page_size": page_size, "keyword": keyword, "scene": scene},
|
|
exc=exc,
|
|
)
|
|
raise
|
|
|
|
|
|
@router.get(
|
|
"/history/{generated_date}",
|
|
response_model=UploadResourceHistoryDayItemsOut,
|
|
summary="获取指定日期下上传历史素材",
|
|
description=(
|
|
_HISTORY_DESCRIPTION
|
|
+ "generated_date 格式为 YYYY-MM-DD,字段名沿用生成历史接口,实际语义为上传日期。"
|
|
),
|
|
responses={
|
|
200: {"description": "查询成功,返回指定上传日期下的素材列表"},
|
|
400: {"description": "日期格式错误、resource_type 不合法或分页参数不合法"},
|
|
401: {"description": "未登录或 Token 无效"},
|
|
},
|
|
)
|
|
async def list_history_day_items(
|
|
generated_date: str = Path(..., description="上传日期,格式 YYYY-MM-DD", examples=["2026-07-08"]),
|
|
resource_type: str | None = Query(
|
|
None,
|
|
description="素材类型筛选:image=图片,video=视频,audio=音频;为空表示全部",
|
|
examples=["video"],
|
|
),
|
|
page: int = Query(1, ge=1, description="当前日期下分页页码,从 1 开始"),
|
|
page_size: int = Query(20, ge=1, le=100, description="当前日期下每页数量,最大 100"),
|
|
keyword: str | None = Query(None, description="文件名或资源 URL 模糊搜索,可为空"),
|
|
scene: str | None = Query(
|
|
None,
|
|
description="前端使用场景标识:record=素材云展示,picker=AI创作选择器;仅用于日志排查,不影响过滤规则",
|
|
examples=["record"],
|
|
),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
return await list_upload_resource_history_day_items(
|
|
db,
|
|
user_id=current_user.id,
|
|
generated_date=generated_date,
|
|
resource_type=resource_type,
|
|
page=page,
|
|
page_size=page_size,
|
|
keyword=keyword,
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
log_upload_resource_exception(
|
|
event_type=UploadResourceEventEnum.HISTORY_DAY_LIST_FAILED.value,
|
|
message=f"查询指定日期上传历史素材失败: {exc}",
|
|
user_id=current_user.id,
|
|
detail={"generated_date": generated_date, "resource_type": resource_type, "page": page, "page_size": page_size, "keyword": keyword, "scene": scene},
|
|
exc=exc,
|
|
)
|
|
raise
|
|
|
|
|
|
@router.delete(
|
|
"/history/batch",
|
|
response_model=UploadResourceHistoryBatchDeleteOut,
|
|
summary="批量删除上传历史素材",
|
|
description=(
|
|
"批量删除当前用户的上传历史素材。一次最多 30 条。"
|
|
"只允许删除未绑定业务记录、bind_status=pending、delete_policy=user_deletable 的 UploadResource。"
|
|
"删除顺序为:主事务内软删 DB 记录并释放上传容量 -> commit 成功 -> 再删除真实静态文件 -> 写回真实文件删除状态。"
|
|
"如果真实文件删除失败,主删除不回滚,file_delete_status 会保留 delete_failed/pending_delete 供后续补偿排查。"
|
|
),
|
|
responses={
|
|
200: {"description": "删除成功,返回软删数量、释放容量和真实文件清理结果"},
|
|
400: {"description": "resource_ids 为空、重复或超过 30 条"},
|
|
404: {"description": "部分素材不存在、已删除、已绑定业务记录或无权操作"},
|
|
401: {"description": "未登录或 Token 无效"},
|
|
},
|
|
)
|
|
async def batch_delete_upload_history(
|
|
req: UploadResourceHistoryBatchDeleteRequest = Body(..., description="批量删除上传历史素材请求体"),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result: UploadResourceHistoryBatchDeleteOut | None = None
|
|
user_id = str(current_user.id)
|
|
try:
|
|
result = await mark_upload_resource_history_deleted(db, current_user=current_user, resource_ids=req.resource_ids)
|
|
await db.commit()
|
|
log_upload_resource_event(
|
|
event_type=UploadResourceEventEnum.DELETE_BATCH_COMMIT_SUCCESS.value,
|
|
user_id=user_id,
|
|
detail={
|
|
"requested_ids": result.requested_ids,
|
|
"deleted_ids": result.deleted_ids,
|
|
"released_size_bytes": result.released_size_bytes,
|
|
},
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
await safe_rollback_with_log(
|
|
db,
|
|
event_type=UploadResourceEventEnum.DELETE_BATCH_ROLLBACK_FAILED.value,
|
|
message="批量删除上传历史素材主事务回滚失败",
|
|
user_id=user_id,
|
|
detail={"resource_ids": req.resource_ids},
|
|
original_exc=exc,
|
|
)
|
|
log_upload_resource_exception(
|
|
event_type=UploadResourceEventEnum.DELETE_BATCH_FAILED.value,
|
|
message=f"批量删除上传历史素材失败: {exc}",
|
|
user_id=user_id,
|
|
resource_ids=req.resource_ids,
|
|
detail={"resource_ids": req.resource_ids},
|
|
exc=exc,
|
|
)
|
|
raise
|
|
|
|
if not result:
|
|
raise HTTPException(status_code=500, detail="批量删除上传历史素材失败")
|
|
|
|
try:
|
|
result = await cleanup_upload_resource_history_files(db, result=result, current_user=current_user)
|
|
await db.commit()
|
|
except Exception as exc: # noqa: BLE001
|
|
await safe_rollback_with_log(
|
|
db,
|
|
event_type=UploadResourceEventEnum.DELETE_BATCH_ROLLBACK_FAILED.value,
|
|
message="批量删除上传历史素材 cleanup 事务回滚失败",
|
|
user_id=user_id,
|
|
detail={"resource_ids": req.resource_ids, "deleted_ids": result.deleted_ids if result else []},
|
|
original_exc=exc,
|
|
)
|
|
log_upload_resource_exception(
|
|
event_type=UploadResourceEventEnum.DELETE_BATCH_CLEANUP_FAILED.value,
|
|
message=f"批量删除上传历史素材后清理真实文件失败: {exc}",
|
|
user_id=user_id,
|
|
resource_ids=result.deleted_ids if result else req.resource_ids,
|
|
detail={"resource_ids": req.resource_ids, "deleted_ids": result.deleted_ids if result else []},
|
|
exc=exc,
|
|
)
|
|
return result
|