feat(oss): 统一OSS/CDN对象存储与直链出口体系

- infrastructure/oss.py 重写:服务端上传/预签名上下行/CDN鉴权直链(方式A)/resolve_url·to_object_path 出入口规范
- config.py 新增 OSS_REGION/OSS_CDN_BASE_URL/OSS_CDN_AUTH_KEY
- media_upload 统一走 oss 单例,返回 CDN 直链;dispatcher /oss 前缀路由
- 管理端用户列表头像出口 CDN 化
This commit is contained in:
Pine
2026-08-28 17:17:38 +08:00
parent 89396eb6b0
commit 985b85cb4e
5 changed files with 214 additions and 50 deletions
+24 -28
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
"""媒体上传(图片/pdf/doc 等)—— OSS 优先(OSS_ENDPOINT 配置后走桶),否则落 serverdata/uploads
"""媒体上传(图片/pdf/doc 等)—— 统一走基础设施层 OSSinfrastructure.oss.oss
统一返回契约:绝对 URL。编辑器图片/视频/封面、C端资料上传共用。
OSS 已配置时上传到桶并返回 /oss/<key> 的绝对 URL(下载经 /oss 路由跳预签名);
未配置时降级本地 serverdata/uploads。编辑器图片/视频/封面、C端资料上传共用。
"""
from __future__ import annotations
@@ -12,12 +13,11 @@ from pathlib import Path
from fastapi import HTTPException, UploadFile
from .. import config
# server-data 上传目录(server-core 根下 serverdata/uploads,本地降级用
SERVER_CORE_DIR = Path(__file__).resolve().parents[2]
UPLOAD_DIR = SERVER_CORE_DIR / "serverdata" / "uploads"
# server-data 上传目录(本地降级用,与 infrastructure.oss 同一目录
UPLOAD_DIR = config.SERVERDATA_DIR / "uploads"
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# 站点公开基础地址(把 /uploads 相对路径补全为完整 HTTPS)
# 站点公开基础地址(把 /oss、/uploads 相对路径补全为完整 HTTPS)
PUBLIC_BASE = __import__("os").environ.get("PUBLIC_BASE", "https://opc.pinesound.cn").rstrip("/")
ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf", ".doc", ".docx"}
@@ -33,23 +33,19 @@ _MIME = {
def abs_url(u: str | None) -> str:
"""相对路径补全为绝对 URL;已是完整 URL 或空则原样返回。"""
if not u:
return u or ""
u = str(u).strip()
if u.startswith("http://") or u.startswith("https://"):
return u
return PUBLIC_BASE + u
"""相对路径补全为绝对 URL;已是完整 URL 或空则原样返回(委托基础设施层 resolve_url"""
from ..infrastructure.oss import resolve_url
return resolve_url(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, dir: str = "misc") -> str:
"""保存上传文件(OSS 优先,本地降级);返回 CDN/OSS 直链绝对 URL;不合法抛 400/413。
OSS 路径规范:<业务目录>/<YYYYMM>/<时间戳>_<随机>.<ext>
业务目录:avatar 头像 / park-admission 入驻认证资料 / news 资讯 / event 活动封面 / park 大屏媒体 / misc 兜底
"""
from ..infrastructure.oss import oss
async def save_media(file: UploadFile) -> str:
"""保存上传文件: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:
@@ -57,13 +53,13 @@ async def save_media(file: UploadFile) -> str:
payload = await file.read()
if len(payload) > MAX_SIZE:
raise HTTPException(status_code=413, detail="文件超过 20MB 限制")
fname = f"up_{int(time.time())}_{secrets.token_hex(2)}{ext}"
key = build_key(dir, ext)
await oss.upload(key, payload, content_type=_MIME.get(ext, "application/octet-stream"))
# 直接返回 CDN/OSS 直链,不经服务端代理
return abs_url(oss.direct_url(key))
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/<key> 或 /uploads/<key>——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}")
def build_key(dir: str, ext: str) -> str:
"""按规范生成对象 key<业务目录>/<YYYYMM>/<时间戳>_<随机>.<ext>。"""
d = "".join(c for c in (dir or "misc").lower() if c.isalnum() or c in "-_") or "misc"
return f"{d}/{time.strftime('%Y%m')}/{int(time.time())}_{secrets.token_hex(4)}{ext}"