update for 1. 优化 2.中英切换
This commit is contained in:
101
backend/app/infrastructure/compliance/docx_export.py
Normal file
101
backend/app/infrastructure/compliance/docx_export.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""DOCX report generator for compliance analysis results.
|
||||
|
||||
Uses python-docx (already in requirements.txt). Returns raw bytes so the
|
||||
caller can stream the response without writing to disk.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from io import BytesIO
|
||||
|
||||
from docx import Document
|
||||
from docx.shared import Pt, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
|
||||
from app.domain.compliance.ports import AnalysisRecord
|
||||
|
||||
_STATUS_LABEL = {"ok": "Compliant", "warn": "Warning", "risk": "Non-Compliant"}
|
||||
_STATUS_COLOR = {
|
||||
"ok": RGBColor(0x22, 0x8B, 0x22),
|
||||
"warn": RGBColor(0xFF, 0x8C, 0x00),
|
||||
"risk": RGBColor(0xDC, 0x14, 0x3C),
|
||||
}
|
||||
|
||||
|
||||
def generate_docx(record: AnalysisRecord) -> bytes:
|
||||
"""Generate a compliance report DOCX and return its raw bytes.
|
||||
|
||||
Structure:
|
||||
- Cover: document name, standard, date, risk score
|
||||
- Executive summary (conclusion)
|
||||
- Findings table
|
||||
- Recommended actions
|
||||
- Footer note
|
||||
"""
|
||||
doc = Document()
|
||||
|
||||
# ── Cover ──────────────────────────────────────────────────────────────────
|
||||
title_para = doc.add_heading("Compliance Analysis Report", level=0)
|
||||
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
doc.add_paragraph("")
|
||||
meta_table = doc.add_table(rows=4, cols=2)
|
||||
meta_table.style = "Table Grid"
|
||||
labels = ["Document", "Standard", "Date", "Risk Score"]
|
||||
values = [
|
||||
record.doc_name,
|
||||
record.standard_name,
|
||||
record.created_at.strftime("%Y-%m-%d %H:%M UTC") if record.created_at else "",
|
||||
f"{record.risk_score} / 100",
|
||||
]
|
||||
for i, (label, value) in enumerate(zip(labels, values)):
|
||||
meta_table.cell(i, 0).text = label
|
||||
meta_table.cell(i, 1).text = value
|
||||
|
||||
# ── Executive Summary ──────────────────────────────────────────────────────
|
||||
doc.add_heading("Executive Summary", level=1)
|
||||
doc.add_paragraph(record.conclusion)
|
||||
|
||||
# ── Findings ───────────────────────────────────────────────────────────────
|
||||
doc.add_heading("Findings", level=1)
|
||||
if record.findings:
|
||||
table = doc.add_table(rows=1, cols=4)
|
||||
table.style = "Table Grid"
|
||||
hdr = table.rows[0].cells
|
||||
for i, h in enumerate(["#", "Status", "Title", "Description / Clause"]):
|
||||
hdr[i].text = h
|
||||
for run in hdr[i].paragraphs[0].runs:
|
||||
run.bold = True
|
||||
|
||||
for f in record.findings:
|
||||
row = table.add_row().cells
|
||||
row[0].text = str(f.seq + 1)
|
||||
row[1].text = _STATUS_LABEL.get(f.status, f.status)
|
||||
row[2].text = f.title
|
||||
desc = f.description
|
||||
if f.clause_ref:
|
||||
desc += f"\n[{f.clause_ref}]"
|
||||
row[3].text = desc
|
||||
else:
|
||||
doc.add_paragraph("No findings recorded.")
|
||||
|
||||
# ── Recommended Actions ────────────────────────────────────────────────────
|
||||
doc.add_heading("Recommended Actions", level=1)
|
||||
for i, action in enumerate(record.actions, start=1):
|
||||
label = action.get("label", "Action")
|
||||
value = action.get("value", "")
|
||||
doc.add_paragraph(f"{i}. {label}: {value}", style="List Number")
|
||||
|
||||
# ── Footer note ────────────────────────────────────────────────────────────
|
||||
doc.add_paragraph("")
|
||||
footer = doc.add_paragraph(
|
||||
f"Generated by AI Regulation Analysis System — {datetime.now(timezone.utc).strftime('%Y-%m-%d')}"
|
||||
)
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
for run in footer.runs:
|
||||
run.font.size = Pt(8)
|
||||
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
|
||||
|
||||
buf = BytesIO()
|
||||
doc.save(buf)
|
||||
return buf.getvalue()
|
||||
Reference in New Issue
Block a user