支持段落删除与移动排序,修复导出残留与外键约束
- 前端:段落列表/配置区/手动编辑区新增上移、下移、删除按钮,hover 显示 - 前端:移动和删除操作后自动保存到后端,修正 canDeleteBlock 判定逻辑 - 后端:保存段落时级联删除关联的 generation_logs 再删段落 - 后端:导出时清理未被引用的标题段落及其内容,避免已删段落残留在 Word 中 - 后端:删除模板时级联清理 generation_logs/documents/paragraphs
This commit is contained in:
@@ -6,11 +6,13 @@ from datetime import datetime
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from config import settings
|
||||
from database import get_db
|
||||
from models.document import Document
|
||||
from models.generation_log import GenerationLog
|
||||
from models.paragraph import Paragraph
|
||||
from models.template import Template
|
||||
from schemas.schemas import Response, TemplateSave
|
||||
@@ -194,8 +196,14 @@ async def save_template_paragraphs(
|
||||
paragraph_map = {item.id: item for item in existing_paragraphs}
|
||||
incoming_ids = {config.id for config in body.paragraphs if config.id}
|
||||
|
||||
print(f"[SAVE] template_id={template_id}, incoming_ids={incoming_ids}, existing_ids={[p.id for p in existing_paragraphs]}")
|
||||
|
||||
for paragraph in existing_paragraphs:
|
||||
if paragraph.id not in incoming_ids:
|
||||
print(f"[SAVE] Deleting paragraph id={paragraph.id} title={paragraph.title}")
|
||||
await db.execute(
|
||||
delete(GenerationLog).where(GenerationLog.paragraph_id == paragraph.id)
|
||||
)
|
||||
await db.delete(paragraph)
|
||||
|
||||
for index, config in enumerate(body.paragraphs, start=1):
|
||||
@@ -229,7 +237,23 @@ async def delete_template(template_id: int, db: AsyncSession = Depends(get_db)):
|
||||
raise HTTPException(status_code=404, detail="模板不存在")
|
||||
|
||||
result = await db.execute(select(Paragraph).where(Paragraph.template_id == template_id))
|
||||
for paragraph in result.scalars().all():
|
||||
paragraphs_to_delete = result.scalars().all()
|
||||
paragraph_ids = [p.id for p in paragraphs_to_delete]
|
||||
|
||||
doc_result = await db.execute(select(Document).where(Document.template_id == template_id))
|
||||
documents_to_delete = doc_result.scalars().all()
|
||||
|
||||
if paragraph_ids:
|
||||
await db.execute(
|
||||
delete(GenerationLog).where(GenerationLog.paragraph_id.in_(paragraph_ids))
|
||||
)
|
||||
for document in documents_to_delete:
|
||||
await db.execute(
|
||||
delete(GenerationLog).where(GenerationLog.document_id == document.id)
|
||||
)
|
||||
await db.delete(document)
|
||||
|
||||
for paragraph in paragraphs_to_delete:
|
||||
await db.delete(paragraph)
|
||||
|
||||
file_path = template.file_path or ""
|
||||
|
||||
@@ -32,6 +32,43 @@ def _delete_block(block):
|
||||
parent.remove(element)
|
||||
|
||||
|
||||
def _delete_heading_section(heading: Paragraph):
|
||||
blocks = [heading]
|
||||
current = heading._element.getnext()
|
||||
while current is not None:
|
||||
if isinstance(current, CT_P):
|
||||
para = Paragraph(current, heading._parent)
|
||||
if _is_heading(para):
|
||||
break
|
||||
blocks.append(para)
|
||||
elif isinstance(current, CT_Tbl):
|
||||
blocks.append(Table(current, heading._parent))
|
||||
current = current.getnext()
|
||||
for block in blocks:
|
||||
_delete_block(block)
|
||||
|
||||
|
||||
def _remove_unreferenced_headings(document: DocumentObject, referenced_anchors: set[str]):
|
||||
headings_to_remove: list[Paragraph] = []
|
||||
found_first_heading = False
|
||||
pre_heading_blocks: list = []
|
||||
print(f"[EXPORT] referenced_anchors: {referenced_anchors}")
|
||||
for block in _iter_block_items(document):
|
||||
if isinstance(block, Paragraph) and _is_heading(block):
|
||||
found_first_heading = True
|
||||
text = block.text.strip()
|
||||
if text not in referenced_anchors:
|
||||
print(f"[EXPORT] Unreferenced heading found, will remove: '{text}'")
|
||||
headings_to_remove.append(block)
|
||||
elif not found_first_heading:
|
||||
pre_heading_blocks.append(block)
|
||||
for heading in headings_to_remove:
|
||||
_delete_heading_section(heading)
|
||||
if not referenced_anchors:
|
||||
for block in pre_heading_blocks:
|
||||
_delete_block(block)
|
||||
|
||||
|
||||
def _clear_paragraph(paragraph: Paragraph):
|
||||
element = paragraph._element
|
||||
for child in list(element):
|
||||
@@ -332,10 +369,22 @@ def _replace_section_group(
|
||||
|
||||
def export_document_bytes(template_bytes: bytes, logs: list[dict]) -> bytes:
|
||||
document = Document(BytesIO(template_bytes))
|
||||
|
||||
referenced_anchors: set[str] = set()
|
||||
for item in logs:
|
||||
for key in ("anchor_title", "title"):
|
||||
val = (item.get(key) or "").strip()
|
||||
if val:
|
||||
referenced_anchors.add(val)
|
||||
|
||||
print(f"[EXPORT] logs count={len(logs)}, anchor_titles={[(l.get('anchor_title'), l.get('title')) for l in logs]}")
|
||||
|
||||
last_heading_element = None
|
||||
for group in _group_logs(logs):
|
||||
last_heading_element = _replace_section_group(document, group, last_heading_element)
|
||||
|
||||
_remove_unreferenced_headings(document, referenced_anchors)
|
||||
|
||||
output = BytesIO()
|
||||
document.save(output)
|
||||
return output.getvalue()
|
||||
|
||||
Reference in New Issue
Block a user