fc / apps /documents /api /external_template_api.py
jun2024's picture
Upload 2241 files
b4f2238 verified
Raw History Blame Contribute Delete
11.3 kB
"""
外部模板 API
提供外部模板分析、填充、匹配等接口.
"""
from __future__ import annotations
import logging
from typing import Any
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext_lazy as _
from ninja import Router, Schema
from apps.core.security.auth import JWTOrSessionAuth
logger = logging.getLogger("apps.documents.api")
router = Router(auth=JWTOrSessionAuth())
# ---------------------------------------------------------------------------
# Factory functions
# ---------------------------------------------------------------------------
def _get_analysis_service() -> Any:
from apps.documents.services.infrastructure.wiring import get_analysis_service
return get_analysis_service()
def _get_filling_service() -> Any:
from apps.documents.services.infrastructure.wiring import get_filling_service
return get_filling_service()
def _get_matching_service() -> Any:
from apps.documents.services.infrastructure.wiring import get_matching_service
return get_matching_service()
# ---------------------------------------------------------------------------
# Request schemas
# ---------------------------------------------------------------------------
class FillRequestSchema(Schema):
"""填充请求体"""
template_ids: list[int]
case_id: int
party_ids: list[int] | None = None
custom_values: dict[str, dict[str, str]] | None = None
class MappingUpdateSchema(Schema):
"""映射更新请求体"""
semantic_label: str | None = None
fill_type: str | None = None
position_description: str | None = None
class MappingCreateSchema(Schema):
"""映射创建请求体"""
position_locator: dict[str, Any]
position_description: str = ""
semantic_label: str
fill_type: str = "text"
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@router.post("/{template_id}/analyze")
def analyze_template(request: HttpRequest, template_id: int) -> dict[str, Any]:
"""触发/重新触发 LLM 分析"""
service = _get_analysis_service()
mappings = service.analyze_template(template_id)
logger.info(
"模板分析完成",
extra={"template_id": template_id, "mapping_count": len(mappings)},
)
return {
"success": True,
"template_id": template_id,
"mapping_count": len(mappings),
}
@router.post("/{template_id}/confirm")
def confirm_mappings(request: HttpRequest, template_id: int) -> dict[str, Any]:
"""确认字段映射"""
service = _get_analysis_service()
service.confirm_mappings(template_id)
logger.info("映射已确认", extra={"template_id": template_id})
return {"success": True, "template_id": template_id}
@router.post("/fill")
def fill_templates(request: HttpRequest, payload: FillRequestSchema) -> dict[str, Any] | HttpResponse:
"""执行填充(单个或批量),返回文件信息"""
service = _get_filling_service()
user = getattr(request, "auth", None) or getattr(request, "user", None)
batch_task = service.batch_fill(
case_id=payload.case_id,
template_ids=payload.template_ids,
party_ids=payload.party_ids,
custom_values=payload.custom_values,
filled_by=user,
)
records = list(batch_task.records.values_list("id", "original_output_name", "file_path", flat=False))
logger.info(
"填充完成",
extra={"batch_task_id": batch_task.id, "record_count": len(records)},
)
return {
"success": True,
"batch_task_id": batch_task.id,
"zip_file_path": batch_task.zip_file_path,
"summary": batch_task.summary_json,
"records": [{"id": r[0], "filename": r[1], "file_path": r[2]} for r in records],
}
@router.get("/{template_id}/preview")
def preview_fill(
request: HttpRequest,
template_id: int,
case_id: int,
party_id: int | None = None,
) -> dict[str, Any]:
"""填充预览"""
service = _get_filling_service()
items = service.generate_preview(
template_id=template_id,
case_id=case_id,
party_id=party_id,
)
return {
"template_id": template_id,
"case_id": case_id,
"party_id": party_id,
"fields": [
{
"position_description": item.position_description,
"semantic_label": item.semantic_label,
"fill_value": item.fill_value,
"value_source": item.value_source,
"fill_type": item.fill_type,
"mapping_id": item.mapping_id,
}
for item in items
],
}
@router.get("/match")
def match_templates(
request: HttpRequest,
case_id: int | None = None,
source_name: str | None = None,
) -> dict[str, Any]:
"""模板匹配推荐"""
service = _get_matching_service()
user = getattr(request, "auth", None) or getattr(request, "user", None)
law_firm_id: int | None = getattr(user, "law_firm_id", None)
if law_firm_id is None:
return {"success": False, "message": str(_("无法确定所属律所"))}
if case_id is not None:
results = service.match_by_case(case_id=case_id, law_firm_id=law_firm_id)
elif source_name is not None:
results = service.match_by_source_name(
source_name=source_name,
law_firm_id=law_firm_id,
)
else:
return {"success": False, "message": str(_("请提供 case_id 或 source_name 参数"))}
return {
"success": True,
"results": [{"id": t.id, "name": t.name, "status": t.status, "version": t.version} for t in results],
}
@router.get("/{template_id}/custom-fields")
def get_custom_fields(request: HttpRequest, template_id: int) -> dict[str, Any]:
"""获取需手动输入的自定义字段"""
service = _get_filling_service()
fields = service.get_custom_fields(template_id)
return {"template_id": template_id, "fields": fields}
@router.get("/history")
def get_fill_history(
request: HttpRequest,
case_id: int | None = None,
template_id: int | None = None,
) -> dict[str, Any]:
"""填充历史查询"""
service = _get_filling_service()
if case_id is not None:
qs = service.get_fill_history_by_case(case_id)
elif template_id is not None:
qs = service.get_fill_history_by_template(template_id)
else:
return {"success": False, "message": str(_("请提供 case_id 或 template_id 参数"))}
records = list(
qs.values(
"id",
"case_id",
"template_id",
"party_id",
"filled_at",
"original_output_name",
"file_available",
)
)
return {"success": True, "records": records}
@router.get("/statistics")
def get_statistics(request: HttpRequest) -> dict[str, Any]:
"""模板统计"""
service = _get_matching_service()
user = getattr(request, "auth", None) or getattr(request, "user", None)
law_firm_id: int | None = getattr(user, "law_firm_id", None)
if law_firm_id is None:
return {"success": False, "message": str(_("无法确定所属律所"))}
stats = service.get_template_statistics(law_firm_id)
return {"success": True, "statistics": stats}
# ---------------------------------------------------------------------------
# 预览与映射编辑 API
# ---------------------------------------------------------------------------
@router.get("/{template_id}/preview-html")
def get_preview_html(request: HttpRequest, template_id: int) -> dict[str, Any]:
"""将 docx 转为 HTML 用于预览"""
from pathlib import Path
import mammoth
from django.conf import settings
from apps.documents.models.external_template import ExternalTemplate
template = ExternalTemplate.objects.get(pk=template_id)
abs_path = Path(settings.MEDIA_ROOT) / template.file_path
with abs_path.open("rb") as f:
result = mammoth.convert_to_html(f)
return {
"html": result.value,
"messages": [str(m) for m in result.messages],
}
@router.get("/{template_id}/mappings")
def list_mappings(request: HttpRequest, template_id: int) -> list[dict[str, Any]]:
"""获取模板的所有字段映射"""
from apps.documents.models.external_template import ExternalTemplateFieldMapping
mappings = ExternalTemplateFieldMapping.objects.filter(template_id=template_id).order_by("sort_order", "id")
return [
{
"id": m.id,
"position_locator": m.position_locator,
"position_description": m.position_description,
"semantic_label": m.semantic_label,
"fill_type": m.fill_type,
"sort_order": m.sort_order,
}
for m in mappings
]
@router.post("/{template_id}/mappings")
def create_mapping(request: HttpRequest, template_id: int, payload: MappingCreateSchema) -> dict[str, Any]:
"""手动添加字段映射"""
service = _get_analysis_service()
m = service.create_manual_mapping(
template_id=template_id,
position_locator=payload.position_locator,
position_description=payload.position_description,
semantic_label=payload.semantic_label,
fill_type=payload.fill_type,
)
logger.info("手动添加映射: template_id=%d, mapping_id=%d", template_id, m.id)
return {
"id": m.id,
"position_locator": m.position_locator,
"position_description": m.position_description,
"semantic_label": m.semantic_label,
"fill_type": m.fill_type,
"sort_order": m.sort_order,
}
@router.put("/mappings/{mapping_id}")
def update_mapping(request: HttpRequest, mapping_id: int, payload: MappingUpdateSchema) -> dict[str, Any]:
"""更新字段映射"""
from apps.documents.models.external_template import ExternalTemplateFieldMapping
m = ExternalTemplateFieldMapping.objects.get(pk=mapping_id)
update_fields: list[str] = ["updated_at"]
if payload.semantic_label is not None:
m.semantic_label = payload.semantic_label
update_fields.append("semantic_label")
if payload.fill_type is not None:
m.fill_type = payload.fill_type
update_fields.append("fill_type")
if payload.position_description is not None:
m.position_description = payload.position_description
update_fields.append("position_description")
m.save(update_fields=update_fields)
logger.info("更新映射: mapping_id=%d", mapping_id)
return {
"id": m.id,
"position_locator": m.position_locator,
"position_description": m.position_description,
"semantic_label": m.semantic_label,
"fill_type": m.fill_type,
"sort_order": m.sort_order,
}
@router.delete("/mappings/{mapping_id}")
def delete_mapping(request: HttpRequest, mapping_id: int) -> dict[str, bool]:
"""删除字段映射"""
service = _get_analysis_service()
service.delete_mapping(mapping_id)
logger.info("删除映射: mapping_id=%d", mapping_id)
return {"success": True}