526278589b
- 新增 metrics.html: 数据分析页面 (Dashboard, 趋势图, 平台对比, 选题推荐) - 新增 assets.html: 素材库页面 (上传/管理/预览/标签) - 新增 tasks.html: 创作任务页面 (任务列表/进度/详情) - 新增 platforms.html: 平台配置页面 (知乎/微信/小红书格式规则) - 更新导航组件: 添加新页面入口, 适配 H5 底部导航 - 修复 calendar.html: 使用本地 Vue/ElementPlus 资源 - 修复 assets.py: db.func.count -> sqlalchemy.func.count - 新增 test_api_unit.py: 后端 API 单元测试 - 新增 test_frontend.sh: 前端页面完整性测试 PC/H5 双端适配, 所有页面统一使用 navbar + navigation 组件
181 lines
5.4 KiB
Python
181 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 sqlalchemy import func
|
|
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, 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} |