feat: 持久化资料包导出记录与列表摘要

This commit is contained in:
2026-06-04 02:41:49 +08:00
parent 0e49466746
commit ab3d520642
9 changed files with 250 additions and 8 deletions

View File

@@ -8,6 +8,7 @@ import zipfile
from django.conf import settings from django.conf import settings
from agent_core.governance import load_governance_config 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: 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 if can_export_formally
else "已生成草稿导出文件,正式版仍被风险项阻断。" else "已生成草稿导出文件,正式版仍被风险项阻断。"
) )
return { report = {
"output_type": "registration_word_export_report", "output_type": "registration_word_export_report",
"summary": summary, "summary": summary,
"template_name": template_mapping["template_name"], "template_name": template_mapping["template_name"],
@@ -72,6 +73,19 @@ def generate_registration_export(*, batch, conversation, upstream_summary: dict
"export_mode": export_mode, "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: def update_conversation_with_export_report(conversation, export_report: dict) -> None:

View File

@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import UploadedDocument from .models import ExportedDocument, UploadedDocument
@admin.register(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_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at")
list_filter = ("status", "scenario_id", "file_type") list_filter = ("status", "scenario_id", "file_type")
search_fields = ("original_name", "scenario_id") 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")

View File

@@ -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"],
},
),
]

View File

@@ -92,3 +92,36 @@ class UploadedDocument(models.Model):
self.STATUS_INDEXED: "已入库,可检索", self.STATUS_INDEXED: "已入库,可检索",
self.STATUS_FAILED: "入库失败", self.STATUS_FAILED: "入库失败",
}.get(self.status, self.status) }.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

View File

@@ -9,7 +9,7 @@ from agent_core.rag.ingest import ingest_document
from apps.chat.services import create_conversation_for_batch from apps.chat.services import create_conversation_for_batch
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from .models import SubmissionBatch, UploadedDocument from .models import ExportedDocument, SubmissionBatch, UploadedDocument
def create_uploaded_document( 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: def extract_text(document: UploadedDocument) -> str:
""" """
根据文档类型选择合适的文本抽取策略。 根据文档类型选择合适的文本抽取策略。

View File

@@ -7,7 +7,7 @@ from apps.scenarios.services import list_scenarios
from .forms import DocumentUploadForm from .forms import DocumentUploadForm
from .models import SubmissionBatch, UploadedDocument 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): def document_list(request):
@@ -45,6 +45,7 @@ def document_list(request):
{ {
"documents": documents, "documents": documents,
"batches": batches, "batches": batches,
"batch_rows": build_batch_rows(batches),
"keyword": keyword, "keyword": keyword,
"status_counts": status_counts, "status_counts": status_counts,
"processing_pipeline": processing_pipeline, "processing_pipeline": processing_pipeline,

View File

@@ -67,11 +67,13 @@
<th>文件数</th> <th>文件数</th>
<th>页数</th> <th>页数</th>
<th>状态</th> <th>状态</th>
<th>最近导出</th>
<th>章节点概览</th> <th>章节点概览</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for batch in batches %} {% for row in batch_rows %}
{% with batch=row.batch latest_export=row.latest_export %}
<tr> <tr>
<td class="nowrap">{{ batch.batch_id }}</td> <td class="nowrap">{{ batch.batch_id }}</td>
<td>{{ batch.product_name|default:"未识别产品名称" }}</td> <td>{{ batch.product_name|default:"未识别产品名称" }}</td>
@@ -89,6 +91,14 @@
{{ batch.get_import_status_display_text }} {{ batch.get_import_status_display_text }}
</span> </span>
</td> </td>
<td class="cell-min-220">
{% if latest_export %}
<div>{{ latest_export.file_name }}</div>
<div class="muted">{{ latest_export.export_mode }} / {{ latest_export.template_version|default:"-" }}</div>
{% else %}
<span class="muted">暂无导出记录</span>
{% endif %}
</td>
<td class="cell-min-280"> <td class="cell-min-280">
{% if batch.chapter_summary %} {% if batch.chapter_summary %}
{% for chapter in batch.chapter_summary %} {% for chapter in batch.chapter_summary %}
@@ -99,9 +109,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endwith %}
{% empty %} {% empty %}
<tr> <tr>
<td colspan="7">暂无资料包,请先导入申报资料。</td> <td colspan="8">暂无资料包,请先导入申报资料。</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -9,7 +9,7 @@ from apps.audit.models import AgentAuditLog
from apps.audit.models import NotificationRecord from apps.audit.models import NotificationRecord
from apps.chat.models import Conversation from apps.chat.models import Conversation
from apps.chat.services import create_conversation_for_batch 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(): 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 "下载导出文件" in content
assert export_report["download_url"] in content assert export_report["download_url"] in content
assert AgentAuditLog.objects.filter(conversation_id=conversation.conversation_id).count() == 1 assert AgentAuditLog.objects.filter(conversation_id=conversation.conversation_id).count() == 1
assert ExportedDocument.objects.filter(batch=batch, conversation_id=conversation.conversation_id).count() == 1

View File

@@ -7,7 +7,7 @@ import types
from zipfile import ZipFile from zipfile import ZipFile
from apps.documents.forms import DocumentUploadForm 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.documents.services import extract_text, import_submission_batch, index_document
from apps.chat.models import Conversation 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.file_count == 1
assert batch.exception_count == 1 assert batch.exception_count == 1
assert any("CH1/忽略图片.png" in warning for warning in warnings) 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