Files
yu-zhi-ran/platform/backend/app/api/assets.py
T
lt cf5103bbca feat: 全面升级项目架构 - PostgreSQL迁移 + 配置化改造
主要变更:
- 数据库: SQLite → PostgreSQL (yzr_nr)
- 选题系统: 硬编码字段 → 配置化 (TopicField/TopicConfigField/TopicStatusConfig)
- 新增模型: ContentCalendar, ContentMetrics, MediaAsset, PlatformConfig, ContentTask
- 新增 API: topic-config, calendar, metrics, assets, tasks, platform-config
- 数据迁移: 现有选题数据迁移到新 schema (field_id/tags/custom_data/scoring_data)
- 初始化数据: 10个领域, 5种状态, 3个平台配置

服务运行: http://localhost:8001
默认账号: admin / admin123
2026-05-08 18:26:01 +08:00

180 lines
5.4 KiB
Python

import os
import uuid
import hashlib
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from ..database import get_db
from ..models import MediaAsset
from ..schemas import MediaAssetCreate, MediaAssetUpdate, MediaAssetResponse
from .auth import get_current_user
router = APIRouter(prefix="/api/assets", tags=["assets"])
PROJECT_ROOT = Path(__file__).resolve().parents[4]
if os.getenv('PROJECT_ROOT'):
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
UPLOAD_DIR = PROJECT_ROOT / "content" / "images"
ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
@router.get("", response_model=List[MediaAssetResponse])
def list_assets(
file_type: Optional[str] = None,
tag: Optional[str] = None,
topic_id: Optional[str] = None,
search: Optional[str] = None,
limit: int = Query(50, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
query = db.query(MediaAsset)
if file_type:
query = query.filter(MediaAsset.file_type == file_type)
if tag:
query = query.filter(MediaAsset.tags.contains([tag]))
if topic_id:
query = query.filter(MediaAsset.topic_ids.contains([topic_id]))
if search:
query = query.filter(
(MediaAsset.filename.contains(search)) |
(MediaAsset.alt_text.contains(search))
)
return query.order_by(MediaAsset.created_at.desc()).offset(offset).limit(limit).all()
@router.get("/tags")
def list_tags(
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
assets = db.query(MediaAsset.tags).all()
all_tags = set()
for a in assets:
if a[0]:
all_tags.update(a[0])
return sorted(all_tags)
@router.get("/counts")
def get_counts(
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
total = db.query(MediaAsset).count()
by_type = {}
rows = db.query(MediaAsset.file_type, db.func.count(MediaAsset.id)).group_by(MediaAsset.file_type).all()
for ftype, cnt in rows:
by_type[ftype] = cnt
return {"total": total, "by_type": by_type}
@router.post("/upload", response_model=MediaAssetResponse)
async def upload_asset(
file: UploadFile = File(...),
tags: Optional[str] = Query(None, description="逗号分隔的标签"),
alt_text: Optional[str] = Query(None),
topic_ids: Optional[str] = Query(None, description="逗号分隔的选题ID"),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
if file.content_type not in ALLOWED_IMAGE_TYPES:
raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file.content_type}")
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
suffix = Path(file.filename).suffix or ""
unique_name = f"{uuid.uuid4().hex[:12]}{suffix}"
file_path = UPLOAD_DIR / unique_name
content = await file.read()
file_size = len(content)
with open(file_path, "wb") as f:
f.write(content)
file_type = file.content_type.split("/")[0]
if file_type not in ("image", "video", "application"):
if suffix in (".pdf", ".doc", ".docx", ".ppt", ".pptx"):
file_type = "document"
elif suffix in (".mp4", ".mov", ".avi"):
file_type = "video"
else:
file_type = "image"
parsed_tags = [t.strip() for t in tags.split(",")] if tags else []
parsed_topic_ids = [t.strip() for t in topic_ids.split(",")] if topic_ids else []
asset = MediaAsset(
filename=file.filename,
file_path=str(file_path),
file_type=file_type,
mime_type=file.content_type,
size=file_size,
alt_text=alt_text,
tags=parsed_tags,
topic_ids=parsed_topic_ids,
uploaded_by=current_user.username
)
db.add(asset)
db.commit()
db.refresh(asset)
return asset
@router.put("/{asset_id}", response_model=MediaAssetResponse)
def update_asset(
asset_id: int,
data: MediaAssetUpdate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(asset, k, v)
db.commit()
db.refresh(asset)
return asset
@router.delete("/{asset_id}")
def delete_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
if os.path.exists(asset.file_path):
try:
os.remove(asset.file_path)
except OSError:
pass
db.delete(asset)
db.commit()
return {"ok": True}
@router.post("/{asset_id}/use")
def increment_usage(
asset_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
asset = db.query(MediaAsset).filter(MediaAsset.id == asset_id).first()
if not asset:
raise HTTPException(status_code=404, detail="素材不存在")
asset.usage_count = (asset.usage_count or 0) + 1
db.commit()
return {"ok": True, "usage_count": asset.usage_count}