实现模板解析与模板管理基础链路
This commit is contained in:
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user