"""文件上传接口""" import os from typing import Optional from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form from sqlalchemy.orm import Session from app.core.database import get_db from app.core.auth import get_current_user from app.core.response import success_response, error_response from app.core.storage import storage router = APIRouter(prefix="/api/upload", tags=["文件上传"]) # 允许的文件类型 ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/heic", "image/heif"} ALLOWED_DOCUMENT_TYPES = { "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" } # 文件大小限制 (字节) MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10MB MAX_DOCUMENT_SIZE = 20 * 1024 * 1024 # 20MB async def validate_file(file: UploadFile, max_size: int, allowed_types: set) -> bytes: """验证文件""" if file.content_type not in allowed_types: raise HTTPException( status_code=400, detail=f"不支持的文件类型: {file.content_type}" ) content = b"" chunk_size = 1024 * 1024 while True: chunk = await file.read(chunk_size) if not chunk: break content += chunk if len(content) > max_size: raise HTTPException(status_code=400, detail="文件大小超过限制") await file.seek(0) return content @router.post("/image", response_model=dict) async def upload_image( file: UploadFile = File(...), folder: str = Form("images"), current_user: dict = Depends(get_current_user) ): """上传图片""" try: content = await validate_file(file, MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES) original_filename = file.filename or "image.jpg" url = storage.upload_file(content, original_filename, folder) return success_response({ "url": url, "filename": original_filename, "contentType": file.content_type, "size": len(content) }) except HTTPException: raise except Exception as e: return error_response(500, f"上传失败: {str(e)}") @router.post("/images", response_model=dict) async def upload_images( files: list[UploadFile] = File(...), folder: str = Form("images"), current_user: dict = Depends(get_current_user) ): """批量上传图片""" if len(files) > 9: return error_response(400, "最多支持同时上传9张图片") results = [] errors = [] for i, file in enumerate(files): try: content = await validate_file(file, MAX_IMAGE_SIZE, ALLOWED_IMAGE_TYPES) original_filename = file.filename or f"image_{i}.jpg" url = storage.upload_file(content, original_filename, folder) results.append({ "url": url, "filename": original_filename, "contentType": file.content_type, "size": len(content), "index": i }) except Exception as e: errors.append({"index": i, "filename": file.filename, "error": str(e)}) return success_response({ "success": results, "errors": errors, "totalSuccess": len(results), "totalError": len(errors) }) @router.post("/document", response_model=dict) async def upload_document( file: UploadFile = File(...), folder: str = Form("documents"), current_user: dict = Depends(get_current_user) ): """上传文档""" try: content = await validate_file(file, MAX_DOCUMENT_SIZE, ALLOWED_DOCUMENT_TYPES) original_filename = file.filename or "document.pdf" url = storage.upload_file(content, original_filename, folder) return success_response({ "url": url, "filename": original_filename, "contentType": file.content_type, "size": len(content) }) except HTTPException: raise except Exception as e: return error_response(500, f"上传失败: {str(e)}") @router.post("/base64", response_model=dict) async def upload_base64( data: dict, current_user: dict = Depends(get_current_user) ): """上传 Base64 编码的图片""" try: base64_data = data.get("base64") filename = data.get("filename", "image.png") folder = data.get("folder", "images") if not base64_data: return error_response(400, "base64数据不能为空") url = storage.upload_base64(base64_data, filename, folder) return success_response({"url": url, "filename": filename}) except Exception as e: return error_response(500, f"上传失败: {str(e)}") @router.delete("/file", response_model=dict) async def delete_file( url: str, current_user: dict = Depends(get_current_user) ): """删除文件""" try: success = storage.delete_file(url) if success: return success_response({"message": "删除成功"}) return error_response(400, "文件不存在或删除失败") except Exception as e: return error_response(500, f"删除失败: {str(e)}") @router.get("/info", response_model=dict) async def get_storage_info( current_user: dict = Depends(get_current_user) ): """获取存储配置信息""" from app.core.config import settings info = { "storageType": settings.STORAGE_TYPE, "maxImageSize": MAX_IMAGE_SIZE, "maxDocumentSize": MAX_DOCUMENT_SIZE, "allowedImageTypes": list(ALLOWED_IMAGE_TYPES), "allowedDocumentTypes": list(ALLOWED_DOCUMENT_TYPES) } if settings.STORAGE_TYPE == "local": info["baseUrl"] = settings.LOCAL_STORAGE_URL elif settings.STORAGE_TYPE == "qiniu": info["domain"] = settings.QINIU_DOMAIN elif settings.STORAGE_TYPE == "oss": info["domain"] = settings.OSS_DOMAIN elif settings.STORAGE_TYPE == "minio": info["domain"] = settings.MINIO_BASE_URL or f"http://{settings.MINIO_ENDPOINT}/{settings.MINIO_BUCKET}" return success_response(info)