225 lines
7.4 KiB
Python
225 lines
7.4 KiB
Python
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})
|