Your Name 8ab2d869fc feat: 阿里云 OSS 迁移至腾讯云 COS + 完善部署配置
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>
2026-02-10 10:28:13 +08:00

126 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
文件上传 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,
)