import os import json import zipfile from typing import Annotated from fastapi import APIRouter, HTTPException, Depends from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, text from app.database import get_db from app.models.story import Story from app.models.person import Person from app.config import settings from app.services.docx_generator import generate_person_doc router = APIRouter(prefix="/export", tags=["导出"]) UNKNOWN_PERSON_ID = "unknown" UNKNOWN_PERSON_NAME = "暂无" @router.get("/list") async def export_list(db: Annotated[AsyncSession, Depends(get_db)]): """获取导出列表(所有已匹配的讲述者,含'暂无')""" # 获取所有已匹配的故事按 person_id 分组 result = await db.execute( select(Story.person_id) .where(Story.match_status == "matched") .distinct() ) person_ids = [row[0] for row in result.all()] items = [] for pid in person_ids: if pid == UNKNOWN_PERSON_ID: story_result = await db.execute( select(Story).where( Story.person_id == UNKNOWN_PERSON_ID, Story.match_status == "matched", ) ) stories = story_result.scalars().all() items.append({ "person_id": UNKNOWN_PERSON_ID, "person_name": UNKNOWN_PERSON_NAME, "story_count": len(stories), "has_photo": False, }) else: person = await db.get(Person, pid) if not person: continue story_result = await db.execute( select(Story).where( Story.person_id == person.id, Story.match_status == "matched", ) ) stories = story_result.scalars().all() items.append({ "person_id": person.id, "person_name": person.name, "story_count": len(stories), "has_photo": bool(person.photo_path), }) return items @router.get("/preview/{person_id}") async def preview_export(person_id: str, db: Annotated[AsyncSession, Depends(get_db)]): """预览某讲述者的文档内容""" if person_id == UNKNOWN_PERSON_ID: person_name = UNKNOWN_PERSON_NAME person_info = "" has_photo = False else: person = await db.get(Person, person_id) if not person: raise HTTPException(404, "讲述者不存在") person_name = person.name person_info = person.info has_photo = bool(person.photo_path) story_result = await db.execute( select(Story).where( Story.person_id == person_id, Story.match_status == "matched", ) ) stories = story_result.scalars().all() return { "person_name": person_name, "person_info": person_info, "has_photo": has_photo, "stories": [{"title": s.title, "summary": s.summary, "tags": json.loads(s.tags) if s.tags else [], "content": s.content, "raw_material": s.raw_material or ""} for s in stories], } @router.get("/download/{person_id}") async def download_one(person_id: str, db: Annotated[AsyncSession, Depends(get_db)]): """下载单个讲述者的 DOCX""" if person_id == UNKNOWN_PERSON_ID: person_name = UNKNOWN_PERSON_NAME person_info = "" photo_path = "" else: person = await db.get(Person, person_id) if not person: raise HTTPException(404, "讲述者不存在") person_name = person.name person_info = person.info photo_path = person.photo_path or "" story_result = await db.execute( select(Story).where( Story.person_id == person_id, Story.match_status == "matched", ) ) stories = story_result.scalars().all() if not stories: raise HTTPException(400, "该讲述者没有已匹配的故事") output_path = os.path.join(settings.EXPORT_DIR, f"{person_name}_故事集.docx") generate_person_doc( person_name=person_name, person_info=person_info, photo_path=photo_path, stories=[{"title": s.title, "summary": s.summary, "tags": json.loads(s.tags) if s.tags else [], "content": s.content} for s in stories], output_path=output_path, ) return FileResponse( output_path, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", filename=f"{person_name}_故事集.docx", ) @router.get("/download-all") async def download_all(db: Annotated[AsyncSession, Depends(get_db)]): """批量下载所有已匹配讲述者的文档(ZIP)""" # 获取所有已匹配的 person_id result = await db.execute( select(Story.person_id) .where(Story.match_status == "matched") .distinct() ) person_ids = [row[0] for row in result.all()] if not person_ids: raise HTTPException(400, "没有可导出的文档") # 生成 ZIP zip_path = os.path.join(settings.EXPORT_DIR, "故事集_全部.zip") with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for pid in person_ids: if pid == UNKNOWN_PERSON_ID: person_name = UNKNOWN_PERSON_NAME person_info = "" photo_path = "" else: person = await db.get(Person, pid) if not person: continue person_name = person.name person_info = person.info photo_path = person.photo_path or "" story_result = await db.execute( select(Story).where( Story.person_id == pid, Story.match_status == "matched", ) ) stories = story_result.scalars().all() if not stories: continue output_path = os.path.join(settings.EXPORT_DIR, f"{person_name}_故事集.docx") generate_person_doc( person_name=person_name, person_info=person_info, photo_path=photo_path, stories=[{"title": s.title, "summary": s.summary, "tags": json.loads(s.tags) if s.tags else [], "content": s.content} for s in stories], output_path=output_path, ) zf.write(output_path, f"{person_name}_故事集.docx") return FileResponse( zip_path, media_type="application/zip", filename="故事集_全部.zip", )