From ab3d5206424dbc2ea61e12a920b11bf3328e31f6 Mon Sep 17 00:00:00 2001 From: bruce Date: Thu, 4 Jun 2026 02:41:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=81=E4=B9=85=E5=8C=96=E8=B5=84?= =?UTF-8?q?=E6=96=99=E5=8C=85=E5=AF=BC=E5=87=BA=E8=AE=B0=E5=BD=95=E4=B8=8E?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=91=98=E8=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/chat/export_service.py | 16 ++++- apps/documents/admin.py | 18 ++++- .../migrations/0003_exporteddocument.py | 52 ++++++++++++++ apps/documents/models.py | 33 +++++++++ apps/documents/services.py | 51 +++++++++++++- apps/documents/views.py | 3 +- templates/documents/document_list.html | 15 ++++- tests/test_chat.py | 3 +- tests/test_documents.py | 67 ++++++++++++++++++- 9 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 apps/documents/migrations/0003_exporteddocument.py diff --git a/apps/chat/export_service.py b/apps/chat/export_service.py index 5df2300..884f2b5 100644 --- a/apps/chat/export_service.py +++ b/apps/chat/export_service.py @@ -8,6 +8,7 @@ import zipfile from django.conf import settings from agent_core.governance import load_governance_config +from apps.documents.services import create_export_record def generate_registration_export(*, batch, conversation, upstream_summary: dict | None = None) -> dict: @@ -48,7 +49,7 @@ def generate_registration_export(*, batch, conversation, upstream_summary: dict if can_export_formally else "已生成草稿导出文件,正式版仍被风险项阻断。" ) - return { + report = { "output_type": "registration_word_export_report", "summary": summary, "template_name": template_mapping["template_name"], @@ -72,6 +73,19 @@ def generate_registration_export(*, batch, conversation, upstream_summary: dict "export_mode": export_mode, }, } + create_export_record( + batch=batch, + conversation_id=conversation.conversation_id, + product_name=batch.product_name or conversation.product_name, + template_name=report["template_name"], + template_version=report["template_version"], + export_mode=export_mode, + output_type=report["output_type"], + file_name=file_name, + relative_path=relative_path.as_posix(), + download_url=download_url, + ) + return report def update_conversation_with_export_report(conversation, export_report: dict) -> None: diff --git a/apps/documents/admin.py b/apps/documents/admin.py index 872bd43..0a746e6 100644 --- a/apps/documents/admin.py +++ b/apps/documents/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import UploadedDocument +from .models import ExportedDocument, UploadedDocument @admin.register(UploadedDocument) @@ -9,3 +9,19 @@ class UploadedDocumentAdmin(admin.ModelAdmin): list_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at") list_filter = ("status", "scenario_id", "file_type") search_fields = ("original_name", "scenario_id") + + +@admin.register(ExportedDocument) +class ExportedDocumentAdmin(admin.ModelAdmin): + """管理导出记录,便于按批次、会话和产品回看导出产物。""" + list_display = ( + "id", + "file_name", + "batch", + "conversation_id", + "product_name", + "export_mode", + "created_at", + ) + list_filter = ("export_mode", "output_type", "template_name") + search_fields = ("file_name", "batch__batch_id", "conversation_id", "product_name") diff --git a/apps/documents/migrations/0003_exporteddocument.py b/apps/documents/migrations/0003_exporteddocument.py new file mode 100644 index 0000000..d40f0f8 --- /dev/null +++ b/apps/documents/migrations/0003_exporteddocument.py @@ -0,0 +1,52 @@ +# Generated by Django 5.2.14 on 2026-06-04 18:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("documents", "0002_submissionbatch_uploadeddocument_chapter_code_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ExportedDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("conversation_id", models.CharField(db_index=True, max_length=64)), + ("product_name", models.CharField(blank=True, db_index=True, max_length=255)), + ("template_name", models.CharField(blank=True, max_length=100)), + ("template_version", models.CharField(blank=True, max_length=50)), + ("export_mode", models.CharField(db_index=True, max_length=32)), + ( + "output_type", + models.CharField(default="registration_word_export_report", max_length=100), + ), + ("file_name", models.CharField(max_length=255)), + ("relative_path", models.CharField(max_length=500)), + ("download_url", models.CharField(blank=True, max_length=500)), + ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), + ( + "batch", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="export_records", + to="documents.submissionbatch", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + ] diff --git a/apps/documents/models.py b/apps/documents/models.py index b33fb3f..a40b77a 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -92,3 +92,36 @@ class UploadedDocument(models.Model): self.STATUS_INDEXED: "已入库,可检索", self.STATUS_FAILED: "入库失败", }.get(self.status, self.status) + + +class ExportedDocument(models.Model): + """ + 导出文件记录。 + + 该对象属于资料包治理范围: + - Documents 维护导出产物与资料包关系 + - Chat 只负责触发导出动作 + - Audit 负责回看执行痕迹 + """ + + batch = models.ForeignKey( + SubmissionBatch, + related_name="export_records", + on_delete=models.CASCADE, + ) + conversation_id = models.CharField(max_length=64, db_index=True) + product_name = models.CharField(max_length=255, blank=True, db_index=True) + template_name = models.CharField(max_length=100, blank=True) + template_version = models.CharField(max_length=50, blank=True) + export_mode = models.CharField(max_length=32, db_index=True) + output_type = models.CharField(max_length=100, default="registration_word_export_report") + file_name = models.CharField(max_length=255) + relative_path = models.CharField(max_length=500) + download_url = models.CharField(max_length=500, blank=True) + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return self.file_name diff --git a/apps/documents/services.py b/apps/documents/services.py index ddbcab7..5795d7e 100644 --- a/apps/documents/services.py +++ b/apps/documents/services.py @@ -9,7 +9,7 @@ from agent_core.rag.ingest import ingest_document from apps.chat.services import create_conversation_for_batch from django.core.files.uploadedfile import SimpleUploadedFile -from .models import SubmissionBatch, UploadedDocument +from .models import ExportedDocument, SubmissionBatch, UploadedDocument def create_uploaded_document( @@ -198,6 +198,55 @@ def append_documents_to_batch( } +def create_export_record( + *, + batch: SubmissionBatch, + conversation_id: str, + product_name: str, + template_name: str, + template_version: str, + export_mode: str, + output_type: str, + file_name: str, + relative_path: str, + download_url: str, +) -> ExportedDocument: + """ + 保存导出文件记录,供资料包与处理历史统一回看。 + """ + return ExportedDocument.objects.create( + batch=batch, + conversation_id=conversation_id, + product_name=product_name, + template_name=template_name, + template_version=template_version, + export_mode=export_mode, + output_type=output_type, + file_name=file_name, + relative_path=relative_path, + download_url=download_url, + ) + + +def build_batch_rows(batches) -> list[dict]: + """ + 为资料包列表补齐最近导出摘要。 + """ + batch_ids = [batch.id for batch in batches] + latest_exports = {} + for record in ExportedDocument.objects.filter(batch_id__in=batch_ids).order_by("batch_id", "-created_at"): + latest_exports.setdefault(record.batch_id, record) + rows = [] + for batch in batches: + rows.append( + { + "batch": batch, + "latest_export": latest_exports.get(batch.id), + } + ) + return rows + + def extract_text(document: UploadedDocument) -> str: """ 根据文档类型选择合适的文本抽取策略。 diff --git a/apps/documents/views.py b/apps/documents/views.py index 8056a9f..5841921 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -7,7 +7,7 @@ from apps.scenarios.services import list_scenarios from .forms import DocumentUploadForm from .models import SubmissionBatch, UploadedDocument -from .services import import_submission_batch, index_document +from .services import build_batch_rows, import_submission_batch, index_document def document_list(request): @@ -45,6 +45,7 @@ def document_list(request): { "documents": documents, "batches": batches, + "batch_rows": build_batch_rows(batches), "keyword": keyword, "status_counts": status_counts, "processing_pipeline": processing_pipeline, diff --git a/templates/documents/document_list.html b/templates/documents/document_list.html index 072722d..be3a45c 100644 --- a/templates/documents/document_list.html +++ b/templates/documents/document_list.html @@ -67,11 +67,13 @@ 文件数 页数 状态 + 最近导出 章节点概览 - {% for batch in batches %} + {% for row in batch_rows %} + {% with batch=row.batch latest_export=row.latest_export %} {{ batch.batch_id }} {{ batch.product_name|default:"未识别产品名称" }} @@ -89,6 +91,14 @@ {{ batch.get_import_status_display_text }} + + {% if latest_export %} +
{{ latest_export.file_name }}
+
{{ latest_export.export_mode }} / {{ latest_export.template_version|default:"-" }}
+ {% else %} + 暂无导出记录 + {% endif %} + {% if batch.chapter_summary %} {% for chapter in batch.chapter_summary %} @@ -99,9 +109,10 @@ {% endif %} + {% endwith %} {% empty %} - 暂无资料包,请先导入申报资料。 + 暂无资料包,请先导入申报资料。 {% endfor %} diff --git a/tests/test_chat.py b/tests/test_chat.py index 9e38605..a9cfe6f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -9,7 +9,7 @@ from apps.audit.models import AgentAuditLog from apps.audit.models import NotificationRecord from apps.chat.models import Conversation from apps.chat.services import create_conversation_for_batch -from apps.documents.models import SubmissionBatch, UploadedDocument +from apps.documents.models import ExportedDocument, SubmissionBatch, UploadedDocument def _create_conversation_with_batch(): @@ -547,3 +547,4 @@ def test_chat_export_word_route_persists_real_download_link(client, db, tmp_path assert "下载导出文件" in content assert export_report["download_url"] in content assert AgentAuditLog.objects.filter(conversation_id=conversation.conversation_id).count() == 1 + assert ExportedDocument.objects.filter(batch=batch, conversation_id=conversation.conversation_id).count() == 1 diff --git a/tests/test_documents.py b/tests/test_documents.py index 7b4bd9f..b91c953 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -7,7 +7,7 @@ import types from zipfile import ZipFile from apps.documents.forms import DocumentUploadForm -from apps.documents.models import SubmissionBatch, UploadedDocument +from apps.documents.models import ExportedDocument, SubmissionBatch, UploadedDocument from apps.documents.services import extract_text, import_submission_batch, index_document from apps.chat.models import Conversation @@ -475,3 +475,68 @@ def test_import_submission_batch_records_warnings_for_unsupported_7z_entries(db, assert batch.file_count == 1 assert batch.exception_count == 1 assert any("CH1/忽略图片.png" in warning for warning in warnings) + + +def test_create_export_record_persists_batch_conversation_and_file_metadata(db): + from apps.documents.services import create_export_record + + batch = SubmissionBatch.objects.create( + batch_id="SUB-20260604-010", + product_name="产品X", + workflow_type="registration", + conversation_id="conv-010", + file_count=2, + page_count=12, + import_status="completed", + ) + + record = create_export_record( + batch=batch, + conversation_id="conv-010", + product_name="产品X", + template_name="注册证导出模板", + template_version="V1.0", + export_mode="draft", + output_type="registration_word_export_report", + file_name="SUB-20260604-010-draft.docx", + relative_path="exports/20260604/SUB-20260604-010-draft.docx", + download_url="/media/exports/20260604/SUB-20260604-010-draft.docx", + ) + + assert ExportedDocument.objects.count() == 1 + assert record.batch == batch + assert record.conversation_id == "conv-010" + assert record.product_name == "产品X" + assert record.template_name == "注册证导出模板" + assert record.export_mode == "draft" + + +def test_document_list_shows_latest_export_record_for_batch(client, db): + batch = SubmissionBatch.objects.create( + batch_id="SUB-20260604-011", + product_name="产品Y", + workflow_type="registration", + conversation_id="conv-011", + file_count=2, + page_count=12, + import_status="completed", + ) + ExportedDocument.objects.create( + batch=batch, + conversation_id="conv-011", + product_name="产品Y", + template_name="注册证导出模板", + template_version="V1.0", + export_mode="draft", + output_type="registration_word_export_report", + file_name="SUB-20260604-011-draft.docx", + relative_path="exports/20260604/SUB-20260604-011-draft.docx", + download_url="/media/exports/20260604/SUB-20260604-011-draft.docx", + ) + + response = client.get(reverse("documents:list")) + + content = response.content.decode("utf-8") + assert response.status_code == 200 + assert "最近导出" in content + assert "SUB-20260604-011-draft.docx" in content