baodan/api/insurance/models/ppt_config.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

170 lines
9.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.

"""PPT 配置模型(公司、产品、模板)。"""
from sqlalchemy import Column, String, Text, Boolean, Integer, SmallInteger, TIMESTAMP, func
from insurance.db.compat import db
class PptCompany(db.Model):
"""保险公司配置表。"""
__tablename__ = "insurance_ppt_companies"
id = Column(String(50), primary_key=True, comment="公司 ID如 aia/ctf/fwd")
display_name = Column(String(100), nullable=False, comment="显示名称")
aliases_json = Column(Text, nullable=True, comment="别名列表 JSON")
tenant_id = Column(String(50), default="default", comment="租户 ID")
knowledge_directories_json = Column(Text, nullable=True, comment="知识库目录 JSON")
evidence_ranking_json = Column(Text, nullable=True, comment="证据排序关键词 JSON")
company_intro = Column(Text, nullable=True, comment="公司简介")
company_highlights_json = Column(Text, nullable=True, comment="公司亮点 JSON")
# 品牌信息
name_zh = Column(String(100), nullable=True, comment="中文名称")
name_en = Column(String(100), nullable=True, comment="英文名称")
short_en = Column(String(20), nullable=True, comment="英文缩写")
rating = Column(String(50), nullable=True, comment="评级")
founded_year = Column(Integer, nullable=True, comment="成立年份")
# 扩展字段PPT/海报功能)
logo_url = Column(String(500), nullable=True, comment="公司 Logo 图片地址")
status = Column(SmallInteger, default=1, comment="1=启用, 0=停用")
sort_order = Column(Integer, default=0, comment="排序权重")
created_at = Column(TIMESTAMP, server_default=func.now())
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
def to_dict(self):
import json
return {
"id": self.id,
"displayName": self.display_name,
"aliases": json.loads(self.aliases_json) if self.aliases_json else [],
"tenantId": self.tenant_id,
"knowledgeDirectories": json.loads(self.knowledge_directories_json) if self.knowledge_directories_json else [],
"evidenceRanking": json.loads(self.evidence_ranking_json) if self.evidence_ranking_json else [],
"companyIntro": self.company_intro,
"companyHighlights": json.loads(self.company_highlights_json) if self.company_highlights_json else [],
"rating": self.rating,
"foundedYear": self.founded_year,
"logoUrl": self.logo_url,
"status": self.status,
"sortOrder": self.sort_order,
}
class PptProduct(db.Model):
"""保险产品配置表。"""
__tablename__ = "insurance_ppt_products"
id = Column(String(50), primary_key=True, comment="产品 ID")
company_id = Column(String(50), nullable=False, comment="所属公司 ID")
plan_type = Column(String(10), nullable=False, comment="产品类型: savings/ci/iul")
display_name = Column(String(100), nullable=False, comment="显示名称")
aliases_json = Column(Text, nullable=True, comment="别名列表 JSON")
required_modules_json = Column(Text, nullable=True, comment="所需模块 JSON")
# 扩展字段
product_code = Column(String(50), nullable=True, comment="产品编码")
product_type = Column(String(20), nullable=True, comment="产品类型: savings/ci/iul")
coverage_period = Column(String(50), nullable=True, comment="保障期限")
payment_period = Column(String(50), nullable=True, comment="缴费期限")
insured_age_range = Column(String(50), nullable=True, comment="投保年龄范围")
waiting_period = Column(String(50), nullable=True, comment="等待期")
highlights = Column(Text, nullable=True, comment="产品亮点 JSON")
extra_fields = Column(Text, nullable=True, comment="扩展字段 JSON")
status = Column(SmallInteger, default=1, comment="1=启用, 0=停用")
sort_order = Column(Integer, default=0, comment="排序")
# 海报相关
manual_file_url = Column(String(500), nullable=True, comment="产品小册子 PDF 地址")
manual_parse_status = Column(String(20), default="none", comment="none/parsing/parsed/reviewed")
manual_parsed_rules = Column(Text, nullable=True, comment="解析出的产品规则 JSON")
manual_reviewed_by = Column(String(50), nullable=True, comment="核对人")
manual_reviewed_at = Column(TIMESTAMP, nullable=True, comment="核对时间")
created_at = Column(TIMESTAMP, server_default=func.now())
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
def to_dict(self):
import json
return {
"id": self.id,
"companyId": self.company_id,
"planType": self.plan_type,
"displayName": self.display_name,
"aliases": json.loads(self.aliases_json) if self.aliases_json else [],
"requiredModules": json.loads(self.required_modules_json) if self.required_modules_json else [],
"productCode": self.product_code,
"productType": self.product_type,
"coveragePeriod": self.coverage_period,
"paymentPeriod": self.payment_period,
"insuredAgeRange": self.insured_age_range,
"waitingPeriod": self.waiting_period,
"highlights": json.loads(self.highlights) if self.highlights else [],
"extraFields": json.loads(self.extra_fields) if self.extra_fields else {},
"status": self.status,
"sortOrder": self.sort_order,
"manualFileUrl": self.manual_file_url,
"manualParseStatus": self.manual_parse_status,
"manualParsedRules": json.loads(self.manual_parsed_rules) if self.manual_parsed_rules else None,
"manualReviewedBy": self.manual_reviewed_by,
"manualReviewedAt": self.manual_reviewed_at.isoformat() if self.manual_reviewed_at else None,
}
class PptTemplate(db.Model):
"""PPT 模板配置表。"""
__tablename__ = "insurance_ppt_templates"
id = Column(String(50), primary_key=True, comment="模板 ID")
plan_type = Column(String(10), nullable=False, comment="产品类型: savings/ci/iul")
style_preset = Column(String(20), nullable=False, comment="风格: broker/business/minimal/chinese/ink")
source_template_asset_id = Column(String(50), nullable=True, comment="源模板资产 ID")
clone_ready = Column(Boolean, default=False, comment="是否克隆就绪")
clone_renderer = Column(String(100), nullable=True, comment="克隆渲染器 ID")
required_page_types_json = Column(Text, nullable=True, comment="必需页面类型 JSON")
# 扩展字段
name = Column(String(100), nullable=True, comment="模板名称")
scenario_tag = Column(String(50), nullable=True, comment="场景标签")
preview_image = Column(String(500), nullable=True, comment="预览图地址")
applicable_company_ids = Column(Text, nullable=True, comment="适用保司 ID 列表 JSON")
applicable_product_ids = Column(Text, nullable=True, comment="适用产品 ID 列表 JSON")
slides_config_json = Column(Text, nullable=True, comment="逐页幻灯片配置 JSON")
status = Column(SmallInteger, default=1, comment="1=启用, 0=停用")
created_at = Column(TIMESTAMP, server_default=func.now())
def to_dict(self):
import json
return {
"id": self.id,
"planType": self.plan_type,
"stylePreset": self.style_preset,
"sourceTemplateAssetId": self.source_template_asset_id,
"cloneReady": self.clone_ready,
"cloneRenderer": self.clone_renderer,
"requiredPageTypes": json.loads(self.required_page_types_json) if self.required_page_types_json else [],
"name": self.name,
"scenarioTag": self.scenario_tag,
"previewImage": self.preview_image,
"applicableCompanyIds": json.loads(self.applicable_company_ids) if self.applicable_company_ids else [],
"applicableProductIds": json.loads(self.applicable_product_ids) if self.applicable_product_ids else [],
"slidesConfig": json.loads(self.slides_config_json) if self.slides_config_json else [],
"status": self.status,
}
class PptBundle(db.Model):
"""PPT Bundle 组合配置表。"""
__tablename__ = "insurance_ppt_bundles"
id = Column(String(50), primary_key=True, comment="Bundle ID")
display_name = Column(String(100), nullable=False, comment="显示名称")
products_json = Column(Text, nullable=False, comment="产品类型列表 JSON")
template_family = Column(String(50), default="bundle", comment="模板族")
status = Column(String(20), default="active", comment="状态: active/inactive")
modules_json = Column(Text, nullable=True, comment="模块列表 JSON")
created_at = Column(TIMESTAMP, server_default=func.now())
def to_dict(self):
import json
return {
"id": self.id,
"displayName": self.display_name,
"products": json.loads(self.products_json) if self.products_json else [],
"templateFamily": self.template_family,
"status": self.status,
"modules": json.loads(self.modules_json) if self.modules_json else [],
}