diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index 1db510f..7e1b4e2 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -1,6 +1,7 @@ # 藏品路由 - 使用字段编码 import os import uuid +import re from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File from sqlalchemy import func, text @@ -13,6 +14,7 @@ from app.schemas.schemas import ( CollectionCreate, CollectionUpdate, CollectionResponse, CollectionListResponse, CollectionImageResponse ) +from app.services.oss import upload_to_oss, get_oss_path, delete_from_oss, get_public_url router = APIRouter(prefix="/api/collections", tags=["藏品"]) @@ -576,55 +578,46 @@ async def upload_image( if file_size > 10 * 1024 * 1024: # 10MB raise HTTPException(status_code=400, detail=f"E00039: 图片大小不能超过 10MB(当前{file_size // 1024 // 1024}MB)") - # 创建上传目录 - upload_dir = "uploads/collections" - os.makedirs(upload_dir, exist_ok=True) - - # 生成文件名:用户名 - 藏品编号 - 冠字号.jpg + # 生成OSS存储路径 + user_id = collection.f99_91_user_id file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'jpg' - # 清理特殊字符,只保留字母、数字、中文、横杠 - import re + + # 清理特殊字符 clean_username = re.sub(r'[^\w\u4e00-\u9fff\-]', '', username) clean_serial = re.sub(r'[^\w\u4e00-\u9fff\-]', '', prefix_serial) - # 文件名格式:用户名 - 藏品编号 - 冠字号 + # 文件名格式:用户名-藏品编号-冠字号.jpg if clean_serial: filename = f"{clean_username}-{code}-{clean_serial}.{file_extension}" else: filename = f"{clean_username}-{code}.{file_extension}" - # 如果文件已存在,添加时间戳避免覆盖 - file_path = os.path.join(upload_dir, filename) - if os.path.exists(file_path): - import time - timestamp = int(time.time()) - base_name = filename.rsplit('.', 1)[0] - filename = f"{base_name}-{timestamp}.{file_extension}" - file_path = os.path.join(upload_dir, filename) + # 生成OSS key + oss_key, unique_name = get_oss_path("collections", user_id=user_id, filename=filename) - # 保存文件 - with open(file_path, "wb") as buffer: - buffer.write(content) + # 上传到OSS + image_url = upload_to_oss(content, oss_key) - # 创建图片记录 + # 创建图片记录(保存OSS URL) image = CollectionImage( id=str(uuid.uuid4()), collection_id=collection_id, - filename=filename, + filename=unique_name, original_name=file.filename, - path=file_path + path=image_url # 保存OSS URL ) db.add(image) db.commit() db.refresh(image) - logger.info(f"图片上传成功:{filename}, collection_id={collection_id}") + logger.info(f"图片上传成功:{image_url}, collection_id={collection_id}") return { "message": "上传成功", "image_id": image.id, - "filename": filename + "filename": unique_name, + "url": image_url } except HTTPException: raise @@ -657,8 +650,16 @@ async def delete_image( if collection and current_user.role != "admin" and collection.f99_91_user_id != current_user.f99_90_id: raise HTTPException(status_code=403, detail="E00014: 无权删除此图片") - # 删除文件 - if image.path and os.path.exists(image.path): + # 删除OSS文件(如果path是OSS URL) + if image.path and image.path.startswith("https://"): + # 从OSS URL提取key + try: + oss_key = image.path.replace("https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com/", "") + delete_from_oss(oss_key) + except Exception as e: + logger.warning(f"OSS文件删除失败: {e}") + elif image.path and os.path.exists(image.path): + # 兼容旧的本地上传 os.remove(image.path) # 删除数据库记录 diff --git a/backend/app/routers/ocr.py b/backend/app/routers/ocr.py index b91a114..961914e 100644 --- a/backend/app/routers/ocr.py +++ b/backend/app/routers/ocr.py @@ -59,18 +59,12 @@ def get_oss_path(file_type: str, user_id: str = None, collection_id: str = None, return None +# 上传图片到OSS - 使用服务层(带压缩) +from app.services.oss import upload_to_oss as oss_upload + def upload_to_oss(file_data, oss_key): - """上传文件到阿里云OSS""" - import oss2 - auth = oss2.Auth(OSS_CONFIG["access_key_id"], OSS_CONFIG["access_key_secret"]) - bucket = oss2.Bucket(auth, OSS_CONFIG["endpoint"], OSS_CONFIG["bucket_name"]) - - result = bucket.put_object(oss_key, file_data) - - if result.status == 200: - return f"{OSS_CONFIG['public_url']}/{oss_key}" - else: - raise Exception(f"OSS上传失败: {result.status}") + """上传文件到阿里云OSS(带自动压缩)""" + return oss_upload(file_data, oss_key) # 专业提示词 PROFESSIONAL_PROMPT = """你是一名专业的人民币生肖纪念钞鉴定专家。请严格按照以下步骤分析这张图片,并提取准确信息。 diff --git a/backend/app/services/oss.py b/backend/app/services/oss.py new file mode 100644 index 0000000..6c52c87 --- /dev/null +++ b/backend/app/services/oss.py @@ -0,0 +1,126 @@ +# 阿里云OSS服务 +import os +import uuid +import datetime +from typing import Optional +import oss2 +from PIL import Image +import io + +# OSS配置 +OSS_CONFIG = { + "access_key_id": os.getenv("OSS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"), + "access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"), + "bucket_name": "jiachenlong-oss", + "endpoint": "oss-cn-hangzhou.aliyuncs.com", + "public_url": "https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com" +} + +# 图片压缩配置 +IMAGE_CONFIG = { + "max_size": 1024 * 1024, # 1MB + "max_width": 2048, + "max_height": 2048, + "quality": 85, + "format": "JPEG" +} + +# 初始化OSS +auth = oss2.Auth(OSS_CONFIG["access_key_id"], OSS_CONFIG["access_key_secret"]) +bucket = oss2.Bucket(auth, OSS_CONFIG["endpoint"], OSS_CONFIG["bucket_name"]) + + +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: + # 如果是图片,进行压缩 + 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"{OSS_CONFIG['public_url']}/{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: + 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"{OSS_CONFIG['public_url']}/{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 diff --git a/config/VERSION b/config/VERSION index dec8cfe..22fd10f 100644 --- a/config/VERSION +++ b/config/VERSION @@ -2,7 +2,7 @@ # Version Configuration for Zodiac Collection Management System # 当前版本号 (语义化版本:主版本。次版本.修订版) -VERSION=1.1.19 +VERSION=1.1.21 # 版本代号 (可选) VERSION_CODENAME="新生" @@ -11,4 +11,4 @@ VERSION_CODENAME="新生" RELEASE_DATE=2026-03-18 # 版本说明 -VERSION_NOTES="个人设置页面 - 支持修改个人信息和密码" +VERSION_NOTES="性能优化 - Nginx反向代理、图片OSS直连、自动压缩、上传限制20MB" diff --git a/frontend/index.html b/frontend/index.html index edb3da0..3030ca7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.1.18 + 甲辰收藏 v1.1.19 diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index cab4a61..a069491 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -95,16 +95,16 @@ export default function Home() { border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)' }}> -
+
甲辰收藏 -
-
退出
-
window.location.hash = '#/settings'} style={{ color: 'rgba(255,255,255,0.5)', cursor: 'pointer', fontSize: '12px', padding: '4px 8px', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px' }}>设置
+
+
{user?.username || '用户'}
+
欢迎回来 👋
+
+
v{APP_VERSION}
-
-
{user?.username || '用户'}
-
欢迎回来 👋
-
+
window.location.hash = '#/settings'} style={{ color: 'rgba(255,255,255,0.5)', cursor: 'pointer', fontSize: '12px', padding: '4px 8px', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px' }}>设置
+
退出