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