fc / apps /documents /services /generation /base_generator.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
6.57 kB
"""
文书生成器基类
定义所有文书生成器的统一接口和通用方法.
"""
from __future__ import annotations
import logging
import time
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from django.utils.translation import gettext_lazy as _
from apps.core.exceptions import ValidationException
from apps.core.utils.path import Path
from apps.documents.services.placeholders.fallback import (
PLACEHOLDER_FALLBACK_VALUE,
build_docx_render_context,
)
from .result import GenerationResult
if TYPE_CHECKING:
from .context_builder import ContextBuilder
logger = logging.getLogger(__name__)
class BaseGenerator(ABC):
"""
文书生成器基类
所有具体生成器必须继承此类并实现抽象方法.
提供统一的生成接口和通用的模板渲染功能.
"""
# 生成器元信息(子类必须定义)
name: str = "" # 生成器名称(唯一标识)
display_name: str = "" # 显示名称
description: str = "" # 描述
category: str = "general" # 分类:litigation, non_litigation, general
template_type: str = "contract" # 模板类型:contract, case, authorization
def __init__(self, context_builder: ContextBuilder | None = None) -> None:
"""
初始化生成器
Args:
context_builder: 上下文构建器,支持依赖注入
"""
self._context_builder = context_builder
@property
def context_builder(self) -> ContextBuilder:
"""获取上下文构建器,支持延迟加载"""
if self._context_builder is None:
from .context_builder import ContextBuilder
self._context_builder = ContextBuilder()
return self._context_builder
@abstractmethod
def get_required_placeholders(self) -> list[str]:
"""
返回此生成器需要的占位符列表
Returns:
占位符名称列表
"""
pass
@abstractmethod
def generate(self, context: dict[str, Any], template_path: str, output_dir: str) -> GenerationResult:
"""
执行文书生成
Args:
context: 替换词上下文字典
template_path: 模板文件路径
output_dir: 输出目录
Returns:
GenerationResult 生成结果
"""
pass
def validate_context(self, context: dict[str, Any]) -> tuple[bool, list[str]]:
"""
验证上下文是否包含所有必需的占位符
Args:
context: 替换词上下文字典
Returns:
(是否有效, 缺失的占位符列表)
"""
missing: list[Any] = []
required_placeholders = self.get_required_placeholders()
for key in required_placeholders:
if key not in context or context[key] is None:
missing.append(key)
is_valid = not missing
return is_valid, missing
def render_template(self, template_path: str, context: dict[str, Any]) -> bytes:
"""
使用 docxtpl 渲染模板
Args:
template_path: 模板文件路径
context: 替换词上下文字典
Returns:
渲染后的文档内容(字节)
Raises:
ValidationException: 模板文件不存在或渲染失败
"""
try:
import io
from datetime import date
from docxtpl import DocxTemplate
# 验证模板文件存在
if not Path(template_path).exists():
raise ValidationException(
message=_("模板文件不存在: %(p)s") % {"p": template_path},
code="TEMPLATE_NOT_FOUND",
errors={"template_path": f"文件不存在: {template_path}"},
)
# 加载并渲染模板
doc = DocxTemplate(template_path)
render_context = dict(context)
render_context.setdefault("年份", str(date.today().year))
render_context = build_docx_render_context(doc=doc, context=render_context)
render_context.setdefault("年份", PLACEHOLDER_FALLBACK_VALUE)
doc.render(render_context)
# 保存到内存缓冲区
buffer = io.BytesIO()
doc.save(buffer)
return buffer.getvalue()
except ImportError as e:
raise ValidationException(
message=_("docxtpl 库未安装"),
code="DOCXTPL_NOT_INSTALLED",
errors={"dependency": "请安装 docxtpl 库: uv add docxtpl"},
) from e
except Exception as e:
raise ValidationException(
message=_("模板渲染失败: %(e)s") % {"e": e},
code="TEMPLATE_RENDER_ERROR",
errors={"template_path": template_path, "error": str(e)},
) from e
def get_output_filename(self, context: dict[str, Any], template_name: str) -> str:
"""
生成输出文件名
Args:
context: 替换词上下文字典
template_name: 模板名称(不含扩展名)
Returns:
输出文件名
"""
# 尝试从上下文获取合同名称,否则使用默认名称
contract_name = context.get("contract_name", "未命名合同")
# 清理文件名中的非法字符
safe_contract_name = self._sanitize_filename(contract_name)
safe_template_name = self._sanitize_filename(template_name)
return f"{safe_contract_name}_{safe_template_name}.docx"
def _sanitize_filename(self, filename: str) -> str:
"""
清理文件名中的非法字符
Args:
filename: 原始文件名
Returns:
清理后的安全文件名
"""
# 移除或替换非法字符
illegal_chars = ["<", ">", ":", '"', "/", "\\", "|", "?", "*"]
safe_name = filename
for char in illegal_chars:
safe_name = safe_name.replace(char, "_")
# 移除首尾空格和点号
safe_name = safe_name.strip(" .")
# 如果文件名为空,使用默认名称
if not safe_name:
safe_name = "document"
return safe_name
def __str__(self) -> str:
"""字符串表示"""
return f"{self.display_name or self.name} ({self.category})"
def __repr__(self) -> str:
"""调试表示"""
return f"<{self.__class__.__name__}: {self.name}>"