baodan/api/insurance/recommend/export_service.py

256 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""方案导出服务:生成 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