init
This commit is contained in:
195
backend/app/api/export.py
Normal file
195
backend/app/api/export.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user