From 74d50c829dfad82102f7f41015069fc04092feef Mon Sep 17 00:00:00 2001 From: Pine Date: Thu, 27 Aug 2026 14:53:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(upload):=20=E5=AA=92=E4=BD=93=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E4=BC=98=E5=85=88=E8=B5=B0=20OSS=EF=BC=88OSS=5FENDPOI?= =?UTF-8?q?NT=20=E5=90=AF=E7=94=A8=E2=86=92=E6=A1=B6=EF=BC=8C=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20endpoint/bucket/key=20=E7=BB=9D=E5=AF=B9URL?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E6=9C=AC=E5=9C=B0=E9=99=8D=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- app/services/media_upload.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/app/services/media_upload.py b/app/services/media_upload.py index 1295194..0e15acc 100644 --- a/app/services/media_upload.py +++ b/app/services/media_upload.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -"""媒体上传(图片/pdf/doc 等)—— 存 serverdata/uploads,返回绝对 URL。 +"""媒体上传(图片/pdf/doc 等)—— OSS 优先(OSS_ENDPOINT 配置后走桶),否则落 serverdata/uploads。 -与 app/training/main.py 的 /api/upload 共用同一目录与返回契约 {ok, url}。 +统一返回契约:绝对 URL。编辑器图片/视频/封面、C端资料上传共用。 """ from __future__ import annotations @@ -10,8 +10,9 @@ import time from pathlib import Path from fastapi import HTTPException, UploadFile +from .. import config -# server-data 上传目录(server-core 根下 serverdata/uploads) +# server-data 上传目录(server-core 根下 serverdata/uploads,本地降级用) SERVER_CORE_DIR = Path(__file__).resolve().parents[2] UPLOAD_DIR = SERVER_CORE_DIR / "serverdata" / "uploads" UPLOAD_DIR.mkdir(parents=True, exist_ok=True) @@ -22,6 +23,14 @@ PUBLIC_BASE = __import__("os").environ.get("PUBLIC_BASE", "https://opc.pinesound ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf", ".doc", ".docx"} MAX_SIZE = 20 * 1024 * 1024 # 20MB +# 扩展名 → content-type +_MIME = { + ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", + ".webp": "image/webp", ".gif": "image/gif", + ".pdf": "application/pdf", ".doc": "application/msword", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", +} + def abs_url(u: str | None) -> str: """相对路径补全为绝对 URL;已是完整 URL 或空则原样返回。""" @@ -33,8 +42,14 @@ def abs_url(u: str | None) -> str: return PUBLIC_BASE + u +def _oss_url(key: str) -> str: + """OSS 对象 -> 可访问绝对 URL(公共读桶 endpoint/bucket/key)。""" + endpoint = (config.OSS_ENDPOINT or "").rstrip("/") + return f"{endpoint}/{config.OSS_BUCKET}/{key}" + + async def save_media(file: UploadFile) -> str: - """保存上传文件到 uploads 目录,返回绝对 URL;不合法抛 400/413。""" + """保存上传文件:OSS 优先,否则本地 uploads;返回绝对 URL;不合法抛 400/413。""" name = (file.filename or "").rsplit("/", 1)[-1] ext = ("." + name.split(".")[-1].lower()) if "." in name else "" if ext not in ALLOWED_EXT: @@ -43,6 +58,12 @@ async def save_media(file: UploadFile) -> str: if len(payload) > MAX_SIZE: raise HTTPException(status_code=413, detail="文件超过 20MB 限制") fname = f"up_{int(time.time())}_{secrets.token_hex(2)}{ext}" + + if config.OSS_ENDPOINT and config.OSS_SECRET_KEY and config.OSS_ACCESS_KEY: + from ..infrastructure.oss import OSS + path = await OSS().upload(fname, payload, content_type=_MIME.get(ext, "application/octet-stream")) + # upload 返回 /oss/ 或 /uploads/——OSS 桶走 endpoint/bucket/key + return _oss_url(fname) with open(UPLOAD_DIR / fname, "wb") as f: f.write(payload) return abs_url(f"/uploads/{fname}")