baodan/api/insurance/recommend/export_service.py

256 lines
9.8 KiB
Python
Raw Normal View History

"""方案导出服务:生成 PDF/Word 文件。"""
import io
import json
from insurance.db.compat import db
from insurance.models.recommendation import RecommendationRecord
DISCLAIMER = (
"\n\n免责声明:以上方案仅供参考,具体保障内容以保险合同条款为准。"
"投保前请仔细阅读产品条款,了解保险责任、责任免除、等待期等重要内容。"
)
def export_word(proposal_id: str):
"""导出方案为 Word 文档。"""
from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
record = db.session.query(RecommendationRecord).filter_by(id=int(proposal_id)).first()
if not record:
return None, "方案不存在"
plans = json.loads(record.plan_variants) if record.plan_variants else []
# 如果结构化解析为空但有 Markdown 原文,直接渲染 Markdown
if not plans and record.generated_plan:
return _export_word_from_markdown(record)
doc = Document()
# 标题
title = doc.add_heading("保险产品推荐方案", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 客户信息
doc.add_heading("客户信息", level=1)
info_table = doc.add_table(rows=4, cols=4, style="Table Grid")
info_data = [
("姓名", record.customer_name or "-", "年龄", f"{record.customer_age}"),
("性别", "" if record.customer_gender == "male" else "", "职业", record.occupation or "-"),
("健康状况", record.health_status or "-", "年收入", f"{record.annual_income}"),
("月预算", f"{record.monthly_budget}", "保障期限", record.coverage_period or "-"),
]
for i, (k1, v1, k2, v2) in enumerate(info_data):
info_table.rows[i].cells[0].text = k1
info_table.rows[i].cells[1].text = v1
info_table.rows[i].cells[2].text = k2
info_table.rows[i].cells[3].text = v2
# 各方案
for plan in plans:
doc.add_heading(plan.get("name", "方案"), level=1)
premium = plan.get("total_premium", "")
doc.add_paragraph(f"年保费合计:{premium}" if premium else "年保费合计:-")
items = plan.get("items", [])
if items:
table = doc.add_table(rows=1 + len(items), cols=4, style="Table Grid")
headers = ["产品名称", "保额", "年保费", "推荐理由"]
for j, h in enumerate(headers):
table.rows[0].cells[j].text = h
for run in table.rows[0].cells[j].paragraphs[0].runs:
run.bold = True
for k, item in enumerate(items):
table.rows[k + 1].cells[0].text = item.get("product_name", "")
table.rows[k + 1].cells[1].text = item.get("coverage", "")
table.rows[k + 1].cells[2].text = item.get("premium", "")
table.rows[k + 1].cells[3].text = item.get("reason", "")
summary = plan.get("summary", "")
if summary:
doc.add_paragraph(summary)
# 免责声明
doc.add_paragraph(DISCLAIMER)
buf = io.BytesIO()
doc.save(buf)
buf.seek(0)
return buf, None
def export_pdf(proposal_id: str):
"""导出方案为 PDF通过 HTML 转换)。"""
record = db.session.query(RecommendationRecord).filter_by(id=int(proposal_id)).first()
if not record:
return None, "方案不存在"
plans = json.loads(record.plan_variants) if record.plan_variants else []
# 如果结构化解析为空但有 Markdown 原文,直接渲染 Markdown
if not plans and record.generated_plan:
return _export_pdf_from_markdown(record)
# 构建 HTML
html_parts = [
"<!DOCTYPE html><html><head><meta charset='utf-8'>",
"<style>",
"body{font-family:sans-serif;padding:20px}",
"table{width:100%;border-collapse:collapse;margin:10px 0}",
"th,td{border:1px solid #ddd;padding:8px;text-align:left}",
"th{background:#f5f5f5}",
".disclaimer{color:#999;font-size:12px;margin-top:20px}",
"</style></head><body>",
"<h1 style='text-align:center'>保险产品推荐方案</h1>",
"<h2>客户信息</h2>",
f"<p>姓名:{record.customer_name or '-'} | 年龄:{record.customer_age}岁 | "
f"性别:{'' if record.customer_gender == 'male' else ''} | "
f"职业:{record.occupation or '-'}</p>",
f"<p>健康状况:{record.health_status or '-'} | 年收入:{record.annual_income}万 | "
f"月预算:{record.monthly_budget}元 | 保障期限:{record.coverage_period or '-'}</p>",
]
for plan in plans:
html_parts.append(f"<h2>{plan.get('name', '方案')}</h2>")
premium = plan.get("total_premium", "")
html_parts.append(f"<p><strong>年保费合计:{premium}元</strong></p>" if premium else "<p><strong>年保费合计:-</strong></p>")
items = plan.get("items", [])
if items:
html_parts.append("<table><thead><tr><th>产品</th><th>保额</th><th>年保费</th><th>推荐理由</th></tr></thead><tbody>")
for item in items:
html_parts.append(
f"<tr><td>{item.get('product_name', '')}</td>"
f"<td>{item.get('coverage', '')}</td>"
f"<td>{item.get('premium', '')}</td>"
f"<td>{item.get('reason', '')}</td></tr>"
)
html_parts.append("</tbody></table>")
summary = plan.get("summary", "")
if summary:
html_parts.append(f"<p>{summary}</p>")
html_parts.append(f"<p class='disclaimer'>{DISCLAIMER}</p>")
html_parts.append("</body></html>")
html_content = "\n".join(html_parts)
# 尝试用 weasyprint 生成 PDF
try:
from weasyprint import HTML
buf = io.BytesIO()
HTML(string=html_content).write_pdf(buf)
buf.seek(0)
return buf, None
except ImportError:
pass
# 回退:返回 HTML 文件
buf = io.BytesIO(html_content.encode("utf-8"))
return buf, None
def _export_word_from_markdown(record) -> tuple:
"""当结构化解析为空时,直接将 Markdown 渲染为 Word。"""
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
import re
doc = Document()
title = doc.add_heading("保险产品推荐方案", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 客户信息
doc.add_heading("客户信息", level=1)
doc.add_paragraph(
f"姓名:{record.customer_name or '-'} | 年龄:{record.customer_age}岁 | "
f"性别:{'' if record.customer_gender == 'male' else ''} | "
f"职业:{record.occupation or '-'}"
)
# 将 Markdown 逐段写入 Word
md_text = record.generated_plan or ""
for line in md_text.split("\n"):
line = line.strip()
if not line:
continue
if line.startswith("### "):
doc.add_heading(line[4:], level=2)
elif line.startswith("## "):
doc.add_heading(line[3:], level=1)
elif line.startswith("# "):
doc.add_heading(line[2:], level=0)
elif line.startswith("|") and "---" not in line:
# 表格行 → 用段落模拟
cells = [c.strip() for c in line.split("|") if c.strip()]
doc.add_paragraph(" | ".join(cells))
else:
doc.add_paragraph(line)
doc.add_paragraph(DISCLAIMER)
buf = io.BytesIO()
doc.save(buf)
buf.seek(0)
return buf, None
def _export_pdf_from_markdown(record) -> tuple:
"""当结构化解析为空时,直接将 Markdown 渲染为 PDF/HTML。"""
import re
md_text = record.generated_plan or ""
# 简单 Markdown → HTML 转换
html_body = md_text
html_body = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html_body, flags=re.MULTILINE)
html_body = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html_body, flags=re.MULTILINE)
html_body = re.sub(r'^# (.+)$', r'<h1>\1</h1>', html_body, flags=re.MULTILINE)
html_body = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html_body)
# 表格行转 HTML 表格(简化处理)
table_lines = re.findall(r'^(\|.+\|)$', html_body, re.MULTILINE)
if table_lines:
table_html = "<table>"
for i, line in enumerate(table_lines):
cells = [c.strip() for c in line.split("|") if c.strip()]
tag = "th" if i == 0 else "td"
row = "".join(f"<{tag}>{c}</{tag}>" for c in cells)
table_html += f"<tr>{row}</tr>"
table_html += "</table>"
html_body = re.sub(r'^(\|.+\|)$', '', html_body, flags=re.MULTILINE)
html_body += table_html
html_body = re.sub(r'\n{2,}', '</p><p>', html_body)
html_body = f"<p>{html_body}</p>"
html_content = (
"<!DOCTYPE html><html><head><meta charset='utf-8'>"
"<style>"
"body{font-family:sans-serif;padding:20px}"
"table{width:100%;border-collapse:collapse;margin:10px 0}"
"th,td{border:1px solid #ddd;padding:8px;text-align:left}"
"th{background:#f5f5f5}"
".disclaimer{color:#999;font-size:12px;margin-top:20px}"
"</style></head><body>"
f"<h1 style='text-align:center'>保险产品推荐方案</h1>"
f"<h2>客户信息</h2>"
f"<p>姓名:{record.customer_name or '-'} | 年龄:{record.customer_age}岁 | "
f"性别:{'' if record.customer_gender == 'male' else ''} | "
f"职业:{record.occupation or '-'}</p>"
f"{html_body}"
f"<p class='disclaimer'>{DISCLAIMER}</p>"
"</body></html>"
)
try:
from weasyprint import HTML
buf = io.BytesIO()
HTML(string=html_content).write_pdf(buf)
buf.seek(0)
return buf, None
except ImportError:
pass
buf = io.BytesIO(html_content.encode("utf-8"))
return buf, None