From 64ed01f5dc93208c8a1d347611b8bfd2fe7eb25f Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 10 Apr 2025 11:38:15 +0800 Subject: [PATCH] update --- app/api/v1/api.py | 4 +- app/api/v1/tryon.py | 15 +++++++ app/api/v1/upload.py | 88 ++++++++++++++++++++++++++++++++++++ app/core/config.py | 6 +++ app/schemas/tryon.py | 17 +++++++ app/services/cos.py | 103 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ++- 7 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 app/api/v1/upload.py create mode 100644 app/services/cos.py diff --git a/app/api/v1/api.py b/app/api/v1/api.py index 2f86a7b..de929da 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -5,6 +5,7 @@ from app.api.v1.auth import router as auth_router from app.api.v1.person_images import router as person_images_router from app.api.v1.clothing import router as clothing_router from app.api.v1.tryon import router as tryon_router +from app.api.v1.upload import router as upload_router api_router = APIRouter() api_router.include_router(endpoints_router, prefix="") @@ -12,4 +13,5 @@ api_router.include_router(users_router, prefix="/users") api_router.include_router(auth_router, prefix="/auth") api_router.include_router(person_images_router, prefix="/person-images") api_router.include_router(clothing_router, prefix="/clothing") -api_router.include_router(tryon_router, prefix="/tryon") \ No newline at end of file +api_router.include_router(tryon_router, prefix="/tryon") +api_router.include_router(upload_router, prefix="/upload") \ No newline at end of file diff --git a/app/api/v1/tryon.py b/app/api/v1/tryon.py index f79ff70..7dfa561 100644 --- a/app/api/v1/tryon.py +++ b/app/api/v1/tryon.py @@ -13,6 +13,8 @@ from app.api.deps import get_current_user from app.services.dashscope_service import DashScopeService from app.schemas.response import StandardResponse from app.models.tryon import TryonHistory +from app.schemas.tryon import TryonHistoryModel +from sqlalchemy import select logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -72,3 +74,16 @@ async def tryon( return StandardResponse(code=200, message="试穿任务已提交", data=tryon_history.id) else: return StandardResponse(code=500, message="试穿任务提交失败") + +@router.get("/tryon/histories", tags=["tryon"]) +async def get_tryon_histories( + db: AsyncSession = Depends(deps.get_db), + current_user: User = Depends(get_current_user) +): + """ + 获取试穿历史 + """ + histories = await db.execute(select(TryonHistory).where(TryonHistory.user_id == current_user.id)) + tryon_histories = histories.scalars().all() + + return StandardResponse(code=200, message="试穿历史获取成功", data=[TryonHistoryModel.model_validate(history) for history in tryon_histories]) diff --git a/app/api/v1/upload.py b/app/api/v1/upload.py new file mode 100644 index 0000000..8e2c692 --- /dev/null +++ b/app/api/v1/upload.py @@ -0,0 +1,88 @@ +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status, Request +from fastapi.responses import JSONResponse +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List, Optional +from app.api import deps +from app.services import cos as cos_service +from app.schemas.response import StandardResponse +import logging + +router = APIRouter() +logger = logging.getLogger(__name__) + +@router.post("", response_model=StandardResponse, tags=["upload"]) +async def upload_file( + file: UploadFile = File(...), + directory: str = "uploads" +): + """ + 上传文件到腾讯云COS + + - 支持的文件类型: 图片(jpg, jpeg, png, gif, webp), 文档(pdf, doc, docx) + - 返回文件的访问URL + """ + try: + content_type = file.content_type or "" + + # 检查文件类型 + allowed_types = [ + "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", + "application/pdf", "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ] + + if content_type not in allowed_types: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的文件类型" + ) + + # 获取文件内容 + file_content = await file.read() + + # 获取文件扩展名 + filename = file.filename or "" + file_extension = "." + filename.split(".")[-1] if "." in filename else "" + + # 上传到腾讯云COS + url = await cos_service.upload_file( + file_content=file_content, + file_extension=file_extension, + directory=directory + ) + + return StandardResponse(code=200, data={"url": url}) + + except Exception as e: + logger.error(f"文件上传失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"文件上传失败: {str(e)}" + ) + +@router.post("/from-url", response_model=StandardResponse, tags=["upload"]) +async def upload_from_url( + url: str, + directory: str = "uploads" +): + """ + 从URL上传文件到腾讯云COS + + - 支持的文件类型: 图片(jpg, jpeg, png, gif, webp) + - 返回文件的访问URL + """ + try: + # 从URL上传文件 + cos_url = await cos_service.upload_file_from_url( + url=url, + directory=directory + ) + + return StandardResponse(code=200, data={"url": cos_url}) + + except Exception as e: + logger.error(f"从URL上传文件失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"从URL上传文件失败: {str(e)}" + ) \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 8bec645..b2a863c 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -34,6 +34,12 @@ class Settings(BaseSettings): # 微信设置 WECHAT_APP_ID: str = os.getenv("WECHAT_APP_ID", "") WECHAT_APP_SECRET: str = os.getenv("WECHAT_APP_SECRET", "") + + # 腾讯云COS配置 + COS_SECRET_ID: str = os.getenv("COS_SECRET_ID", "") + COS_SECRET_KEY: str = os.getenv("COS_SECRET_KEY", "") + COS_REGION: str = os.getenv("COS_REGION", "ap-guangzhou") + COS_BUCKET: str = os.getenv("COS_BUCKET", "") @property def cors_origins(self): diff --git a/app/schemas/tryon.py b/app/schemas/tryon.py index bb04087..0cef009 100644 --- a/app/schemas/tryon.py +++ b/app/schemas/tryon.py @@ -1,5 +1,7 @@ from pydantic import BaseModel from typing import Optional +from app.models.tryon import TryonStatus +from datetime import datetime class TryonRequest(BaseModel): @@ -7,3 +9,18 @@ class TryonRequest(BaseModel): bottom_clothing_id: Optional[int] = None top_clothing_url: Optional[str] = None bottom_clothing_url: Optional[str] = None + + +class TryonHistoryModel(BaseModel): + id: int + person_image_id: int + top_clothing_id: int + bottom_clothing_id: int + top_clothing_url: str + bottom_clothing_url: str + status: TryonStatus + create_time: datetime + update_time: datetime + + class Config: + from_attributes = True diff --git a/app/services/cos.py b/app/services/cos.py new file mode 100644 index 0000000..4afe3d3 --- /dev/null +++ b/app/services/cos.py @@ -0,0 +1,103 @@ +import logging +import os +import uuid +from datetime import datetime +from qcloud_cos import CosConfig, CosS3Client +from app.core.config import settings + +logger = logging.getLogger(__name__) + +# 腾讯云COS配置 +config = CosConfig( + Region=settings.COS_REGION, + SecretId=settings.COS_SECRET_ID, + SecretKey=settings.COS_SECRET_KEY +) + +# 创建客户端 +cos_client = CosS3Client(config) + +def generate_file_path(directory: str, file_extension: str) -> str: + """生成文件路径""" + today = datetime.now().strftime("%Y%m%d") + filename = f"{uuid.uuid4().hex}{file_extension}" + return f"{directory}/{today}/{filename}" + +async def upload_file(file_content: bytes, file_extension: str, directory: str = "uploads") -> str: + """上传文件到腾讯云COS""" + try: + # 生成唯一文件路径 + file_path = generate_file_path(directory, file_extension) + + # 上传到腾讯云COS + cos_client.put_object( + Bucket=settings.COS_BUCKET, + Body=file_content, + Key=file_path + ) + + # 返回可访问的URL + url = f"https://{settings.COS_BUCKET}.cos.{settings.COS_REGION}.myqcloud.com/{file_path}" + logger.info(f"文件上传成功: {url}") + return url + except Exception as e: + logger.error(f"文件上传失败: {str(e)}") + raise + +async def upload_file_from_url(url: str, directory: str = "uploads") -> str: + """从URL下载文件并上传到腾讯云COS""" + try: + import requests + + # 下载文件 + response = requests.get(url, timeout=10) + response.raise_for_status() + + # 获取文件扩展名 + file_extension = os.path.splitext(url)[1] + if not file_extension: + # 如果URL没有扩展名,根据内容类型判断 + content_type = response.headers.get("Content-Type", "") + if "jpeg" in content_type or "jpg" in content_type: + file_extension = ".jpg" + elif "png" in content_type: + file_extension = ".png" + elif "gif" in content_type: + file_extension = ".gif" + else: + file_extension = ".bin" # 默认二进制文件 + + # 上传到腾讯云COS + return await upload_file(response.content, file_extension, directory) + except Exception as e: + logger.error(f"从URL上传文件失败: {str(e)}") + raise + +async def get_presigned_url(key: str, expires: int = 3600) -> str: + """获取预签名URL""" + try: + # 生成预签名URL + url = cos_client.get_presigned_url( + Method='GET', + Bucket=settings.COS_BUCKET, + Key=key, + Expired=expires + ) + return url + except Exception as e: + logger.error(f"获取预签名URL失败: {str(e)}") + raise + +async def delete_file(key: str) -> bool: + """删除文件""" + try: + # 删除文件 + cos_client.delete_object( + Bucket=settings.COS_BUCKET, + Key=key + ) + logger.info(f"文件删除成功: {key}") + return True + except Exception as e: + logger.error(f"文件删除失败: {str(e)}") + return False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0ca6872..0e910ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,8 @@ python-jose[cryptography]==3.3.0 passlib==1.7.4 httpx==0.24.1 dashscope==1.10.0 -itsdangerous==2.2.0 \ No newline at end of file +itsdangerous==2.2.0 +# 腾讯云COS SDK +cos-python-sdk-v5==1.9.25 +requests>=2.28.1 +python-multipart>=0.0.10 \ No newline at end of file