baodan/api/insurance/db/migrate_017.py
wsb1224 1debd7df39 feat(ppt): upgrade renderer from 5 fixed slides to 10+ configurable pages
Rewrite fast_pptx_renderer.py to read requiredPageTypes from template
config instead of hardcoding 5 slides. Add 10 slide builder functions
(cover, company, narrative, chart, timeline, table, compare, synergy,
conclusion, closing) with python-pptx native charts.

Key changes:
- Renderer reads templateConfig.requiredPageTypes and slidesConfig
  from DeckContract to determine slide sequence and per-slide metadata
- routes.py loads PptTemplate and PptCompany from DB, normalizes all
  PDF extractions (not just the first), passes full context to renderer
- renderer.py injects templateConfig, company info, and multi-product
  data into DeckContract
- Add slides_config_json column to PptTemplate (migrate_017) for
  per-slide title/narrative/chartType configuration via admin UI
- Admin template editor now supports drag-reorder slides, per-slide
  title/narrative hint, chart/table type selection
- Add requiredPageTypes to savings/ink, savings/minimal, savings/business
  templates (were missing, causing fallback to defaults)
- Fix IUL normalizer: add payYears and totalPremium to policy dict
- Fix CI/IUL normalizer: add totalSurrenderValue alias to benefitRows
  so charts render correctly for all product types
- Port calculation functions from baodanppt: decade_rows, paid_premium,
  simple_return, compound_return, find_payback_year

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 17:49:09 +08:00

142 lines
6.1 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.

"""迁移 017: 给 PPT 模板表新增 slides_config_json 列。
该列存储每种模板的逐页幻灯片配置(标题模板、叙事提示、图表类型等),
使管理员可以在后台管理页面中配置 PPT 的每一页内容。
"""
import json
import logging
from sqlalchemy import text
logger = logging.getLogger(__name__)
def upgrade(engine):
"""执行迁移。"""
with engine.begin() as conn:
# 检查列是否已存在
result = conn.execute(text(
"SELECT COUNT(*) FROM information_schema.columns "
"WHERE table_name = 'insurance_ppt_templates' AND column_name = 'slides_config_json'"
))
if result.scalar() > 0:
logger.info("slides_config_json 列已存在,跳过迁移")
return
conn.execute(text(
"ALTER TABLE insurance_ppt_templates ADD COLUMN slides_config_json TEXT"
))
logger.info("已添加 slides_config_json 列")
# 为现有模板填充默认 slides_config
_seed_default_slides_config(conn)
def _seed_default_slides_config(conn):
"""为现有模板填充默认的逐页配置。"""
templates = conn.execute(text(
"SELECT id, plan_type, required_page_types_json FROM insurance_ppt_templates WHERE status = 1"
)).fetchall()
for tpl in templates:
tpl_id, plan_type, rpt_json = tpl
if not rpt_json:
continue
try:
page_types = json.loads(rpt_json)
except (json.JSONDecodeError, TypeError):
continue
slides_config = _build_default_slides_config(page_types, plan_type)
conn.execute(text(
"UPDATE insurance_ppt_templates SET slides_config_json = :config WHERE id = :id"
), {"config": json.dumps(slides_config, ensure_ascii=False), "id": tpl_id})
logger.info(f"已为 {len(templates)} 个模板填充默认 slides_config")
def _build_default_slides_config(page_types, plan_type):
"""根据 pageType 列表构建默认的逐页配置。"""
defaults = {
"cover": {"pageType": "cover", "title": "{{customerName}} 专属方案",
"subtitle": "{{productName}}", "narrativeHint": ""},
"company": {"pageType": "company", "title": "{{companyName}} 公司介绍",
"narrativeHint": "公司事实来自内部知识库权威口径"},
"narrative": {"pageType": "narrative", "title": "方案核心逻辑",
"narrativeHint": "先看结构,再看收益与保障"},
"chart": {"pageType": "chart", "title": "价值增长曲线",
"chartType": "growth", "narrativeHint": "先看回本区间再看20/30年关键点"},
"timeline": {"pageType": "timeline", "title": "家庭时间轴",
"narrativeHint": "把谁在什么阶段发挥作用讲清楚"},
"table": {"pageType": "table", "title": "数据表每10年",
"tableType": "no_withdraw", "narrativeHint": "含单利复利,便于长期价值对比"},
"compare": {"pageType": "compare", "title": "产品对比",
"narrativeHint": "保证部分是底线,非保证部分是弹性"},
"synergy": {"pageType": "synergy", "title": "协同关系",
"narrativeHint": "功能分层,互不冲突"},
"conclusion": {"pageType": "conclusion", "title": "结论",
"narrativeHint": "一张保家庭不失速,一张保未来不落空"},
"closing": {"pageType": "closing", "title": "感谢信任",
"narrativeHint": "方案可继续迭代优化,如有疑问请随时联系"},
}
# 根据产品类型调整默认标题
if plan_type == "ci":
defaults["narrative"]["title"] = "家庭风险防线"
defaults["narrative"]["narrativeHint"] = "先防风险,再做未来"
defaults["chart"]["title"] = "重疾保障数据曲线"
defaults["chart"]["chartType"] = "growth"
elif plan_type == "iul":
defaults["narrative"]["title"] = "传承杠杆设计"
defaults["narrative"]["narrativeHint"] = "先看杠杆,再看现金价值弹性"
defaults["chart"]["title"] = "IUL 长期利益曲线"
defaults["chart"]["chartType"] = "growth"
slides = []
table_count = 0
chart_count = 0
for pt in page_types:
if pt == "chart":
meta = dict(defaults.get(pt, {"pageType": pt}))
if chart_count == 0:
meta["chartType"] = "growth"
meta["title"] = "价值增长曲线"
else:
meta["chartType"] = "stacked"
meta["title"] = "保证/非保证构成"
chart_count += 1
elif pt == "table":
meta = dict(defaults.get(pt, {"pageType": pt}))
if table_count == 0:
meta["tableType"] = "no_withdraw"
meta["title"] = "不提领方案数据表每10年"
else:
meta["tableType"] = "withdraw"
meta["title"] = "提领方案数据表每10年"
table_count += 1
elif pt == "timeline":
meta = dict(defaults.get(pt, {"pageType": pt}))
# 第二个 timeline 用不同标题
if sum(1 for s in slides if s["pageType"] == "timeline") > 0:
meta["title"] = "中后期里程碑"
meta["narrativeHint"] = "养老金与长期传承阶段"
elif pt == "conclusion":
meta = dict(defaults.get(pt, {"pageType": pt}))
if sum(1 for s in slides if s["pageType"] == "conclusion") > 0:
meta["title"] = "下一步行动建议"
meta["narrativeHint"] = "建议尽快与保险经纪人预约确认方案"
else:
meta = dict(defaults.get(pt, {"pageType": pt}))
slides.append(meta)
return {"slides": slides}
def downgrade(engine):
"""回滚迁移。"""
with engine.begin() as conn:
conn.execute(text(
"ALTER TABLE insurance_ppt_templates DROP COLUMN IF EXISTS slides_config_json"
))
logger.info("已删除 slides_config_json 列")