Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Form | |
| from sqlalchemy.orm import Session | |
| from sqlalchemy import func | |
| from typing import List, Optional | |
| import uuid | |
| import hashlib | |
| import os | |
| import logging | |
| from datetime import datetime | |
| from dotenv import load_dotenv | |
| from database import get_db | |
| from models.media import Media, MediaDirectory, MediaTag, MediaTagMapping, MediaStatus | |
| from models.company_r2_config import CompanyR2Config | |
| from models.user import User | |
| from schemas.media import ( | |
| MediaResponse, MediaCreate, MediaUpdate, | |
| MediaDirectoryResponse, MediaDirectoryCreate, MediaDirectoryUpdate, | |
| MediaTagResponse, MediaTagCreate, MediaTagUpdate, | |
| MediaBatchResponse, | |
| MediaBatchDeleteRequest, MediaBatchUpdateRequest, MediaBatchTagRequest | |
| ) | |
| from routers.auth import get_current_user | |
| from utils.r2_uploader import R2Uploader, R2Config, get_r2_uploader | |
| router = APIRouter() | |
| # 配置日志 | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| def get_r2_config(db: Session, company_code: str) -> Optional[R2Config]: | |
| """获取 R2 配置,优先级:1.当前公司配置 → 2.'0000'公司配置 → 3..env 环境配置""" | |
| # 重新加载 .env 文件,确保获取最新配置 | |
| load_dotenv() | |
| # 1. 优先获取当前公司的 R2 配置 | |
| logger.info(f"🔍 查找 R2 配置 - 公司编码:{company_code}") | |
| config = db.query(CompanyR2Config).filter( | |
| CompanyR2Config.company_code == company_code | |
| ).first() | |
| if config and config.r2_enabled: | |
| logger.info(f"✅ 使用当前公司 R2 配置:{company_code}") | |
| return R2Config({ | |
| 'r2_account_id': config.r2_account_id, | |
| 'r2_access_key_id': config.r2_access_key_id, | |
| 'r2_secret_access_key': config.r2_secret_access_key, | |
| 'r2_bucket_name': config.r2_bucket_name, | |
| 'r2_public_url': config.r2_public_url, | |
| 'r2_enabled': config.r2_enabled | |
| }) | |
| # 2. 获取默认公司(0000)的 R2 配置 | |
| if company_code != "0000": | |
| config = db.query(CompanyR2Config).filter( | |
| CompanyR2Config.company_code == "0000" | |
| ).first() | |
| if config and config.r2_enabled: | |
| logger.info(f"✅ 使用默认公司 R2 配置:0000") | |
| return R2Config({ | |
| 'r2_account_id': config.r2_account_id, | |
| 'r2_access_key_id': config.r2_access_key_id, | |
| 'r2_secret_access_key': config.r2_secret_access_key, | |
| 'r2_bucket_name': config.r2_bucket_name, | |
| 'r2_public_url': config.r2_public_url, | |
| 'r2_enabled': config.r2_enabled | |
| }) | |
| # 3. 从 .env 文件读取 R2 配置 | |
| logger.info(f"ℹ️ 尝试从 .env 文件读取 R2 配置") | |
| r2_account_id = os.getenv("R2_ACCOUNT_ID", "") | |
| r2_access_key_id = os.getenv("R2_ACCESS_KEY_ID", "") | |
| r2_secret_access_key = os.getenv("R2_SECRET_ACCESS_KEY", "") | |
| r2_bucket_name = os.getenv("R2_BUCKET_NAME", "") | |
| r2_public_url = os.getenv("R2_PUBLIC_URL", "") | |
| r2_enabled = os.getenv("R2_ENABLED", "true").lower() in ("true", "1", "yes") | |
| logger.info(f" - R2_ACCOUNT_ID: {r2_account_id[:10] if r2_account_id else 'None'}...") | |
| logger.info(f" - R2_ACCESS_KEY_ID: {r2_access_key_id[:10] if r2_access_key_id else 'None'}...") | |
| logger.info(f" - R2_BUCKET_NAME: {r2_bucket_name}") | |
| logger.info(f" - R2_ENABLED: {r2_enabled}") | |
| if r2_account_id and r2_access_key_id and r2_secret_access_key and r2_bucket_name: | |
| logger.info(f"✅ 使用 .env 文件中的 R2 配置") | |
| return R2Config({ | |
| 'r2_account_id': r2_account_id, | |
| 'r2_access_key_id': r2_access_key_id, | |
| 'r2_secret_access_key': r2_secret_access_key, | |
| 'r2_bucket_name': r2_bucket_name, | |
| 'r2_public_url': r2_public_url, | |
| 'r2_enabled': r2_enabled | |
| }) | |
| logger.error(f"❌ 未找到任何 R2 配置") | |
| return None | |
| def generate_media_id() -> str: | |
| """生成32位UUID作为media_id""" | |
| return uuid.uuid4().hex | |
| def generate_file_hash(file_path: str) -> str: | |
| """生成文件MD5哈希""" | |
| hash_md5 = hashlib.md5() | |
| with open(file_path, "rb") as f: | |
| for chunk in iter(lambda: f.read(4096), b""): | |
| hash_md5.update(chunk) | |
| return hash_md5.hexdigest() | |
| # ==================== 目录管理(先排,避免与 {media_id} 冲突) ==================== | |
| async def get_media_directories( | |
| parent_id: Optional[int] = None, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取媒体目录列表""" | |
| print(f"获取目录列表 - parent_id: {parent_id}, company_code: {current_user.company_code}") | |
| query = db.query(MediaDirectory).filter( | |
| MediaDirectory.company_code == current_user.company_code | |
| ) | |
| if parent_id is not None: | |
| query = query.filter(MediaDirectory.parent_id == parent_id) | |
| else: | |
| query = query.filter(MediaDirectory.parent_id == None) | |
| directories = query.order_by(MediaDirectory.sort_order, MediaDirectory.created_at).all() | |
| # 为每个目录计算总大小和文件数量 | |
| result = [] | |
| for directory in directories: | |
| # 查询该目录下的所有媒体文件总大小(只统计未删除的) | |
| total_size = db.query(func.sum(Media.file_size)).filter( | |
| Media.directory_id == directory.id, | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ).scalar() or 0 | |
| # 查询该目录下的媒体文件数量(只统计未删除的) | |
| media_count = db.query(func.count(Media.media_id)).filter( | |
| Media.directory_id == directory.id, | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ).scalar() or 0 | |
| # 创建响应对象 | |
| dir_response = MediaDirectoryResponse( | |
| id=directory.id, | |
| name=directory.name, | |
| parent_id=directory.parent_id, | |
| path=directory.path, | |
| level=directory.level, | |
| sort_order=directory.sort_order, | |
| cover_r2_key=directory.cover_r2_key, | |
| cover_r2_url=directory.cover_r2_url, | |
| description=directory.description, | |
| is_public=directory.is_public, | |
| media_count=media_count, | |
| total_size=total_size, | |
| company_code=directory.company_code, | |
| created_at=directory.created_at, | |
| updated_at=directory.updated_at | |
| ) | |
| result.append(dir_response) | |
| print(f"查询到 {len(result)} 个目录: {[d.name for d in result]}") | |
| return result | |
| async def get_media_directory( | |
| directory_id: int, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取单个媒体目录""" | |
| directory = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == directory_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if not directory: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Directory not found" | |
| ) | |
| return directory | |
| async def create_media_directory( | |
| directory_data: MediaDirectoryCreate, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """创建媒体目录""" | |
| try: | |
| print(f"创建文件夹请求 - name: {directory_data.name}, parent_id: {directory_data.parent_id}, company_code: {current_user.company_code}") | |
| # 验证文件夹名称 | |
| if not directory_data.name or directory_data.name.strip() == '': | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="文件夹名称不能为空" | |
| ) | |
| # 验证父目录 | |
| parent = None | |
| if directory_data.parent_id is not None: | |
| parent = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == directory_data.parent_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if not parent: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="父目录不存在" | |
| ) | |
| # 检查同级目录是否已存在同名文件夹 | |
| existing_dir = db.query(MediaDirectory).filter( | |
| MediaDirectory.name == directory_data.name, | |
| MediaDirectory.company_code == current_user.company_code, | |
| MediaDirectory.parent_id == directory_data.parent_id | |
| ).first() | |
| if existing_dir: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="同级目录下已存在同名文件夹" | |
| ) | |
| # 生成路径 | |
| path = directory_data.name | |
| level = 0 | |
| if parent: | |
| path = f"{parent.path}/{directory_data.name}" | |
| level = parent.level + 1 | |
| directory = MediaDirectory( | |
| name=directory_data.name, | |
| parent_id=directory_data.parent_id, | |
| company_code=current_user.company_code, | |
| path=path, | |
| level=level, | |
| sort_order=directory_data.sort_order or 0, | |
| cover_r2_key=directory_data.cover_r2_key, | |
| cover_r2_url=directory_data.cover_r2_url, | |
| description=directory_data.description, | |
| is_public=directory_data.is_public or False | |
| ) | |
| db.add(directory) | |
| db.commit() | |
| db.refresh(directory) | |
| print(f"文件夹创建成功 - id: {directory.id}, name: {directory.name}") | |
| return directory | |
| except HTTPException: | |
| db.rollback() | |
| raise | |
| except Exception as e: | |
| db.rollback() | |
| print(f"创建文件夹失败: {str(e)}") | |
| import traceback | |
| traceback.print_exc() | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"创建文件夹失败: {str(e)}" | |
| ) | |
| async def update_media_directory( | |
| directory_id: int, | |
| directory_update: MediaDirectoryUpdate, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """更新媒体目录""" | |
| directory = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == directory_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if not directory: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Directory not found" | |
| ) | |
| update_data = directory_update.model_dump(exclude_unset=True) | |
| # 如果更新父目录,需要重新计算路径 | |
| if 'parent_id' in update_data and update_data['parent_id'] != directory.parent_id: | |
| if update_data['parent_id']: | |
| parent = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == update_data['parent_id'], | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if parent: | |
| directory.path = f"{parent.path}/{directory.name}" | |
| directory.level = parent.level + 1 | |
| else: | |
| directory.path = directory.name | |
| directory.level = 0 | |
| for field, value in update_data.items(): | |
| setattr(directory, field, value) | |
| db.commit() | |
| db.refresh(directory) | |
| return directory | |
| async def delete_media_directory( | |
| directory_id: int, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """删除媒体目录""" | |
| directory = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == directory_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if not directory: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Directory not found" | |
| ) | |
| # 检查是否有子目录 | |
| child_count = db.query(MediaDirectory).filter( | |
| MediaDirectory.parent_id == directory_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).count() | |
| if child_count > 0: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="该文件夹下还有子文件夹,无法删除" | |
| ) | |
| # 检查是否有媒体文件 | |
| media_count = db.query(Media).filter( | |
| Media.directory_id == directory_id, | |
| Media.company_code == current_user.company_code | |
| ).count() | |
| if media_count > 0: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="该文件夹下还有文件,无法删除" | |
| ) | |
| db.delete(directory) | |
| db.commit() | |
| return None | |
| # ==================== 标签管理 ==================== | |
| async def get_tags( | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取标签列表""" | |
| tags = db.query(MediaTag).filter( | |
| MediaTag.company_code == current_user.company_code | |
| ).order_by(MediaTag.sort_order, MediaTag.created_at).all() | |
| return tags | |
| async def create_tag( | |
| tag_data: MediaTagCreate, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """创建标签""" | |
| tag = MediaTag( | |
| tag_name=tag_data.tag_name, | |
| tag_color=tag_data.tag_color, | |
| sort_order=tag_data.sort_order or 0, | |
| company_code=current_user.company_code | |
| ) | |
| db.add(tag) | |
| db.commit() | |
| db.refresh(tag) | |
| return tag | |
| async def update_tag( | |
| tag_id: int, | |
| tag_update: MediaTagUpdate, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """更新标签""" | |
| tag = db.query(MediaTag).filter( | |
| MediaTag.id == tag_id, | |
| MediaTag.company_code == current_user.company_code | |
| ).first() | |
| if not tag: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Tag not found" | |
| ) | |
| update_data = tag_update.model_dump(exclude_unset=True) | |
| for field, value in update_data.items(): | |
| setattr(tag, field, value) | |
| db.commit() | |
| db.refresh(tag) | |
| return tag | |
| async def delete_tag( | |
| tag_id: int, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """删除标签""" | |
| tag = db.query(MediaTag).filter( | |
| MediaTag.id == tag_id, | |
| MediaTag.company_code == current_user.company_code | |
| ).first() | |
| if not tag: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Tag not found" | |
| ) | |
| db.delete(tag) | |
| db.commit() | |
| return None | |
| async def batch_add_tags( | |
| batch_tag: MediaBatchTagRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """批量添加标签到素材""" | |
| # 验证标签是否存在 | |
| for tag_id in batch_tag.tag_ids: | |
| tag = db.query(MediaTag).filter( | |
| MediaTag.id == tag_id, | |
| MediaTag.company_code == current_user.company_code | |
| ).first() | |
| if not tag: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Tag {tag_id} not found" | |
| ) | |
| # 更新素材的标签 | |
| media_list = db.query(Media).filter( | |
| Media.media_id.in_(batch_tag.media_ids), | |
| Media.company_code == current_user.company_code | |
| ).all() | |
| if not media_list: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="No media found" | |
| ) | |
| for media in media_list: | |
| current_tags = media.tags or [] | |
| for tag_id in batch_tag.tag_ids: | |
| if tag_id not in current_tags: | |
| current_tags.append(tag_id) | |
| media.tags = current_tags | |
| db.commit() | |
| return {"message": "Tags added successfully", "count": len(media_list)} | |
| async def batch_remove_tags( | |
| batch_tag: MediaBatchTagRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """批量移除素材的标签""" | |
| media_list = db.query(Media).filter( | |
| Media.media_id.in_(batch_tag.media_ids), | |
| Media.company_code == current_user.company_code | |
| ).all() | |
| if not media_list: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="No media found" | |
| ) | |
| for media in media_list: | |
| current_tags = media.tags or [] | |
| media.tags = [tag_id for tag_id in current_tags if tag_id not in batch_tag.tag_ids] | |
| db.commit() | |
| return {"message": "Tags removed successfully", "count": len(media_list)} | |
| # ==================== 批量操作 ==================== | |
| async def batch_upload_media( | |
| files: List[UploadFile] = File(...), | |
| directory_id: Optional[int] = Form(None), | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """批量上传媒体文件(每 5 张一批次)""" | |
| logger.info(f"\n{'='*60}") | |
| logger.info(f"🚀 接收到批量上传请求") | |
| logger.info(f"{'='*60}") | |
| logger.info(f"📊 文件数:{len(files)}") | |
| logger.info(f"📊 directory_id: {directory_id}") | |
| logger.info(f"👤 用户:{current_user.username}") | |
| logger.info(f"🏢 公司编码:{current_user.company_code}") | |
| logger.info(f"📧 邮箱:{current_user.email}") | |
| logger.info(f"🆔 用户 ID: {current_user.id}") | |
| # 验证文件数量 | |
| if len(files) == 0: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="No files provided" | |
| ) | |
| # 生成批次 ID | |
| batch_id = generate_media_id() | |
| logger.info(f"📦 生成批次 ID: {batch_id}") | |
| # 添加调试信息到响应 | |
| debug_info = { | |
| 'company_code': current_user.company_code, | |
| 'username': current_user.username, | |
| 'user_id': current_user.id | |
| } | |
| # 验证并处理每个文件 | |
| results = [] | |
| successful = 0 | |
| failed = 0 | |
| # 收集文件信息 | |
| file_infos = [] | |
| valid_files = [] | |
| for idx, file in enumerate(files, 1): | |
| print(f"\n[{idx}/{len(files)}] 处理文件:{file.filename}") | |
| # 验证文件类型 | |
| content_type = file.content_type or "" | |
| print(f" - 文件类型:{content_type}") | |
| if not any(t in content_type for t in ['image', 'video']): | |
| print(f" ❌ 文件类型不支持") | |
| results.append({ | |
| 'file_name': file.filename, | |
| 'status': 'failed', | |
| 'error': '仅支持图片和视频文件' | |
| }) | |
| failed += 1 | |
| continue | |
| # 验证文件大小 | |
| file_content = await file.read() | |
| file_size = len(file_content) | |
| print(f" - 文件大小:{file_size} 字节 ({file_size / 1024:.2f} KB)") | |
| if file_size > 5 * 1024 * 1024: | |
| print(f" ❌ 文件大小超过限制") | |
| results.append({ | |
| 'file_name': file.filename, | |
| 'status': 'failed', | |
| 'error': '文件大小超过 5MB 限制' | |
| }) | |
| failed += 1 | |
| continue | |
| # 生成文件名(MD5) | |
| file_hash = hashlib.md5(file_content).hexdigest() | |
| file_extension = os.path.splitext(file.filename)[1] | |
| r2_key = f"{file_hash}{file_extension}" | |
| print(f" - 文件哈希:{file_hash}") | |
| print(f" - R2 Key: {r2_key}") | |
| # 生成media_id | |
| media_id = generate_media_id() | |
| # 收集文件信息 | |
| file_info = { | |
| 'media_id': media_id, | |
| 'file_name': file.filename, | |
| 'file_content': file_content, | |
| 'file_size': file_size, | |
| 'content_type': content_type, | |
| 'r2_key': r2_key, | |
| 'directory_id': directory_id | |
| } | |
| file_infos.append(file_info) | |
| valid_files.append(file_info) | |
| # 批量检查数据库中是否存在相同文件 | |
| existing_r2_keys = set() | |
| existing_media_map = {} | |
| if valid_files: | |
| # 提取所有r2_key | |
| r2_keys = [f['r2_key'] for f in valid_files] | |
| # 批量查询数据库,只查询r2_url有值的记录 | |
| existing_medias = db.query(Media).filter( | |
| Media.r2_key.in_(r2_keys), | |
| Media.r2_url.isnot(None), | |
| Media.r2_url != '' | |
| ).all() | |
| # 构建映射 | |
| for media in existing_medias: | |
| existing_r2_keys.add(media.r2_key) | |
| existing_media_map[media.r2_key] = media | |
| print(f" 📊 数据库中已存在 {len(existing_r2_keys)} 个文件") | |
| # 按MD5分组文件,相同MD5的文件只上传一次到R2 | |
| md5_groups = {} | |
| for file_info in valid_files: | |
| r2_key = file_info['r2_key'] | |
| if r2_key in existing_r2_keys: | |
| # 文件已存在,复用R2文件信息 | |
| existing_media = existing_media_map[r2_key] | |
| print(f" ⚠️ 文件已存在,复用R2文件但创建新记录") | |
| print(f" - 复用R2 Key:{existing_media.r2_key}") | |
| print(f" - 复用R2 URL:{existing_media.r2_url}") | |
| print(f" - 目标目录:{file_info['directory_id']}") | |
| # 创建新的数据库记录,复用R2文件信息 | |
| new_media = Media( | |
| media_id=file_info['media_id'], | |
| file_name=file_info['file_name'], | |
| r2_key=existing_media.r2_key, | |
| r2_url=existing_media.r2_url, | |
| thumbnail_r2_key=existing_media.thumbnail_r2_key, | |
| thumbnail_r2_url=existing_media.thumbnail_r2_url, | |
| media_type=existing_media.media_type, | |
| mime_type=existing_media.mime_type, | |
| file_size=existing_media.file_size, | |
| directory_id=file_info['directory_id'], | |
| batch_id=batch_id, | |
| status=MediaStatus.SUCCESS, | |
| company_code=current_user.company_code | |
| ) | |
| db.add(new_media) | |
| db.commit() | |
| db.refresh(new_media) | |
| print(f" ✅ 新记录创建成功 - media_id: {file_info['media_id']}") | |
| results.append({ | |
| 'file_name': file_info['file_name'], | |
| 'media_id': file_info['media_id'], | |
| 'status': 'success', | |
| 'r2_url': existing_media.r2_url, | |
| 'thumbnail_r2_url': existing_media.thumbnail_r2_url, | |
| 'info': '复用R2文件,创建新记录' | |
| }) | |
| successful += 1 | |
| else: | |
| # 文件不存在,添加到上传列表 | |
| if r2_key not in md5_groups: | |
| md5_groups[r2_key] = { | |
| 'file_content': file_info['file_content'], | |
| 'file_size': file_info['file_size'], | |
| 'content_type': file_info['content_type'], | |
| 'files': [] | |
| } | |
| # 添加文件信息到分组 | |
| md5_groups[r2_key]['files'].append({ | |
| 'media_id': file_info['media_id'], | |
| 'file_name': file_info['file_name'], | |
| 'directory_id': file_info['directory_id'] | |
| }) | |
| # 获取 R2 配置 | |
| r2_config = get_r2_config(db, current_user.company_code) | |
| if not r2_config: | |
| print(f" ❌ R2 配置未找到") | |
| for r2_key, group in md5_groups.items(): | |
| for file_info in group['files']: | |
| results.append({ | |
| 'file_name': file_info['file_name'], | |
| 'status': 'failed', | |
| 'error': 'R2 配置未找到' | |
| }) | |
| failed += 1 | |
| else: | |
| print(f" ✅ R2 配置已加载:{r2_config.bucket_name}") | |
| # 准备批量上传文件 | |
| files_to_upload = [] | |
| thumbnails_to_upload = [] | |
| for r2_key, group in md5_groups.items(): | |
| # 添加到文件上传列表 | |
| files_to_upload.append({ | |
| 'file_buffer': group['file_content'], | |
| 'file_name': r2_key, | |
| 'content_type': group['content_type'] | |
| }) | |
| # 如果是图片,添加到缩略图上传列表 | |
| if 'image' in group['content_type']: | |
| thumbnails_to_upload.append({ | |
| 'file_buffer': group['file_content'], | |
| 'file_name': f"thumb_{r2_key}", | |
| 'width': 200, | |
| 'height': 200 | |
| }) | |
| # 批量上传文件 | |
| upload_results = {} | |
| thumbnail_results = {} | |
| if files_to_upload: | |
| print(f" 📤 开始批量上传 {len(files_to_upload)} 个唯一文件到 R2...") | |
| r2_uploader = R2Uploader(r2_config, company_code=current_user.company_code) | |
| upload_results = await r2_uploader.batch_upload_files(files_to_upload, max_concurrency=5) | |
| # 批量上传缩略图 | |
| if thumbnails_to_upload: | |
| print(f" 🖼️ 开始批量上传 {len(thumbnails_to_upload)} 个缩略图到 R2...") | |
| thumbnail_results = await r2_uploader.batch_upload_thumbnails(thumbnails_to_upload, max_concurrency=5) | |
| # 处理上传结果 | |
| for r2_key, group in md5_groups.items(): | |
| r2_url = upload_results.get(r2_key, '') | |
| thumbnail_r2_url = thumbnail_results.get(f"thumb_{r2_key}", '') | |
| if r2_url: | |
| # 为该MD5分组中的每个文件创建数据库记录 | |
| for file_info in group['files']: | |
| # 保存到数据库 | |
| print(f" 💾 保存到数据库...") | |
| media = Media( | |
| media_id=file_info['media_id'], | |
| file_name=file_info['file_name'], | |
| r2_key=r2_key, | |
| r2_url=r2_url, | |
| thumbnail_r2_key=f"thumb_{r2_key}" if 'image' in group['content_type'] else None, | |
| thumbnail_r2_url=thumbnail_r2_url if 'image' in group['content_type'] else None, | |
| media_type='image' if 'image' in group['content_type'] else 'video', | |
| mime_type=group['content_type'] or "", | |
| file_size=group['file_size'], | |
| directory_id=file_info['directory_id'], | |
| batch_id=batch_id, | |
| status=MediaStatus.SUCCESS, | |
| company_code=current_user.company_code | |
| ) | |
| db.add(media) | |
| db.commit() | |
| db.refresh(media) | |
| print(f" ✅ 数据库保存成功 - media_id: {file_info['media_id']}") | |
| results.append({ | |
| 'file_name': file_info['file_name'], | |
| 'media_id': file_info['media_id'], | |
| 'status': 'success', | |
| 'r2_url': r2_url, | |
| 'thumbnail_r2_url': thumbnail_r2_url, | |
| 'info': '复用R2文件,创建新记录' if len(group['files']) > 1 else '' | |
| }) | |
| successful += 1 | |
| else: | |
| # 上传失败,为该MD5分组中的每个文件添加失败记录并写入数据库 | |
| for file_info in group['files']: | |
| print(f" ❌ 文件上传失败:{file_info['file_name']}") | |
| # 创建失败的数据库记录,以便后续可以重新上传 | |
| failed_media = Media( | |
| media_id=file_info['media_id'], | |
| file_name=file_info['file_name'], | |
| r2_key=r2_key, | |
| r2_url='', | |
| thumbnail_r2_key=f"thumb_{r2_key}" if 'image' in group['content_type'] else None, | |
| thumbnail_r2_url='', | |
| media_type='image' if 'image' in group['content_type'] else 'video', | |
| mime_type=group['content_type'] or "", | |
| file_size=group['file_size'], | |
| directory_id=file_info['directory_id'], | |
| batch_id=batch_id, | |
| status=MediaStatus.FAILED, | |
| company_code=current_user.company_code | |
| ) | |
| db.add(failed_media) | |
| db.commit() | |
| db.refresh(failed_media) | |
| print(f" 💾 失败记录已保存到数据库 - media_id: {file_info['media_id']}") | |
| results.append({ | |
| 'file_name': file_info['file_name'], | |
| 'media_id': file_info['media_id'], | |
| 'status': 'failed', | |
| 'error': '文件上传失败' | |
| }) | |
| failed += 1 | |
| logger.info(f"\n📊 批量上传完成 - 总计:{len(files)}, 成功:{successful}, 失败:{failed}") | |
| response_data = { | |
| 'batch_id': batch_id, | |
| 'total': len(files), | |
| 'successful': successful, | |
| 'failed': failed, | |
| 'results': results, | |
| 'debug': debug_info | |
| } | |
| logger.info(f"📥 返回响应:{response_data}") | |
| return response_data | |
| async def batch_delete_media( | |
| batch_delete: MediaBatchDeleteRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """批量删除媒体素材(软删除)""" | |
| media_list = db.query(Media).filter( | |
| Media.media_id.in_(batch_delete.media_ids), | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ).all() | |
| if not media_list: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="No media found" | |
| ) | |
| print(f"\n📤 批量软删除 {len(media_list)} 个媒体文件") | |
| now = datetime.now() | |
| for idx, media in enumerate(media_list, 1): | |
| print(f"\n [{idx}/{len(media_list)}] 软删除: {media.file_name}") | |
| print(f" - R2 Key: {media.r2_key}") | |
| # 软删除:标记为已删除,不删除R2文件 | |
| media.is_deleted = True | |
| media.deleted_at = now | |
| db.commit() | |
| print(f"\n✅ 批量软删除完成\n") | |
| return None | |
| async def batch_update_media( | |
| batch_update: MediaBatchUpdateRequest, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """批量更新媒体素材(设置目录、标签)""" | |
| media_list = db.query(Media).filter( | |
| Media.media_id.in_(batch_update.media_ids), | |
| Media.company_code == current_user.company_code | |
| ).all() | |
| if not media_list: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="No media found" | |
| ) | |
| if batch_update.directory_id is not None: | |
| if batch_update.directory_id != 0: | |
| directory = db.query(MediaDirectory).filter( | |
| MediaDirectory.id == batch_update.directory_id, | |
| MediaDirectory.company_code == current_user.company_code | |
| ).first() | |
| if not directory: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Directory not found" | |
| ) | |
| for media in media_list: | |
| if batch_update.directory_id is not None: | |
| media.directory_id = batch_update.directory_id if batch_update.directory_id > 0 else None | |
| if batch_update.tags is not None: | |
| media.tags = batch_update.tags | |
| db.commit() | |
| for media in media_list: | |
| db.refresh(media) | |
| return media_list | |
| # ==================== 基础媒体操作(最后排,避免与其他路由冲突) ==================== | |
| async def get_media_list( | |
| page: int = 1, | |
| page_size: int = 20, | |
| media_type: Optional[str] = None, | |
| status: Optional[str] = None, | |
| directory_id: Optional[int] = None, | |
| keyword: Optional[str] = None, | |
| tag_ids: Optional[List[int]] = None, | |
| start_date: Optional[str] = None, | |
| end_date: Optional[str] = None, | |
| min_size: Optional[int] = None, | |
| max_size: Optional[int] = None, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取媒体素材列表(支持搜索筛选)""" | |
| query = db.query(Media).filter( | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ) | |
| if keyword: | |
| query = query.filter( | |
| Media.file_name.contains(keyword) | | |
| Media.media_id.contains(keyword) | |
| ) | |
| if media_type: | |
| query = query.filter(Media.media_type == media_type) | |
| if status: | |
| query = query.filter(Media.status == status) | |
| if directory_id is not None: | |
| query = query.filter(Media.directory_id == directory_id) | |
| if tag_ids: | |
| query = query.filter( | |
| Media.tags.contains(tag_ids) | |
| ) | |
| if start_date: | |
| query = query.filter(Media.created_at >= start_date) | |
| if end_date: | |
| query = query.filter(Media.created_at <= end_date) | |
| if min_size is not None: | |
| query = query.filter(Media.file_size >= min_size) | |
| if max_size is not None: | |
| query = query.filter(Media.file_size <= max_size) | |
| total = query.count() | |
| # 计算 skip | |
| skip = (page - 1) * page_size | |
| media_list = query.offset(skip).limit(page_size).all() | |
| # 转换为 MediaResponse 对象 | |
| items = [] | |
| for media in media_list: | |
| item = MediaResponse( | |
| id=media.id, | |
| media_id=media.media_id, | |
| file_name=media.file_name, | |
| file_path="", | |
| file_size=media.file_size, | |
| media_type=media.media_type, | |
| content_type=media.mime_type, | |
| r2_key=media.r2_key, | |
| r2_url=media.r2_url, | |
| thumbnail_r2_key=media.thumbnail_r2_key, | |
| thumbnail_r2_url=media.thumbnail_r2_url, | |
| width=media.width, | |
| height=media.height, | |
| duration=media.duration, | |
| status=media.status, | |
| error_message=media.error_message, | |
| directory_id=media.directory_id, | |
| tags=media.tags or [], | |
| metadata=media.metadata or {}, | |
| company_code=media.company_code, | |
| created_at=media.created_at, | |
| updated_at=media.updated_at | |
| ) | |
| items.append(item) | |
| return { | |
| 'items': items, | |
| 'total': total, | |
| 'page': page, | |
| 'page_size': page_size | |
| } | |
| async def get_media( | |
| media_id: str, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取单个媒体素材""" | |
| media = db.query(Media).filter( | |
| Media.media_id == media_id, | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ).first() | |
| if not media: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Media not found" | |
| ) | |
| return media | |
| async def create_media( | |
| file: UploadFile = File(...), | |
| directory_id: Optional[int] = Form(None), | |
| description: Optional[str] = Form(None), | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """上传单个媒体文件""" | |
| # 验证文件类型 | |
| allowed_types = ['image', 'video'] | |
| content_type = file.content_type or "" | |
| if not any(t in content_type for t in allowed_types): | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="Only image and video files are allowed" | |
| ) | |
| # 验证文件大小(最大5MB) | |
| file_size = 0 | |
| file_content = await file.read() | |
| file_size = len(file_content) | |
| if file_size > 5 * 1024 * 1024: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail="File size exceeds 5MB limit" | |
| ) | |
| # 生成media_id | |
| media_id = generate_media_id() | |
| # 生成文件名(MD5) | |
| file_hash = hashlib.md5(file_content).hexdigest() | |
| file_extension = os.path.splitext(file.filename)[1] | |
| r2_key = f"{file_hash}{file_extension}" | |
| # 获取R2配置 | |
| r2_config = get_r2_config(db, current_user.company_code) | |
| if not r2_config: | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail="R2 configuration not found" | |
| ) | |
| # 上传到R2 | |
| try: | |
| r2_uploader = R2Uploader(r2_config) | |
| # 上传原图 | |
| file_buffer = file_content | |
| r2_url = await r2_uploader.upload_file( | |
| file_buffer=file_buffer, | |
| file_name=r2_key, | |
| content_type=content_type | |
| ) | |
| # 生成缩略图(如果是图片) | |
| thumbnail_r2_key = None | |
| thumbnail_r2_url = None | |
| if 'image' in content_type: | |
| thumbnail_r2_key = f"thumb_{r2_key}" | |
| thumbnail_r2_url = await r2_uploader.upload_thumbnail( | |
| file_buffer=file_buffer, | |
| file_name=thumbnail_r2_key, | |
| width=200, | |
| height=200 | |
| ) | |
| # 保存到数据库 | |
| media = Media( | |
| media_id=media_id, | |
| file_name=file.filename, | |
| r2_key=r2_key, | |
| r2_url=r2_url, | |
| thumbnail_r2_key=thumbnail_r2_key, | |
| thumbnail_r2_url=thumbnail_r2_url, | |
| media_type='image' if 'image' in content_type else 'video', | |
| mime_type=file.content_type or "", | |
| file_size=file_size, | |
| directory_id=directory_id, | |
| status=MediaStatus.SUCCESS, | |
| company_code=current_user.company_code | |
| ) | |
| db.add(media) | |
| db.commit() | |
| db.refresh(media) | |
| return media | |
| except Exception as e: | |
| db.rollback() | |
| raise HTTPException( | |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | |
| detail=f"Upload failed: {str(e)}" | |
| ) | |
| async def update_media( | |
| media_id: str, | |
| media_update: MediaUpdate, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """更新媒体素材""" | |
| media = db.query(Media).filter( | |
| Media.media_id == media_id, | |
| Media.company_code == current_user.company_code | |
| ).first() | |
| if not media: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Media not found" | |
| ) | |
| update_data = media_update.model_dump(exclude_unset=True) | |
| for field, value in update_data.items(): | |
| setattr(media, field, value) | |
| db.commit() | |
| db.refresh(media) | |
| return media | |
| async def delete_media( | |
| media_id: str, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """删除媒体素材(软删除)""" | |
| media = db.query(Media).filter( | |
| Media.media_id == media_id, | |
| Media.company_code == current_user.company_code, | |
| Media.is_deleted == False | |
| ).first() | |
| if not media: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="Media not found" | |
| ) | |
| print(f"\n📤 软删除媒体: {media.file_name} (ID: {media_id})") | |
| print(f" - R2 Key: {media.r2_key}") | |
| # 软删除:标记为已删除,不删除R2文件 | |
| media.is_deleted = True | |
| media.deleted_at = datetime.now() | |
| db.commit() | |
| print(f" ✅ 软删除完成\n") | |
| return None | |