feat: 持久化资料包导出记录与列表摘要
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
52
apps/documents/migrations/0003_exporteddocument.py
Normal file
52
apps/documents/migrations/0003_exporteddocument.py
Normal 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"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
根据文档类型选择合适的文本抽取策略。
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -67,11 +67,13 @@
|
||||
<th>文件数</th>
|
||||
<th>页数</th>
|
||||
<th>状态</th>
|
||||
<th>最近导出</th>
|
||||
<th>章节点概览</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for batch in batches %}
|
||||
{% for row in batch_rows %}
|
||||
{% with batch=row.batch latest_export=row.latest_export %}
|
||||
<tr>
|
||||
<td class="nowrap">{{ batch.batch_id }}</td>
|
||||
<td>{{ batch.product_name|default:"未识别产品名称" }}</td>
|
||||
@@ -89,6 +91,14 @@
|
||||
{{ batch.get_import_status_display_text }}
|
||||
</span>
|
||||
</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">
|
||||
{% if batch.chapter_summary %}
|
||||
{% for chapter in batch.chapter_summary %}
|
||||
@@ -99,9 +109,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7">暂无资料包,请先导入申报资料。</td>
|
||||
<td colspan="8">暂无资料包,请先导入申报资料。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user