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
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user