This commit is contained in:
2026-04-24 16:02:16 +08:00
commit 1d6f0cc370
66 changed files with 9990 additions and 0 deletions

195
backend/app/api/export.py Normal file
View File

@@ -0,0 +1,195 @@
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",
)