The migration runner (db/__init__.py) calls module.migrate() with no arguments, but migrate_017 defined upgrade(engine). This meant the slides_config_json column was never added to insurance_ppt_templates in production, causing a 500 on /insurance/ppt/render-options.
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""迁移 017: 给 PPT 模板表新增 slides_config_json 列。
|
||
|
||
该列存储每种模板的逐页幻灯片配置(标题模板、叙事提示、图表类型等),
|
||
使管理员可以在后台管理页面中配置 PPT 的每一页内容。
|
||
"""
|
||
import json
|
||
import logging
|
||
from sqlalchemy import text
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def migrate():
|
||
"""执行迁移。"""
|
||
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):
|
||
"""为现有模板填充默认的逐页配置。"""
|
||
templates = db.session.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)
|
||
db.session.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})
|
||
|
||
db.session.commit()
|
||
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 列")
|