from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import and_, desc, asc from pydantic import BaseModel, Field from typing import Optional import uuid from datetime import datetime from ..database import get_db from ..models import ( User, Asset, AssetCategory, NovelChapter, KnowledgeDocument, KnowledgeBase, gen_uuid ) from ..auth import get_current_user from ..middleware.log_service import LogService router = APIRouter() class CreateAssetRequest(BaseModel): name: str = Field(min_length=1, max_length=200) category: Optional[str] = None description: Optional[str] = None file_type: Optional[str] = None content: Optional[str] = None class UpdateAssetRequest(BaseModel): name: Optional[str] = None description: Optional[str] = None file_type: Optional[str] = None class CreateChapterRequest(BaseModel): title: str = Field(min_length=1, max_length=500) content: Optional[str] = None sort_order: int = Field(default=0) VALID_ASSET_CATEGORIES = ("file", "novel", "image", "video", "audio", "custom") # ============================================================ # 静态路由 - 必须在动态路由之前定义 # ============================================================ @router.get("/storage-overview") def get_storage_overview( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): """获取用户资产存储概览,包含资产和知识库文档的总存储量""" # Asset表中的文件大小 from sqlalchemy import func as sa_func asset_size = db.query(sa_func.sum(Asset.size)).filter( and_( Asset.user_id == current_user.id, Asset.status != "deleted", ) ).scalar() or 0 # 知识库文档的文件大小 kb_doc_size = db.query(sa_func.sum(KnowledgeDocument.file_size)).join( KnowledgeBase, KnowledgeDocument.knowledge_base_id == KnowledgeBase.id ).filter( KnowledgeBase.user_id == current_user.id ).scalar() or 0 total_storage = asset_size + kb_doc_size return { "code": 0, "data": { "asset_storage": asset_size, "knowledge_doc_storage": kb_doc_size, "total_storage": total_storage, }, "message": "ok", } @router.get("/categories") def list_categories( db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): categories = ( db.query(AssetCategory) .filter(AssetCategory.user_id == current_user.id) .order_by(asc(AssetCategory.name)) .all() ) result = [] for c in categories: count = db.query(Asset).filter( and_( Asset.category == c.name, Asset.user_id == current_user.id, Asset.status != "deleted", ) ).count() result.append({ "id": c.id, "name": c.name, "type": c.type, "asset_count": count, "created_at": str(c.created_at) if c.created_at else None, }) return {"code": 0, "data": result, "message": "ok"} @router.get("/novels/{asset_id}/chapters") def get_novel_chapters( asset_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = db.query(Asset).filter( and_(Asset.id == asset_id, Asset.user_id == current_user.id) ).first() if not asset: raise HTTPException(status_code=404, detail="资产不存在") chapters = ( db.query(NovelChapter) .filter(NovelChapter.asset_id == asset_id) .order_by(asc(NovelChapter.sort_order), asc(NovelChapter.created_at)) .all() ) result = [] for ch in chapters: result.append({ "id": ch.id, "asset_id": ch.asset_id, "title": ch.title, "sort_order": ch.sort_order, "word_count": ch.word_count or 0, "created_at": str(ch.created_at) if ch.created_at else None, "updated_at": str(ch.updated_at) if ch.updated_at else None, }) return {"code": 0, "data": result, "message": "ok"} @router.post("/novels/{asset_id}/chapters") def add_novel_chapter( asset_id: str, req: CreateChapterRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = db.query(Asset).filter( and_(Asset.id == asset_id, Asset.user_id == current_user.id) ).first() if not asset: raise HTTPException(status_code=404, detail="资产不存在") chapter = NovelChapter( id=gen_uuid(), asset_id=asset_id, title=req.title, content=req.content, sort_order=req.sort_order, word_count=len(req.content or ""), created_at=datetime.now(), updated_at=datetime.now(), ) db.add(chapter) LogService.log_operation( user_id=current_user.id, username=current_user.username, action="add_chapter", target="novel_chapter", target_id=chapter.id, detail=f"为资产 {asset.name} 添加章节 {chapter.title}", ) db.commit() db.refresh(chapter) return { "code": 0, "data": { "id": chapter.id, "asset_id": chapter.asset_id, "title": chapter.title, "sort_order": chapter.sort_order, "word_count": chapter.word_count, "created_at": str(chapter.created_at), }, "message": "章节添加成功", } # ============================================================ # 动态路由 - {asset_id} 必须放在静态路由之后 # ============================================================ @router.get("/") def list_assets( category: Optional[str] = Query(None), search: Optional[str] = Query(None), page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): query = db.query(Asset).filter( and_( Asset.user_id == current_user.id, Asset.status != "deleted", ) ) if category: query = query.filter(Asset.category == category) if search: query = query.filter(Asset.name.ilike(f"%{search}%")) total = query.count() assets = ( query.order_by(desc(Asset.updated_at)) .offset((page - 1) * page_size) .limit(page_size) .all() ) result = [] for a in assets: result.append({ "id": a.id, "name": a.name, "category": a.category, "file_type": a.file_type, "size": a.size or 0, "description": a.description, "url": a.url or "", "content": a.content or "", "status": a.status, "created_at": str(a.created_at) if a.created_at else None, "updated_at": str(a.updated_at) if a.updated_at else None, }) return { "code": 0, "data": {"total": total, "page": page, "page_size": page_size, "items": result}, "message": "ok", } @router.post("/") def create_asset( req: CreateAssetRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): cat = req.category if req.category in VALID_ASSET_CATEGORIES else "file" asset = Asset( id=gen_uuid(), user_id=current_user.id, name=req.name, category=cat, description=req.description or (req.content[:100] if req.content else None), file_type=req.file_type or "unknown", size=len(req.content.encode('utf-8')) if req.content else 0, content=req.content, status="active", created_at=datetime.now(), updated_at=datetime.now(), ) db.add(asset) db.commit() db.refresh(asset) LogService.log_operation( user_id=current_user.id, username=current_user.username, action="create", target="asset", target_id=asset.id, detail=f"创建资产 {asset.name}", ) return { "code": 0, "data": { "id": asset.id, "name": asset.name, "category": asset.category, "description": asset.description, "status": asset.status, "created_at": str(asset.created_at), }, "message": "创建成功", } @router.get("/{asset_id}") def get_asset( asset_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = db.query(Asset).filter( and_(Asset.id == asset_id, Asset.user_id == current_user.id) ).first() if not asset: raise HTTPException(status_code=404, detail="资产不存在") return { "code": 0, "data": { "id": asset.id, "name": asset.name, "category": asset.category, "description": asset.description, "file_type": asset.file_type, "size": asset.size or 0, "url": asset.url or "", "content": asset.content or "", "status": asset.status, "created_at": str(asset.created_at), "updated_at": str(asset.updated_at), }, "message": "ok", } @router.put("/{asset_id}") def update_asset( asset_id: str, req: UpdateAssetRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = db.query(Asset).filter( and_(Asset.id == asset_id, Asset.user_id == current_user.id) ).first() if not asset: raise HTTPException(status_code=404, detail="资产不存在") if req.name is not None: asset.name = req.name if req.description is not None: asset.description = req.description if req.file_type is not None: asset.file_type = req.file_type asset.updated_at = datetime.now() LogService.log_operation( user_id=current_user.id, username=current_user.username, action="update", target="asset", target_id=asset.id, detail=f"更新资产 {asset.name}", ) db.commit() db.refresh(asset) return {"code": 0, "data": { "id": asset.id, "name": asset.name, "status": asset.status, "updated_at": str(asset.updated_at), }, "message": "更新成功"} @router.delete("/{asset_id}") def delete_asset( asset_id: str, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): asset = db.query(Asset).filter( and_(Asset.id == asset_id, Asset.user_id == current_user.id) ).first() if not asset: raise HTTPException(status_code=404, detail="资产不存在") # 软删除 asset.status = "deleted" asset.updated_at = datetime.now() # 若资产由知识库文档同步而来,双向删除对应的知识库文档 if asset.knowledge_doc_id: try: kb_doc = db.query(KnowledgeDocument).filter(KnowledgeDocument.id == asset.knowledge_doc_id).first() if kb_doc: kb = db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_doc.knowledge_base_id).first() if kb: kb.document_count = max(0, (kb.document_count or 0) - 1) kb.updated_at = datetime.now() db.delete(kb_doc) except Exception: pass LogService.log_operation( user_id=current_user.id, username=current_user.username, action="delete", target="asset", target_id=asset.id, detail=f"删除资产 {asset.name}", ) db.commit() return {"code": 0, "message": "资产已删除"}