baodan/api/insurance/poster/content_builder.py
wsb1224 b8c4e8b672 主要完成内容:
修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。
脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。
保司支持独立控制 PPT、海报 Logo 显示。
PPT 核验新增吸烟状态、币种及三个条件字段。
利益演示、退保提取调整为警告,不再阻止生成。
PPT 生成完成后可以直接返回数据核验页修改。
建立不同险种、单图/长图共六套海报字段画像。
PPT“生成场景”支持后台新增、启停和删除。
保司、产品、PPT 模板、文案模板均支持安全删除。
内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。
补充策略变更及删除审计日志。
更新 API 文档、部署文档及修复计划实施记录。
关键交付文件:
[数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py)
[海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py)
[动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py)
[新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py)
[优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md)
验证结果:
核心链路测试:37 passed,1 skipped
扩展回归测试:140 passed
PPT 渲染器测试:6 passed
前端生产构建:通过
Python 编译检查:通过
完整测试集:190 passed,1 failed
唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00

151 lines
5.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.

"""海报内容构建器 — 将计划书和小册子数据组装为单图/长图模块。"""
import logging
logger = logging.getLogger(__name__)
def build_poster_content(
parsed_data: dict,
product_rules: dict,
output_mode: str = "single",
plan_type: str = "other",
) -> dict:
"""组装海报内容模块。
参数:
parsed_data: 计划书解析数据(含 benefit_table
product_rules: 产品小册子解析数据(含 features 来源页)
output_mode: 'single''long'
返回:
结构化的海报内容 dict供前端 PosterHtmlCanvas 使用
"""
from insurance.poster.field_profiles import get_field_profile
profile = get_field_profile(plan_type, output_mode)
features = _build_features(product_rules)[:profile["maxFeatureCount"]]
benefit_table = _select_benefit_rows(
_build_benefit_table(parsed_data),
profile["milestoneYears"],
profile["outputMode"],
)
if profile["planType"] in ("ci", "other"):
benefit_table = []
content = {
"summary": _build_summary(parsed_data),
"features": features,
"benefit_table": benefit_table,
"bonus_mechanism": product_rules.get("bonus_mechanism", ""),
"flexible_options": product_rules.get("flexible_options", []),
"risk_warnings": product_rules.get("risk_warnings", []),
"investment_rules": product_rules.get("investment_rules", {}),
"currency_options": product_rules.get("currency_options", []),
"data_completeness": _assess_completeness(parsed_data, product_rules),
"field_profile": profile,
"warnings": _profile_warnings(profile, parsed_data, product_rules),
}
return content
def _select_benefit_rows(rows: list, milestone_years: list[int], output_mode: str) -> list:
"""按画像裁剪年度数据,绝不把完整利益表塞进单图。"""
if not rows or not milestone_years:
return []
selected = [row for row in rows if row.get("year") in milestone_years]
if not selected:
selected = rows[:1] if output_mode == "single" else rows[:6]
return selected[:1] if output_mode == "single" else selected[:6]
def _profile_warnings(profile: dict, parsed_data: dict, product_rules: dict) -> list[str]:
warnings = []
if profile["planType"] == "ci" and profile["outputMode"] == "long":
if not product_rules.get("coverage_highlights"):
warnings.append("保障列表为空,已使用简版长图")
if profile["outputMode"] == "long" and profile["planType"] in ("savings", "iul"):
rows = parsed_data.get("benefit_table") or parsed_data.get("benefit_illustration") or []
if len(rows) < 3:
warnings.append("利益演示数据不足,已隐藏年度图表")
return warnings
def _build_summary(parsed_data: dict) -> dict:
"""构建客户摘要卡片数据。"""
return {
"age": parsed_data.get("age"),
"gender": parsed_data.get("gender"),
"currency": parsed_data.get("currency"),
"sum_assured": parsed_data.get("sum_assured"),
"annual_premium": parsed_data.get("annual_premium"),
"premium_term": parsed_data.get("premium_term"),
"coverage_period": parsed_data.get("coverage_period"),
}
def _build_features(product_rules: dict) -> list:
"""构建产品亮点列表,保留来源页码。
优先使用小册子提取的 features带 source_page
如果小册子无数据,回退到计划书的 key_benefits。
"""
features = product_rules.get("features", [])
if features:
result = []
for f in features[:8]:
item = {
"title": f.get("title", ""),
"summary": f.get("summary", ""),
"source_page": f.get("source_page"),
}
if item["title"]:
result.append(item)
return result
# 回退:使用计划书的 key_benefits
return [] # 由调用方从 parsed_data.key_benefits 补充
def _build_benefit_table(parsed_data: dict) -> list:
"""构建利益演示表数据(供图表组件使用)。"""
table = (
parsed_data.get("benefit_table")
or parsed_data.get("benefit_illustration")
or []
)
if not table:
return []
result = []
for row in table:
entry = {
"year": row.get("year") or row.get("policy_year") or row.get("policyYear") or 0,
"guaranteed": row.get("guaranteed_csv") or row.get("guaranteedCashValue") or row.get("guaranteed") or 0,
"nonGuaranteed": row.get("non_guaranteed") or row.get("nonGuaranteed") or 0,
"totalSurrender": row.get("total_surrender") or row.get("totalSurrenderValue") or row.get("total") or 0,
"deathBenefit": row.get("death_benefit") or row.get("deathBenefit") or 0,
}
if entry["year"] > 0:
result.append(entry)
return result
def _assess_completeness(parsed_data: dict, product_rules: dict) -> dict:
"""评估数据完整度,决定哪些模块应显示。
返回:
{field: bool} 表示各模块是否有足够数据
"""
has_benefit_table = bool(parsed_data.get("benefit_table") or parsed_data.get("benefit_illustration"))
benefit_rows = parsed_data.get("benefit_table") or parsed_data.get("benefit_illustration") or []
return {
"has_summary": bool(parsed_data.get("age") or parsed_data.get("sum_assured")),
"has_benefit_chart": has_benefit_table and len(benefit_rows) >= 3,
"has_features": bool(product_rules.get("features")),
"has_currency_options": bool(product_rules.get("currency_options")),
"has_bonus": bool(product_rules.get("bonus_mechanism")),
"has_flexible_options": bool(product_rules.get("flexible_options")),
"has_risk_warnings": bool(product_rules.get("risk_warnings")),
}