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
|
|
|
|
"""迁移 017: 给 PPT 模板表新增 slides_config_json 列。
|
|
|
|
|
|
|
|
|
|
|
|
该列存储每种模板的逐页幻灯片配置(标题模板、叙事提示、图表类型等),
|
|
|
|
|
|
使管理员可以在后台管理页面中配置 PPT 的每一页内容。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 08:47:35 +08:00
|
|
|
|
def migrate():
|
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
|
|
|
|
"""执行迁移。"""
|
2026-07-25 08:47:35 +08:00
|
|
|
|
from insurance.db.compat import db
|
|
|
|
|
|
|
|
|
|
|
|
# 检查列是否已存在
|
|
|
|
|
|
result = db.session.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
|
|
|
|
|
|
|
|
|
|
|
|
db.session.execute(text(
|
|
|
|
|
|
"ALTER TABLE insurance_ppt_templates ADD COLUMN slides_config_json TEXT"
|
|
|
|
|
|
))
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
logger.info("已添加 slides_config_json 列")
|
|
|
|
|
|
|
|
|
|
|
|
# 为现有模板填充默认 slides_config
|
|
|
|
|
|
_seed_default_slides_config(db)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_default_slides_config(db):
|
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
|
|
|
|
"""为现有模板填充默认的逐页配置。"""
|
2026-07-25 08:47:35 +08:00
|
|
|
|
templates = db.session.execute(text(
|
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
|
|
|
|
"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)
|
2026-07-25 08:47:35 +08:00
|
|
|
|
db.session.execute(text(
|
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
|
|
|
|
"UPDATE insurance_ppt_templates SET slides_config_json = :config WHERE id = :id"
|
|
|
|
|
|
), {"config": json.dumps(slides_config, ensure_ascii=False), "id": tpl_id})
|
|
|
|
|
|
|
2026-07-25 08:47:35 +08:00
|
|
|
|
db.session.commit()
|
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
|
|
|
|
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 列")
|