2026-07-23 13:10:50 +08:00
|
|
|
|
"""PPT 配置模型(公司、产品、模板)。"""
|
2026-07-28 20:35:28 +08:00
|
|
|
|
import uuid
|
2026-07-23 15:04:16 +08:00
|
|
|
|
from sqlalchemy import Column, String, Text, Boolean, Integer, SmallInteger, TIMESTAMP, func
|
2026-07-23 13:10:50 +08:00
|
|
|
|
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")
|
2026-07-28 16:45:14 +08:00
|
|
|
|
# 脱敏展示名(PPT/海报导出时替代真实名称)
|
|
|
|
|
|
masked_display_name = Column(String(100), nullable=True, comment="脱敏展示名")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
# 品牌信息
|
|
|
|
|
|
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="成立年份")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
# 扩展字段(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="排序权重")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
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,
|
2026-07-28 16:45:14 +08:00
|
|
|
|
"maskedDisplayName": self.masked_display_name or "",
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"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 [],
|
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
|
|
|
|
"rating": self.rating,
|
|
|
|
|
|
"foundedYear": self.founded_year,
|
2026-07-29 12:19:26 +08:00
|
|
|
|
"nameZh": self.name_zh,
|
|
|
|
|
|
"nameEn": self.name_en,
|
|
|
|
|
|
"shortEn": self.short_en,
|
2026-07-23 15:04:16 +08:00
|
|
|
|
"logoUrl": self.logo_url,
|
|
|
|
|
|
"status": self.status,
|
|
|
|
|
|
"sortOrder": self.sort_order,
|
2026-07-23 13:10:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
2026-07-28 16:45:14 +08:00
|
|
|
|
# 脱敏展示名(PPT/海报导出时替代真实名称)
|
|
|
|
|
|
masked_display_name = Column(String(100), nullable=True, comment="脱敏展示名")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
# 扩展字段
|
|
|
|
|
|
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 地址")
|
2026-07-28 20:35:28 +08:00
|
|
|
|
manual_parse_status = Column(String(20), default="none", comment="none/pending/queued/parsing/parsed/reviewed/failed")
|
|
|
|
|
|
manual_parse_message = Column(String(500), default="", comment="解析进度消息")
|
|
|
|
|
|
manual_parse_error = Column(Text, nullable=True, comment="解析失败原因")
|
|
|
|
|
|
manual_parse_task_id = Column(String(200), nullable=True, comment="Celery 任务ID")
|
|
|
|
|
|
manual_parse_started_at = Column(TIMESTAMP, nullable=True, comment="解析开始时间")
|
|
|
|
|
|
manual_parse_finished_at = Column(TIMESTAMP, nullable=True, comment="解析完成时间")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
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="核对时间")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
2026-07-23 15:04:16 +08:00
|
|
|
|
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
2026-07-23 13:10:50 +08:00
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
import json
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": self.id,
|
|
|
|
|
|
"companyId": self.company_id,
|
|
|
|
|
|
"planType": self.plan_type,
|
|
|
|
|
|
"displayName": self.display_name,
|
2026-07-28 16:45:14 +08:00
|
|
|
|
"maskedDisplayName": self.masked_display_name or "",
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"aliases": json.loads(self.aliases_json) if self.aliases_json else [],
|
|
|
|
|
|
"requiredModules": json.loads(self.required_modules_json) if self.required_modules_json else [],
|
2026-07-23 15:04:16 +08:00
|
|
|
|
"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,
|
2026-07-28 20:35:28 +08:00
|
|
|
|
"manualParseMessage": self.manual_parse_message or "",
|
|
|
|
|
|
"manualParseError": self.manual_parse_error or "",
|
|
|
|
|
|
"manualParseTaskId": self.manual_parse_task_id,
|
|
|
|
|
|
"manualParseStartedAt": self.manual_parse_started_at.isoformat() if self.manual_parse_started_at else None,
|
|
|
|
|
|
"manualParseFinishedAt": self.manual_parse_finished_at.isoformat() if self.manual_parse_finished_at else None,
|
2026-07-23 15:04:16 +08:00
|
|
|
|
"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,
|
2026-07-23 13:10:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
# 扩展字段
|
|
|
|
|
|
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")
|
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
|
|
|
|
slides_config_json = Column(Text, nullable=True, comment="逐页幻灯片配置 JSON")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
status = Column(SmallInteger, default=1, comment="1=启用, 0=停用")
|
2026-07-23 13:10:50 +08:00
|
|
|
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
import json
|
2026-07-29 21:26:48 +08:00
|
|
|
|
slides = json.loads(self.slides_config_json) if self.slides_config_json else []
|
2026-07-23 13:10:50 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"id": self.id,
|
|
|
|
|
|
"planType": self.plan_type,
|
|
|
|
|
|
"stylePreset": self.style_preset,
|
|
|
|
|
|
"sourceTemplateAssetId": self.source_template_asset_id,
|
2026-07-29 21:26:48 +08:00
|
|
|
|
"sourceFileUrl": (
|
|
|
|
|
|
f"/insurance/admin/ppt/templates/{self.id}/file"
|
|
|
|
|
|
if self.source_template_asset_id else ""
|
|
|
|
|
|
),
|
|
|
|
|
|
"isBuiltIn": str(self.source_template_asset_id or "").startswith("builtin://"),
|
2026-07-23 13:10:50 +08:00
|
|
|
|
"cloneReady": self.clone_ready,
|
|
|
|
|
|
"cloneRenderer": self.clone_renderer,
|
|
|
|
|
|
"requiredPageTypes": json.loads(self.required_page_types_json) if self.required_page_types_json else [],
|
2026-07-23 15:04:16 +08:00
|
|
|
|
"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 [],
|
2026-07-29 21:26:48 +08:00
|
|
|
|
"slidesConfig": slides,
|
|
|
|
|
|
"slideCount": len(slides),
|
2026-07-23 15:04:16 +08:00
|
|
|
|
"status": self.status,
|
2026-07-23 13:10:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 [],
|
|
|
|
|
|
}
|
2026-07-28 20:35:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CompanyLogo(db.Model):
|
|
|
|
|
|
"""保司Logo图片表(支持多张)。"""
|
|
|
|
|
|
__tablename__ = "insurance_ppt_company_logos"
|
|
|
|
|
|
|
|
|
|
|
|
id = Column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex)
|
|
|
|
|
|
company_id = Column(String(50), nullable=False, index=True, comment="所属公司 ID")
|
|
|
|
|
|
file_path = Column(String(500), nullable=False, comment="服务器文件路径")
|
|
|
|
|
|
file_url = Column(String(500), nullable=True, comment="可访问的URL")
|
|
|
|
|
|
original_name = Column(String(200), nullable=True, comment="原始文件名")
|
|
|
|
|
|
mime_type = Column(String(50), nullable=True, comment="MIME类型")
|
|
|
|
|
|
file_size = Column(Integer, nullable=True, comment="文件大小(字节)")
|
|
|
|
|
|
is_primary = Column(Boolean, default=False, comment="是否为主Logo")
|
|
|
|
|
|
sort_order = Column(Integer, default=0, comment="排序权重")
|
|
|
|
|
|
created_at = Column(TIMESTAMP, server_default=func.now())
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": self.id,
|
|
|
|
|
|
"companyId": self.company_id,
|
|
|
|
|
|
"url": self.file_url or f"/insurance/admin/ppt/assets/company-logos/{self.id}",
|
|
|
|
|
|
"originalName": self.original_name or "",
|
|
|
|
|
|
"mimeType": self.mime_type,
|
|
|
|
|
|
"fileSize": self.file_size,
|
|
|
|
|
|
"isPrimary": self.is_primary,
|
|
|
|
|
|
"sortOrder": self.sort_order,
|
|
|
|
|
|
}
|