# 阿里云OSS服务 import os import uuid import datetime from typing import Optional import oss2 from PIL import Image import io from app.core.config import settings # 图片压缩配置 IMAGE_CONFIG = { "max_size": 1024 * 1024, # 1MB "max_width": 2048, "max_height": 2048, "quality": 85, "format": "JPEG" } # 懒加载OSS连接 _oss_bucket = None def _get_oss_bucket(): """获取OSS Bucket实例(懒加载)""" global _oss_bucket if _oss_bucket is None: if not settings.OSS_ACCESS_KEY_ID or not settings.OSS_ACCESS_KEY_SECRET: raise ValueError("OSS配置未设置,请检查环境变量 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET") auth = oss2.Auth(settings.OSS_ACCESS_KEY_ID, settings.OSS_ACCESS_KEY_SECRET) _oss_bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.OSS_BUCKET) return _oss_bucket def get_oss_path(prefix: str, user_id: str = None, filename: str = None) -> str: """生成OSS存储路径""" now = datetime.datetime.now() year = now.strftime("%Y") month = now.strftime("%m") day = now.strftime("%d") if filename: ext = filename.split('.')[-1] if '.' in filename else 'jpg' unique_name = f"{uuid.uuid4().hex}.{ext}" else: unique_name = f"{uuid.uuid4().hex}.jpg" if user_id: path = f"{prefix}/{user_id}/{year}/{month}/{unique_name}" else: path = f"{prefix}/{year}/{month}/{day}/{unique_name}" return path, unique_name def upload_to_oss(file_data: bytes, oss_key: str, compress: bool = True) -> str: """上传文件到OSS,返回公网URL""" try: bucket = _get_oss_bucket() # 如果是图片,进行压缩 if compress and any(oss_key.lower().endswith(ext) for ext in ['.jpg', '.jpeg', '.png', '.webp']): file_data = compress_image(file_data) # 上传文件 result = bucket.put_object(oss_key, file_data) if result.status == 200: # 返回公网URL return f"https://{settings.OSS_BUCKET}.{settings.OSS_ENDPOINT}/{oss_key}" else: raise Exception(f"OSS上传失败: {result.status}") except Exception as e: raise Exception(f"OSS上传失败: {str(e)}") def delete_from_oss(oss_key: str) -> bool: """从OSS删除文件""" try: bucket = _get_oss_bucket() result = bucket.delete_object(oss_key) return result.status == 204 except Exception as e: print(f"OSS删除失败: {str(e)}") return False def get_public_url(oss_key: str) -> str: """获取公网URL""" return f"https://{settings.OSS_BUCKET}.{settings.OSS_ENDPOINT}/{oss_key}" def compress_image(image_data: bytes, max_size: int = None) -> bytes: """压缩图片到指定大小以内""" if max_size is None: max_size = IMAGE_CONFIG["max_size"] # 如果已经小于限制,直接返回 if len(image_data) <= max_size: return image_data # 打开图片 img = Image.open(io.BytesIO(image_data)) # 如果是PNG且有透明通道,转换为RGB if img.mode in ('RGBA', 'P'): img = img.convert('RGB') # 逐步降低质量直到达到目标大小 quality = 95 compressed_data = image_data while quality > 30 and len(compressed_data) > max_size: output = io.BytesIO() img.save(output, format=IMAGE_CONFIG["format"], quality=quality, optimize=True) compressed_data = output.getvalue() quality -= 10 # 如果还是太大,缩小尺寸 if len(compressed_data) > max_size: width, height = img.size while len(compressed_data) > max_size and width > 400: width = int(width * 0.8) height = int(height * 0.8) img_resized = img.resize((width, height), Image.Resampling.LANCZOS) output = io.BytesIO() img_resized.save(output, format=IMAGE_CONFIG["format"], quality=80, optimize=True) compressed_data = output.getvalue() return compressed_data