实现模板解析与模板管理基础链路

This commit is contained in:
zwt13703
2026-07-02 14:56:54 +08:00
parent e558733f05
commit b15a3a1f18
15 changed files with 541 additions and 30 deletions
+224
View File
@@ -0,0 +1,224 @@
import asyncio
import os
import tempfile
import uuid
from datetime import datetime
from io import BytesIO
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from database import get_db
from models.paragraph import Paragraph
from models.template import Template
from schemas.schemas import Response, TemplateSave
from services.minio_client import minio_client
from services.template_parser import parse_template
router = APIRouter()
def _build_object_path(filename: str) -> tuple[str, str]:
ext = os.path.splitext(filename)[1].lower()
date_prefix = datetime.now().strftime("%Y%m%d")
object_name = f"{date_prefix}/{uuid.uuid4().hex}{ext}"
return ext, object_name
def _serialize_paragraph(paragraph: Paragraph) -> dict:
return {
"id": paragraph.id,
"template_id": paragraph.template_id,
"sort_index": paragraph.sort_index,
"title": paragraph.title,
"content": paragraph.content,
"style_json": paragraph.style_json,
"is_table": paragraph.is_table,
"table_json": paragraph.table_json,
"edit_mode": paragraph.edit_mode,
"model_id": paragraph.model_id,
"need_prompt": paragraph.need_prompt,
"prompt_text": paragraph.prompt_text,
"need_file": paragraph.need_file,
"file_note": paragraph.file_note,
"output_format": paragraph.output_format,
}
def _serialize_template(template: Template) -> dict:
return {
"id": template.id,
"name": template.name,
"description": template.description,
"file_path": template.file_path,
"paragraph_count": template.paragraph_count,
"status": template.status,
"created_at": template.created_at,
"updated_at": template.updated_at,
}
@router.get("")
async def list_templates(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
keyword: str = Query("", alias="q"),
db: AsyncSession = Depends(get_db),
):
filters = []
if keyword:
filters.append(Template.name.like(f"%{keyword}%"))
total_stmt = select(func.count(Template.id))
list_stmt = select(Template).order_by(Template.id.desc())
if filters:
total_stmt = total_stmt.where(*filters)
list_stmt = list_stmt.where(*filters)
total = (await db.execute(total_stmt)).scalar_one()
result = await db.execute(list_stmt.offset((page - 1) * page_size).limit(page_size))
items = [_serialize_template(item) for item in result.scalars().all()]
return Response(
data={"items": items, "total": total, "page": page, "page_size": page_size}
)
@router.get("/{template_id}")
async def get_template(template_id: int, db: AsyncSession = Depends(get_db)):
template = await db.get(Template, template_id)
if template is None:
raise HTTPException(status_code=404, detail="模板不存在")
result = await db.execute(
select(Paragraph)
.where(Paragraph.template_id == template_id)
.order_by(Paragraph.sort_index.asc(), Paragraph.id.asc())
)
paragraphs = [_serialize_paragraph(item) for item in result.scalars().all()]
payload = _serialize_template(template)
payload["paragraphs"] = paragraphs
return Response(data=payload)
@router.post("/upload")
async def upload_template(file: UploadFile = File(...), db: AsyncSession = Depends(get_db)):
if not file.filename:
raise HTTPException(status_code=400, detail="文件名不能为空")
ext, object_name = _build_object_path(file.filename)
if ext != ".docx":
raise HTTPException(status_code=400, detail="模板仅支持 .docx 格式")
content = await file.read()
if not content:
raise HTTPException(status_code=400, detail="上传文件不能为空")
if len(content) > settings.MAX_UPLOAD_SIZE:
raise HTTPException(status_code=400, detail="文件大小超过限制")
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_file:
temp_file.write(content)
temp_path = temp_file.name
try:
parsed_items = await asyncio.to_thread(parse_template, temp_path)
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
await asyncio.to_thread(
minio_client.put_object,
settings.MINIO_BUCKET_TEMPLATES,
object_name,
BytesIO(content),
len(content),
file.content_type or "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
)
template = Template(
name=os.path.splitext(file.filename)[0],
description="",
file_path=f"{settings.MINIO_BUCKET_TEMPLATES}/{object_name}",
paragraph_count=len(parsed_items),
status="draft",
)
db.add(template)
await db.flush()
paragraph_rows: list[Paragraph] = []
for item in parsed_items:
paragraph = Paragraph(
template_id=template.id,
sort_index=item.sort_index,
title=item.title,
content=item.content,
style_json=item.style_json,
is_table=item.is_table,
table_json=item.table_json,
)
db.add(paragraph)
paragraph_rows.append(paragraph)
await db.commit()
await db.refresh(template)
for paragraph in paragraph_rows:
await db.refresh(paragraph)
payload = _serialize_template(template)
payload["paragraphs"] = [_serialize_paragraph(item) for item in paragraph_rows]
return Response(data=payload)
@router.put("/{template_id}/paragraphs")
async def save_template_paragraphs(
template_id: int,
body: TemplateSave,
db: AsyncSession = Depends(get_db),
):
template = await db.get(Template, template_id)
if template is None:
raise HTTPException(status_code=404, detail="模板不存在")
result = await db.execute(select(Paragraph).where(Paragraph.template_id == template_id))
paragraph_map = {item.id: item for item in result.scalars().all()}
for config in body.paragraphs:
paragraph = paragraph_map.get(config.id)
if paragraph is None:
continue
paragraph.sort_index = config.sort_index
paragraph.title = config.title
paragraph.edit_mode = config.edit_mode
paragraph.model_id = config.model_id
paragraph.need_prompt = config.need_prompt
paragraph.prompt_text = config.prompt_text
paragraph.need_file = config.need_file
paragraph.file_note = config.file_note
paragraph.output_format = config.output_format
await db.commit()
return Response(data={"template_id": template_id, "saved": len(body.paragraphs)})
@router.delete("/{template_id}")
async def delete_template(template_id: int, db: AsyncSession = Depends(get_db)):
template = await db.get(Template, template_id)
if template is None:
raise HTTPException(status_code=404, detail="模板不存在")
result = await db.execute(select(Paragraph).where(Paragraph.template_id == template_id))
for paragraph in result.scalars().all():
await db.delete(paragraph)
file_path = template.file_path or ""
if "/" in file_path:
bucket, object_name = file_path.split("/", 1)
try:
await asyncio.to_thread(minio_client.remove_object, bucket, object_name)
except Exception:
pass
await db.delete(template)
await db.commit()
return Response(data={"id": template_id})