COS 迁移: - 后端签名服务改为 COS HMAC-SHA1 表单直传签名 - config.py: OSS_* 配置项替换为 COS_SECRET_ID/KEY/REGION/BUCKET_NAME/CDN_DOMAIN - upload.py: UploadPolicyResponse 改为 COS 字段 - 前端 useOSSUpload hook: FormData 字段改为 COS 格式 - 前端 api.ts: UploadPolicyResponse 类型对齐 部署配置: - docker-compose.yml: 新增 Nginx + 前端容器,数据卷宿主机持久化 - Nginx: HTTPS + HTTP/2 + SSE 长连接 + API/前端反向代理 - backup.sh: PostgreSQL 每日备份 → 本地 + COS - .env.example: 更新为 COS 配置模板 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.2 KiB
Python
126 lines
3.2 KiB
Python
"""
|
||
文件上传 API
|
||
"""
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
from datetime import datetime
|
||
|
||
from app.services.oss import generate_upload_policy, get_file_url
|
||
from app.config import settings
|
||
from app.models.user import User
|
||
from app.api.deps import get_current_user
|
||
|
||
router = APIRouter(prefix="/upload", tags=["文件上传"])
|
||
|
||
|
||
class UploadPolicyRequest(BaseModel):
|
||
"""获取上传凭证请求"""
|
||
file_type: str = "general" # script, video, image, general
|
||
file_name: Optional[str] = None
|
||
|
||
|
||
class UploadPolicyResponse(BaseModel):
|
||
"""COS 直传凭证响应"""
|
||
q_sign_algorithm: str
|
||
q_ak: str
|
||
q_key_time: str
|
||
q_signature: str
|
||
policy: str
|
||
host: str
|
||
dir: str
|
||
expire: int
|
||
max_size_mb: int
|
||
|
||
|
||
class FileUploadedRequest(BaseModel):
|
||
"""文件上传完成回调"""
|
||
file_key: str
|
||
file_name: str
|
||
file_size: int
|
||
file_type: str
|
||
|
||
|
||
class FileUploadedResponse(BaseModel):
|
||
"""文件上传完成响应"""
|
||
url: str
|
||
file_key: str
|
||
file_name: str
|
||
file_size: int
|
||
file_type: str
|
||
|
||
|
||
@router.post("/policy", response_model=UploadPolicyResponse)
|
||
async def get_upload_policy(
|
||
request: UploadPolicyRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""
|
||
获取 COS 直传凭证
|
||
|
||
前端使用此凭证直接上传文件到腾讯云 COS,无需经过后端。
|
||
|
||
文件类型说明:
|
||
- script: 脚本文档 (docx, pdf, xlsx, txt, pptx)
|
||
- video: 视频文件 (mp4, mov, webm)
|
||
- image: 图片文件 (jpg, png, gif)
|
||
- general: 通用文件
|
||
"""
|
||
# 根据文件类型设置上传目录
|
||
now = datetime.now()
|
||
base_dir = f"uploads/{now.year}/{now.month:02d}"
|
||
|
||
if request.file_type == "script":
|
||
upload_dir = f"{base_dir}/scripts/"
|
||
elif request.file_type == "video":
|
||
upload_dir = f"{base_dir}/videos/"
|
||
elif request.file_type == "image":
|
||
upload_dir = f"{base_dir}/images/"
|
||
else:
|
||
upload_dir = f"{base_dir}/files/"
|
||
|
||
try:
|
||
policy = generate_upload_policy(
|
||
max_size_mb=settings.MAX_FILE_SIZE_MB,
|
||
expire_seconds=3600,
|
||
upload_dir=upload_dir,
|
||
)
|
||
except ValueError as e:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail=str(e),
|
||
)
|
||
|
||
return UploadPolicyResponse(
|
||
q_sign_algorithm=policy["q_sign_algorithm"],
|
||
q_ak=policy["q_ak"],
|
||
q_key_time=policy["q_key_time"],
|
||
q_signature=policy["q_signature"],
|
||
policy=policy["policy"],
|
||
host=policy["host"],
|
||
dir=policy["dir"],
|
||
expire=policy["expire"],
|
||
max_size_mb=settings.MAX_FILE_SIZE_MB,
|
||
)
|
||
|
||
|
||
@router.post("/complete", response_model=FileUploadedResponse)
|
||
async def file_uploaded(
|
||
request: FileUploadedRequest,
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
"""
|
||
文件上传完成回调
|
||
|
||
前端上传完成后调用此接口,获取文件的完整 URL。
|
||
"""
|
||
url = get_file_url(request.file_key)
|
||
|
||
return FileUploadedResponse(
|
||
url=url,
|
||
file_key=request.file_key,
|
||
file_name=request.file_name,
|
||
file_size=request.file_size,
|
||
file_type=request.file_type,
|
||
)
|