baodan/api/insurance/models/ppt_config.py
wsb1224 b8c4e8b672 主要完成内容:
修复 PPT 异步任务无法生成的问题,包括任务变量引用错误、失败状态回写、心跳缺失任务恢复。
脱敏改为保司/产品后台统一配置,生成端不再让用户选择;任务创建时保存策略快照。
保司支持独立控制 PPT、海报 Logo 显示。
PPT 核验新增吸烟状态、币种及三个条件字段。
利益演示、退保提取调整为警告,不再阻止生成。
PPT 生成完成后可以直接返回数据核验页修改。
建立不同险种、单图/长图共六套海报字段画像。
PPT“生成场景”支持后台新增、启停和删除。
保司、产品、PPT 模板、文案模板均支持安全删除。
内置模板禁止删除,只允许停用;存在关联数据时拒绝危险删除。
补充策略变更及删除审计日志。
更新 API 文档、部署文档及修复计划实施记录。
关键交付文件:
[数据库迁移 migrate_027.py](D:/work/code/python/coding/baodanagent/api/insurance/db/migrate_027.py)
[海报字段画像 field_profiles.py](D:/work/code/python/coding/baodanagent/api/insurance/poster/field_profiles.py)
[动态场景服务 scenarios.py](D:/work/code/python/coding/baodanagent/api/insurance/ppt/scenarios.py)
[新增回归测试](D:/work/code/python/coding/baodanagent/tests/ppt_poster_optimization_test.py)
[优化修复计划书](D:/work/code/python/coding/baodanagent/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md)
验证结果:
核心链路测试:37 passed,1 skipped
扩展回归测试:140 passed
PPT 渲染器测试:6 passed
前端生产构建:通过
Python 编译检查:通过
完整测试集:190 passed,1 failed
唯一失败为 tests/test_chat_save.py::test_chat_logs_query 未建立 Flask application context,与本次 PPT/海报链路无关。
2026-07-31 14:10:24 +08:00

282 lines
15 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 配置模型(公司、产品、模板)。"""
import uuid
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")
# 脱敏展示名PPT/海报导出时替代真实名称)
masked_display_name = Column(String(100), nullable=True, comment="脱敏展示名")
masking_enabled = Column(Boolean, default=False, nullable=False, comment="是否启用名称脱敏")
# 品牌信息
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 图片地址")
logo_enabled = Column(Boolean, default=True, nullable=False, 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())
deleted_at = Column(TIMESTAMP, nullable=True, comment="软删除时间")
def to_dict(self):
import json
return {
"id": self.id,
"displayName": self.display_name,
"maskedDisplayName": self.masked_display_name or "",
"maskingEnabled": bool(self.masking_enabled),
"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,
"nameZh": self.name_zh,
"nameEn": self.name_en,
"shortEn": self.short_en,
"logoUrl": self.logo_url,
"logoEnabled": bool(self.logo_enabled),
"status": self.status,
"sortOrder": self.sort_order,
"deletedAt": self.deleted_at.isoformat() if self.deleted_at else None,
}
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")
# 脱敏展示名PPT/海报导出时替代真实名称)
masked_display_name = Column(String(100), nullable=True, comment="脱敏展示名")
masking_enabled = Column(Boolean, default=False, nullable=False, comment="是否启用名称脱敏")
# 扩展字段
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/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="解析完成时间")
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())
deleted_at = Column(TIMESTAMP, nullable=True, comment="软删除时间")
def to_dict(self):
import json
return {
"id": self.id,
"companyId": self.company_id,
"planType": self.plan_type,
"displayName": self.display_name,
"maskedDisplayName": self.masked_display_name or "",
"maskingEnabled": bool(self.masking_enabled),
"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,
"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,
"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,
"deletedAt": self.deleted_at.isoformat() if self.deleted_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())
deleted_at = Column(TIMESTAMP, nullable=True, comment="软删除时间")
def to_dict(self):
import json
try:
slides = json.loads(self.slides_config_json) if self.slides_config_json else []
except (json.JSONDecodeError, TypeError):
slides = []
try:
required_types = json.loads(self.required_page_types_json) if self.required_page_types_json else []
except (json.JSONDecodeError, TypeError):
required_types = []
try:
company_ids = json.loads(self.applicable_company_ids) if self.applicable_company_ids else []
except (json.JSONDecodeError, TypeError):
company_ids = []
try:
product_ids = json.loads(self.applicable_product_ids) if self.applicable_product_ids else []
except (json.JSONDecodeError, TypeError):
product_ids = []
return {
"id": self.id,
"planType": self.plan_type,
"stylePreset": self.style_preset,
"sourceTemplateAssetId": self.source_template_asset_id,
"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://"),
"cloneReady": self.clone_ready,
"cloneRenderer": self.clone_renderer,
"requiredPageTypes": required_types,
"name": self.name,
"scenarioTag": self.scenario_tag,
"previewImage": self.preview_image,
"applicableCompanyIds": company_ids,
"applicableProductIds": product_ids,
"slidesConfig": slides,
"slideCount": len(slides),
"status": self.status,
"deletedAt": self.deleted_at.isoformat() if self.deleted_at else None,
}
class PptScenario(db.Model):
"""PPT 生成场景;场景分类与底层计算模式分离。"""
__tablename__ = "insurance_ppt_scenarios"
code = Column(String(50), primary_key=True)
name = Column(String(100), nullable=False)
base_scenario = Column(String(50), nullable=True)
generation_mode = Column(String(20), nullable=False, default="single")
description = Column(Text, nullable=True)
status = Column(SmallInteger, default=1, nullable=False)
sort_order = Column(Integer, default=0, nullable=False)
is_builtin = Column(Boolean, default=False, nullable=False)
created_at = Column(TIMESTAMP, server_default=func.now())
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
deleted_at = Column(TIMESTAMP, nullable=True)
def to_dict(self):
return {
"code": self.code,
"name": self.name,
"baseScenario": self.base_scenario,
"generationMode": self.generation_mode,
"description": self.description or "",
"status": self.status,
"sortOrder": self.sort_order,
"isBuiltIn": bool(self.is_builtin),
"deletedAt": self.deleted_at.isoformat() if self.deleted_at else None,
}
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 [],
}
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,
}