2026-07-31 09:00:39 +08:00
|
|
|
|
"""海报内容构建器 — 将计划书和小册子数据组装为单图/长图模块。"""
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 14:10:24 +08:00
|
|
|
|
def build_poster_content(
|
|
|
|
|
|
parsed_data: dict,
|
|
|
|
|
|
product_rules: dict,
|
|
|
|
|
|
output_mode: str = "single",
|
|
|
|
|
|
plan_type: str = "other",
|
|
|
|
|
|
) -> dict:
|
2026-07-31 09:00:39 +08:00
|
|
|
|
"""组装海报内容模块。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
parsed_data: 计划书解析数据(含 benefit_table)
|
|
|
|
|
|
product_rules: 产品小册子解析数据(含 features 来源页)
|
|
|
|
|
|
output_mode: 'single' 或 'long'
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
|
结构化的海报内容 dict,供前端 PosterHtmlCanvas 使用
|
|
|
|
|
|
"""
|
2026-07-31 14:10:24 +08:00
|
|
|
|
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 = []
|
|
|
|
|
|
|
2026-07-31 09:00:39 +08:00
|
|
|
|
content = {
|
|
|
|
|
|
"summary": _build_summary(parsed_data),
|
2026-07-31 14:10:24 +08:00
|
|
|
|
"features": features,
|
|
|
|
|
|
"benefit_table": benefit_table,
|
2026-07-31 09:00:39 +08:00
|
|
|
|
"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),
|
2026-07-31 14:10:24 +08:00
|
|
|
|
"field_profile": profile,
|
|
|
|
|
|
"warnings": _profile_warnings(profile, parsed_data, product_rules),
|
2026-07-31 09:00:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 14:10:24 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-31 09:00:39 +08:00
|
|
|
|
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")),
|
|
|
|
|
|
}
|