baodan/docs/保险智能客服系统_PPT与海报优化修复计划书_20260731.md

54 KiB
Raw Blame History

保险智能客服系统 PPT 与海报优化修复计划书

版本v1.1 日期2026-07-31
范围PPT 生成、海报生成、保司管理、产品管理、PPT 模板管理、文案模板管理
原则:先恢复可用性,再统一配置与数据口径,最后补齐管理能力;仅修改 api/insurance/frontend/tests/ 和自定义迁移,不修改 BaoDan 基座业务文件。

1. 执行摘要

本轮需求应拆成四个层次推进:

  1. P0 恢复 PPT 生成:当前 PPT 一直生成不出来存在已确认的代码级直接根因。generate_ppt_task() 在异常捕获块之前访问未定义变量 task.workspace_idWorker 领取任务后会立即抛出 NameError,任务还可能停留在 running
  2. P0 让所选 PPT 模板真正生效:当前 Worker 能正确解析 templateId 和源模板路径,但渲染器随后删除源 PPTX 的全部幻灯片,再用统一的硬编码构建器重建页面。三个内置模板虽然分别包含 20、17、18 张完整设计页,实际成品却变成 12、10、15 张统一深蓝基础样式。
  3. P1 统一生成策略与字段校验:取消用户侧脱敏开关,改由保司和产品后台配置;新增 Logo 开关;补齐吸烟状态、币种和三个待确认字段;把利益演示和退保提取从阻断错误降级为非阻断警告。
  4. P1/P2 完善海报与后台管理:按险种和单图/长图建立字段画像;让 PPT 模板场景可扩展为保司、产品、PPT 模板和文案模板提供安全删除能力。

本计划不建议把所有问题一次性塞进一个大版本。建议按 P0 热修、P1 数据与策略、P1 海报、P2 管理能力四个可独立验收的版本交付。

2. 审计方法与成功标准

本计划基于当前仓库代码、迁移、测试和部署配置形成,不把推测当成结论。

2.1 已执行的检查

  • 对 PPT/海报前后端、后台管理、数据模型、迁移和测试进行了关键词与调用链审计。
  • 对 PPT 路由、校验、归一化、渲染器、Celery 任务执行了 Python 语法检查,语法检查通过。
  • 执行相关测试:
    • 结果:10 passed, 1 failed, 1 skipped
    • 唯一失败为提取器测试 mock 仍返回旧签名,实际代码现在要求 _extract_pdf_text() 返回 (pdf_text, page_qualities),说明测试与新接口不同步。
  • 检查本地 SQLite
    • 迁移历史只到 migrate_016
    • 当前代码中的统一任务表和工作区字段由 migrate_022 创建。
    • 这不是线上环境已确认事实,但属于必须上线前核验的高风险环境差异。
  • 检查 Docker 配置:
    • Worker 已监听 insurance 队列。
    • API 与 Worker 已配置相同 INSURANCE_STORAGE_ROOT
    • 自定义镜像显式安装并验证 python-pptx

2.2 总体验收目标

  • PPT 正常任务在约定时限内从 queued → running → done,失败任务必定进入 failed,不存在永久 running
  • PPT 和海报页面不再出现用户脱敏开关;最终是否脱敏完全由后台当前配置快照决定。
  • Logo 关闭后PPT、海报预览、导出文件和 AI Prompt 均不出现 Logo。
  • 利益演示或退保提取缺失/异常不再阻止进入生成步骤。
  • 单图与长图、不同险种使用各自字段规则,不因无关字段缺失阻塞。
  • 自定义场景能被创建、选择、保存和用于模板生成,不被固定场景校验拒绝。
  • 选择三个内置 PPT 模板中的任意一个时,输出必须保留该模板的母版、页面结构、背景、图片、图表、表格和排版特征,不得退回统一基础样式。
  • 任务结果可追溯实际使用的模板资产版本和 SHA-256“已应用模板”不能只代表模板 ID 校验通过。
  • 四类后台对象均可删除或安全下架,历史记录和已生成文件仍可追溯。

3. 当前问题与结论

编号 需求 当前实现 结论 优先级
1 后台控制脱敏 PPT、海报前台均有 useMaskedData 开关;后台只有脱敏展示名,没有启停开关 必须移除前台控制,并由服务端解析后台策略 P1
2 PPT 核验字段与非阻断板块 核验页缺吸烟状态和币种编辑项;后端仍把部分利益/退保问题标成 error 新增字段并重构错误分级 P1
3 PPT 一直无法生成 Worker 使用未定义的 task,且位于 try 之前 已确认 P0 直接根因 P0
4 后台控制 Logo 有 Logo 上传/主 Logo但无是否启用字段 增加保司级 logo_enabled,服务端强制执行 P1
5 海报按险种和版式取字段 当前主要按字段是否存在展示,单图/长图只改变布局高度 需要字段画像和内容预算层 P1
6 PPT 生成场景可添加 前端固定 3 个场景;后端探测器固定 6 个代码;自定义值会被匹配校验拒绝 不能只做 allow-create,需拆分业务场景和计算模式 P1
7 四类后台删除 仅文案模板已有硬删除保司、产品、PPT 模板没有删除接口 增加软删除和引用检查 P2
8 三个内置 PPT 模板选中后仍使用基础样式 模板 ID 和文件路径已进入 Worker但渲染器删除源模板全部页面并由统一 SLIDE_BUILDERS/THEMES 重建 不是缓存或前端传参主因;必须新增“克隆并原位编辑”渲染模式 P0
9 后台上传模板可能无法稳定保存或追溯版本 source_template_asset_idVARCHAR(50),而 stored://{templateId}/{uuid}.pptx 可超过 50 字符;任务也未固化资产版本/hash 扩容字段并建立版本化模板资产与任务快照 P0/P1

4. P0PPT 无法生成专项修复

4.1 已确认直接根因

调用链:

POST /insurance/ppt/generate/{sessionId}
  → task_service.create_task()
  → generate_ppt_task.apply_async(queue="insurance")
  → Worker _claim_task(taskId)
  → 访问 task.workspace_id
  → NameError: name 'task' is not defined

证据:

  • api/insurance/generation/celery_tasks.py:266 使用 task.workspace_id
  • 同一函数此前没有查询或赋值 task
  • _claim_task() 的返回类型是 bool,不会返回任务对象。
  • 该代码在 try 之前执行,因此现有失败回写逻辑捕获不到异常。
  • git blame 显示这段状态同步代码由 2026-07-30 的提交 5f78598b3 引入,属于明确回归点。

实际影响:

  • Worker 任务本身异常退出。
  • 任务数据库状态已由 _claim_task() 改为 running,但不会被改为 failed
  • 前端可能持续轮询或仅看到“生成中”。
  • 过期任务只能等待启动时的 stale recovery 才可能恢复,用户感知为“一直生成不出来”。

4.2 P0 修复方案

  1. generate_ppt_task() 成功领取任务后,先通过 GenerationTask.query.get(task_id) 获取任务。
  2. 把任务读取、工作区状态同步和 _execute_ppt_generate() 全部放入同一个 try
  3. 若任务记录不存在,显式结束,不继续执行。
  4. 所有异常统一:
    • 任务状态改为 failed
    • error_code=generate_error
    • 同步 PptSession.status/error/workflow_step
    • 记录带 task_id/session_id/stage 的结构化日志。
  5. 不使用 Celery 声明上的 max_retries 伪装重试能力:
    • 当前函数没有调用 self.retry()
    • 第一阶段直接移除误导性重试参数,或明确仅对瞬时渲染异常调用受控重试;
    • 数据校验错误不得重试。
  6. 增加一次性运维脚本或管理命令,把该回归期间超过阈值的 running PPT 任务改为 failed/stale_task,同步工作区状态。

推荐的最小修复结构:

if not _claim_task(task_id):
    return

try:
    task = GenerationTask.query.get(task_id)
    if not task:
        return
    sync_workspace_to_generating(task.workspace_id)
    _execute_ppt_generate(task_id)
except Exception as exc:
    mark_task_and_workspace_failed(task_id, exc)
    raise

4.3 必须同时核验的环境项

直接根因修复后,仍需在部署环境执行以下检查,防止第二层问题被遮住:

  1. 数据库迁移:
    • db_migration_history 至少包含 migrate_022migrate_026
    • insurance_generation_tasks 表存在。
    • insurance_ppt_sessions 包含 workflow_step/latest_task_id/draft_revision 等字段。
  2. Worker
    • celery inspect registered 能看到 insurance.generate_ppt
    • Worker 命令包含 -Q ...,insurance
  3. 共享存储:
    • API 和 Worker 的 INSURANCE_STORAGE_ROOT 完全一致。
    • Worker 可写 outputs/ppt/{userId}/{taskId}
    • API 可读 Worker 生成的同一路径。
  4. 渲染依赖:
    • Worker 容器内 import pptx 成功。
    • fast_pptx_renderer.py 存在。
    • 使用 Worker 实际 Python 解释器完成一次最小 DeckContract 冒烟测试。

4.4 P0 测试

新增测试:

  • test_generate_ppt_task_loads_task_before_workspace_sync
  • test_generate_ppt_task_exception_marks_task_failed
  • test_generate_ppt_task_exception_syncs_workspace_failed
  • test_generate_ppt_task_missing_task_exits_cleanly
  • test_generate_ppt_task_success_reaches_done
  • test_stale_running_generate_task_is_recovered
  • API/Worker/共享存储容器级冒烟测试

修复现有失败测试:

  • 更新 tests/ppt_task_lifecycle_test.py_extract_pdf_text mock使其返回 ("文本", page_qualities)
  • 该失败不是当前 PPT 生成直接根因,但必须清零后才允许发布。

4.5 P0 验收

  • 连续生成 20 次 PPT无永久 queued/running
  • 人工制造渲染异常30 秒内前端显示可理解的失败原因。
  • 重启 Worker 后,旧任务不会覆盖新任务状态。
  • 生成文件可由 API 下载PPTX 可被 PowerPoint/WPS 打开。

5. 后台统一脱敏策略

5.1 当前范围说明

现有 masking.py 实际处理的是保司名称和产品名称替换,不是完整客户隐私脱敏系统。后台已有:

  • PptCompany.masked_display_name
  • PptProduct.masked_display_name

前端 PPT 和海报却让用户通过 useMaskedData 决定是否执行,服务端也信任该请求参数。这会导致同一产品由不同用户生成不同口径,且用户可以绕过后台策略。

本轮建议明确:

  • 保司开关只控制保司名称是否替换为保司脱敏展示名。
  • 产品开关只控制产品名称是否替换为产品脱敏展示名。
  • 客户姓名、证件号、手机号等个人信息脱敏属于另一套 PII 规则;若业务也要求,应另立范围,不能误以为当前名称替换已经覆盖。

5.2 数据模型

在新迁移中增加:

insurance_ppt_companies.masking_enabled BOOLEAN NOT NULL DEFAULT FALSE
insurance_ppt_companies.logo_enabled    BOOLEAN NOT NULL DEFAULT TRUE
insurance_ppt_products.masking_enabled  BOOLEAN NOT NULL DEFAULT FALSE

不设计保司与产品之间的复杂继承关系:

  • 保司名称是否脱敏,看保司自己的开关。
  • 产品名称是否脱敏,看产品自己的开关。
  • 多产品 PPT 对每个产品独立处理。

这样每个后台开关的含义唯一,也符合“一键设置”的要求。

5.3 服务端策略

新增统一解析函数,例如:

resolve_generation_brand_policy(company, products)
  → companyNameMasked
  → productNameMaskedById
  → logoEnabled
  → policyVersion

规则:

  1. PPT/海报 API 忽略客户端传入的 useMaskedData
  2. 任务创建时从数据库读取当前配置,写入任务快照。
  3. Worker 只使用快照,不在执行中途重新读取,避免管理员修改设置导致同一任务前后不一致。
  4. 历史记录保存最终策略布尔值和策略版本,不保存无必要的原始敏感文本。
  5. 用户自上传资料没有后台产品配置时:
    • 默认不自动伪造脱敏产品名;
    • 若能匹配到保司,则仍执行保司名称策略;
    • 产品名脱敏需先由管理员建立映射,或在用户资料模型中另加管理员审核配置。

5.4 前后端改造

后台:

  • 保司管理增加“名称脱敏”开关。
  • 产品管理增加“名称脱敏”开关。
  • 开启但未填写 maskedDisplayName 时禁止保存,避免运行时采用不可审计的临时规则。

用户端:

  • 删除 PptGenerate.vue 的“数据脱敏”开关。
  • 删除 PosterProductPanel.vue 的“使用脱敏数据”开关。
  • 删除草稿、请求类型和自动保存中的 useMaskedData
  • 可只读显示“后台策略:保司名已脱敏/产品名未脱敏”,但不能由用户修改。

兼容:

  • API 在一个版本内可接收旧 useMaskedData 字段但完全忽略,并记录一次弃用日志。
  • 下一版本从接口文档和类型中删除。

5.5 验收矩阵

保司开关 产品开关 期望结果
保司名、产品名均显示真实配置名
只替换保司名
只替换产品名
两者均替换

以上四种组合需分别验证 PPT、海报预览、最终导出、历史快照和 AI Prompt。

6. Logo 启停策略

6.1 当前问题

系统已有 Logo 上传、排序、主 Logo 和 logoUrl但没有启停配置。PPT 的 DeckContract 和海报产品快照会直接携带 logoUrl

仅在前端隐藏 Logo 不足够,因为:

  • Worker 仍可能把 Logo 渲染进最终文件。
  • 海报 Prompt 或快照仍可能携带 Logo。
  • 用户可以直接调用 API 绕过页面。

6.2 改造方案

  1. 保司模型新增 logo_enabled,默认 TRUE,保证升级后现有行为不突变。
  2. 保司管理增加“生成物展示 Logo”开关。
  3. 统一品牌策略解析时:
    • logo_enabled=false → 输出 logoUrl=""logoAssetId=null
    • 不删除已上传 Logo
    • 重新开启后继续使用原主 Logo。
  4. PPT
    • DeckContract 中不携带 Logo。
    • 模板占位符没有 Logo 时隐藏,而不是显示破图或空白边框。
  5. 海报:
    • HTML 画布和最终导出均不渲染 Logo 图层。
    • AI 图片 Prompt 继续禁止模型生成伪 Logo。
  6. 多保司比较:
    • 每个保司分别按自身开关处理;
    • 不因其中一家关闭而隐藏其他保司 Logo。

6.3 验收

  • 关闭 Logo 后,新生成 PPT/海报无 Logo。
  • 历史文件不被追溯修改。
  • 重新开启后恢复主 Logo。
  • 无主 Logo、Logo 文件已丢失、外部 URL 失效时均能无损降级。

7. PPT 核验字段与校验分级

7.1 新增关键字段

核验页“关键字段”新增:

中文名 原始字段 归一化字段 类型 建议口径
吸烟状态 insured.smoker insured.smoker 枚举 yes/no/unknown
币种 policy.currency policy.currency 枚举 ISO 4217 代码
基本计划年保费 policy.basic_plan_annual_premium policy.basicPlanAnnualPremium 金额,可选 仅来源明确标注时填写
基本计划名义金额 policy.basic_sum_insured policy.basicSumInsured 金额,可选 对应 Basic Sum Assured/Notional Amount
首年应缴金额 policy.first_year_amount_due policy.firstYearAmountDue 金额,可选 含折扣、征费或附加费后的首年实际应缴

注意:

  • 需求中的“USB”应按“USD”处理USB 不是货币代码,不应作为选项。
  • 人民币统一保存为 CNY页面可以显示“人民币CNY输入 RMB/¥ 时归一化为 CNY
  • 建议常用选项:USD、HKD、CNY、SGD、EUR、GBP,并保留可搜索的 ISO 代码扩展能力。
  • 当前 normalizer.py 在部分路径中把缺失币种默认成 USD。该行为必须移除,未知币种应为 null/unknown 并提示用户确认,不能伪装成已确认数据。

7.2 三个待判断字段的结论

仓库目前没有真实 PDF 样本,无法证明三个中文标签在所有保司计划书中含义一致。因此不能把它们直接设为所有险种必填。

推荐结论:

  1. 基本计划年保费
    • 与现有 annual_premium 很可能重叠。
    • 只有计划书同时出现“基本计划年保费”和“总年保费/附加保障保费”时,才应保存为独立字段。
    • 普通储蓄险只有一个年缴金额时,继续使用 annual_premium
  2. 基本计划名义金额
    • 可映射到已有提取结构中的 basic_sum_insured
    • 对 CI/IUL 更常见;储蓄险可能不存在或不适合作为主要销售指标。
    • 建议作为条件字段,不设全局必填。
  3. 首年应缴金额
    • 不等同于年缴保费,可能包含首年折扣、保费征费、附加费。
    • 当前 total_premium_with_levy 只能作为候选来源,不能无条件等同。
    • 建议新增独立字段,并保留 source_label/source_page

落地前需要业务方提供至少以下脱敏样本:

  • 储蓄险 3 份,至少 2 家保司;
  • 重疾险 3 份,至少 2 家保司;
  • IUL 3 份,至少 2 家保司;
  • 至少 1 份含征费/折扣;
  • 至少 1 份明确出现 Basic Sum Assured/Notional Amount。

样本确认后建立“原文标签 → 规范字段”映射表和金标测试,未确认前三个字段均按可选字段开发。

7.3 必填与非阻断规则

建议将校验分为三级:

BLOCKER无法确定产品或生成基本事实禁止进入生成
WARNING可能影响部分页面准确性允许继续
INFO展示提示不影响流程

全局 BLOCKER

  • 产品名称缺失;
  • 险种无法确定;
  • 被保人年龄缺失或非法;
  • 币种缺失;
  • 生成所需的核心金额全部缺失。

按险种 BLOCKER

  • 储蓄险:年缴保费、缴费年期。
  • 重疾险:保额、年缴保费、缴费年期。
  • IUL保额目标/计划保费至少有一个有效值。

吸烟状态:

  • 作为关键字段展示。
  • yes/no 已确认时正常通过。
  • unknown 时产生 WARNING允许继续避免计划书确实未载明时形成死锁。

以下全部改为 WARNING不再阻断

  • 利益演示行不足;
  • 利益演示年份不连续;
  • 利益演示金额关系异常;
  • 退保提取缺失;
  • 退保提取年份不连续;
  • 利益/退保来源页缺失。

生成策略:

  • 数据缺失的板块不生成对应页面,或明确显示“待正式计划书确认”。
  • 不得用 0、默认 USD 或 AI 推测值填补缺失事实。
  • 用户完成生成后可以回到核验步骤修改数据,再创建新版本。

7.4 前端交互

  • 关键字段显示必填标识和来源页。
  • 币种使用可搜索下拉框。
  • 吸烟状态使用“吸烟/不吸烟/未确认”单选或下拉。
  • WARNING 使用黄色,不禁用“下一步”。
  • 点击警告可定位对应字段或板块。
  • 进入下一步时如仍有 WARNING弹出一次汇总确认不要求逐条解决。
  • 结果页“返回修改”保留当前 session、模板、字段和修改日志重新生成形成新版本不覆盖旧版本。

7.5 后端改造点

  • prompts.py:补齐三个候选字段及来源要求。
  • regex_extractor.py:补充标签映射;未知币种不默认 USD。
  • normalizer.py:全险种统一输出吸烟、币种及三个候选字段。
  • validator.py:按分区和险种分级;利益/退保只返回 warning。
  • routes.pyvalidated 只由 BLOCKER 数量决定。
  • celery_tasks.py:生成阶段不得重新把 WARNING 当成 error 阻断。
  • PptDataReview.vue:补字段、选项、来源和非阻断交互。
  • fast_pptx_renderer.py:字段缺失时隐藏相关卡片,不制造默认值。

8. 海报按险种和单图/长图的字段画像

8.1 当前问题

当前海报前端主要根据 parsedFields 中是否存在字段决定展示;outputMode=single/long 主要只控制画布固定比例或自动高度。系统还没有“险种 × 版式”的字段选择层,因此会出现:

  • 单图塞入过多表格和事实,信息密度过高;
  • 长图仍只显示单图级摘要,信息不足;
  • 储蓄险、重疾险、IUL 使用同一套卡片;
  • 某险种不存在的字段被误判为缺失;
  • 生成 Prompt 和 HTML 画布可能使用不同字段。

8.2 推荐架构

新增单一字段画像配置,不在多个组件中各写一套判断:

PosterFieldProfile
  planType: savings | ci | iul | other
  outputMode: single | long
  required: []
  recommended: []
  optional: []
  forbidden: []
  maxFeatureCount
  milestoneYears

流程:

产品资料/计划书
  → 统一事实模型
  → 按险种选择 FieldProfile
  → 按 single/long 裁剪内容
  → 同一 ViewModel 同时供 HTML 预览、AI Prompt、最终导出使用

8.3 字段矩阵

储蓄险

单图:

  • 必需:产品名、保司名、币种、年缴保费、缴费年期。
  • 推荐23 个已审核产品卖点、一个可核验里程碑数据。
  • 可选:基本计划名义金额、保障期、投保年龄。
  • 不展示:完整利益演示表、完整退保提取表、过多年度数据。

长图:

  • 必需:单图必需字段。
  • 推荐投保条件、总计划保费、10/20/30 年里程碑、保证/非保证拆分、回本提示。
  • 可选:首年应缴金额、提取方案、完整产品亮点。
  • 利益演示不足时隐藏图表,不阻断长图生成。

重疾险CI

单图:

  • 必需:产品名、保司名、币种、基本保额。
  • 推荐:年缴保费、缴费年期、核心疾病/保障层级、等待期。
  • 条件字段:投保年龄、性别、吸烟状态。
  • 不展示:储蓄险回本倍数、退保价值营销结论。

长图:

  • 必需:单图必需字段。
  • 推荐:保障项目列表、早期/严重疾病结构、额外赔付、等待期、保障期限。
  • 可选:豁免、身故保障、特定疾病、年龄/吸烟条件。
  • 保障列表为空时只允许简版长图,并标记 WARNING。

IUL

单图:

  • 必需:产品名、保司名、币种、名义保额/身故保障。
  • 推荐:目标保费或计划保费、缴费方式、一个指数账户核心机制。
  • 条件字段:投保年龄、性别、吸烟状态。
  • 不展示:未经确认的收益率承诺。

长图:

  • 必需:单图必需字段。
  • 推荐:目标/最低保费、指数账户、封顶率/参与率/保底机制、关键年度账户价值和退保价值。
  • 可选:贷款/提取说明、费用说明、首年应缴金额。
  • 所有非保证数据必须带演示口径和免责声明。

其他险种

  • 使用通用保守画像:产品名、保司名、保障对象、核心保障、适用人群、免责声明。
  • 未建立专属画像前,不展示收益图表或推导指标。

8.4 单图与长图的内容预算

项目 单图 长图
主标题 1 1
副标题/正文 12 段 24 段
核心数字卡 24 个 48 个
卖点 23 个 36 个
图表 01 个 03 个
年度数据 1 个里程碑 36 个里程碑
免责声明 必须 必须、可更完整

8.5 改造位置

  • 后端新增 poster/field_profiles.py 或等价配置模块。
  • product_source_resolver.py 输出统一事实模型,不直接决定视觉字段。
  • content_builder.py 根据画像构建 Poster ViewModel。
  • copy_generator.pyimage_generator.py 只消费 ViewModel。
  • PosterHtmlCanvas.vue 不再自行猜测字段别名。
  • PosterSummaryCards.vue 接收已经筛选和排序的 cards。
  • 长图图表只使用服务端确认允许展示的里程碑数据。

8.6 海报验收

每个险种至少使用 2 份样本,分别生成单图和长图,共至少 12 个基准输出:

  • 无跨险种错误字段;
  • 单图无信息溢出;
  • 长图缺失板块能自动隐藏;
  • 数字、币种、产品名在预览和导出中一致;
  • 后台脱敏和 Logo 策略一致生效;
  • 不生成虚构文字、数字或 Logo。

9. PPT 模板“生成场景”动态化

9.1 为什么不能只把下拉框改成可输入

前端目前固定三个场景:

  • single_savings
  • multi_savings_comparison
  • savings_iul_comprehensive

后端 detect_generation_scenario() 仍使用固定代码计算场景;生成接口还要求模板 scenario_tag 必须等于检测结果。因此只给 el-select 增加 allow-create 会导致:

  • 后台可以保存自定义值;
  • 用户选择后生成接口仍提示“模板不适用于当前计划书组合”;
  • 自动选模板永远不会命中新场景。

9.2 推荐模型

新增 insurance_ppt_scenarios

字段 类型 说明
code VARCHAR(50) PK 稳定代码
name VARCHAR(100) 后台显示名
base_scenario VARCHAR(50) 对应内置数据组合,可空
generation_mode VARCHAR(20) single/compare/portfolio
description TEXT 场景说明
status SMALLINT 启停
sort_order INT 排序
is_builtin BOOLEAN 是否内置
deleted_at TIMESTAMP 软删除

设计边界:

  • 场景负责模板分类和业务话术。
  • generation_mode仍由实际上传产品组合确定,负责计算逻辑。
  • 自定义场景不能修改底层金融计算模式,避免后台误配置导致错误比较。
  • base_scenario 用于自动匹配;为空的自定义场景只能由管理员配置模板、用户手动选择。

9.3 接口

GET    /insurance/admin/ppt/scenarios
POST   /insurance/admin/ppt/scenarios
PUT    /insurance/admin/ppt/scenarios/{code}
DELETE /insurance/admin/ppt/scenarios/{code}
PUT    /insurance/admin/ppt/scenarios/{code}/status

模板接口继续保存 scenarioTag=code,但要校验场景存在且启用。

9.4 前端

  • PPT 模板管理从场景接口加载选项。
  • 提供“新增场景”弹窗,而不是在下拉框里随意输入不可审计文本。
  • 内置场景允许改显示名和排序,不允许修改代码与计算模式。
  • 已被模板使用的场景删除时返回引用冲突,引导先迁移模板。

9.5 生成匹配

  1. 根据实际产品计算 detectedScenariogenerationMode
  2. 自动推荐:
    • 先匹配 base_scenario=detectedScenario
    • 再按适用保司、产品和状态过滤。
  3. 手动选择自定义场景模板:
    • 不再要求 template.scenario_tag == detectedScenario
    • 校验模板场景的 generation_mode 与实际 mode 一致;
    • 继续校验适用保司和适用产品。
  4. 任务快照同时保存:
    • detectedScenario
    • selectedScenario
    • generationMode

这样既允许业务新增场景,又不破坏底层计算规则。

10. 四类后台删除能力

10.1 删除策略

这些对象会被历史记录、生成任务、产品、Logo、模板适用范围或文件引用。直接物理删除会破坏追溯因此统一采用

默认软删除deleted_at
必要时:先停用,再删除
文件:引用计数为 0 且过保留期后异步清理
历史:永不级联删除

所有列表默认排除软删除数据,管理员可选择查看“回收站”。

10.2 保司删除

删除前检查:

  • 是否存在未删除产品;
  • 是否有进行中的 PPT/海报任务;
  • 是否仍被模板 applicable_company_ids 引用;
  • 是否有 Logo 文件。

规则:

  • 有未删除产品时返回 409 COMPANY_HAS_PRODUCTS,不自动级联。
  • 管理员先迁移或删除产品。
  • 删除保司时同时软删除 Logo 元数据;物理 Logo 文件进入延迟清理。
  • 历史记录保留公司 ID 和当时快照。

10.3 产品删除

删除前检查:

  • 是否有进行中的任务;
  • 是否被 PPT 模板适用产品列表引用;
  • 是否有关联已审核产品小册子或用户资料。

规则:

  • 进行中任务存在时禁止删除。
  • 模板引用时返回具体模板列表。
  • 已生成历史不阻止软删除。
  • 小册子文件不立即删除,按数据保留策略异步处理。

10.4 PPT 模板删除

规则:

  • 内置模板默认只允许停用/隐藏,不物理删除内置资产。
  • 自定义模板允许软删除。
  • 进行中任务使用该模板时禁止删除。
  • 历史 PPT 继续保留 template_id 和内容快照。
  • 模板源 PPTX 只有在无其他版本引用且过保留期后才清理。

10.5 文案模板删除

当前 delete_copy_template() 是硬删除,应改为软删除:

  • 新增 deleted_at
  • 海报历史必须使用生成时的文案快照,不回查已删除模板内容。
  • 已被草稿引用时允许软删除,但草稿打开时提示“模板已下架,保留当前文案,重新选择后才能再次套用”。

10.6 API 与交互

新增:

DELETE /insurance/admin/ppt/companies/{id}
DELETE /insurance/admin/ppt/products/{id}
DELETE /insurance/admin/ppt/templates/{id}
DELETE /insurance/admin/ppt/copy-templates/{id}  # 改软删除语义

统一响应:

{
  "code": 0,
  "message": "success",
  "data": {
    "id": "xxx",
    "deleted": true,
    "deletedAt": "2026-07-31T12:00:00"
  }
}

引用冲突:

{
  "code": 1409,
  "message": "该保司仍有 3 个产品,无法删除",
  "data": {
    "reason": "COMPANY_HAS_PRODUCTS",
    "references": []
  }
}

前端要求:

  • config_manage 权限可见删除按钮。
  • 二次确认显示对象名称和影响。
  • 禁止只用无上下文的“确定删除?”。
  • 删除成功后刷新当前分页,不跳回第一页。
  • 409 时展示具体依赖和处理入口。
  • 所有删除写入 system_operation_logs

11. 数据库迁移计划

建议分两个迁移,降低回滚复杂度。

11.1 migrate_027:策略与软删除

新增:

insurance_ppt_companies.masking_enabled
insurance_ppt_companies.logo_enabled
insurance_ppt_companies.deleted_at

insurance_ppt_products.masking_enabled
insurance_ppt_products.deleted_at

insurance_ppt_templates.deleted_at
poster_copy_templates.deleted_at

默认值:

  • masking_enabled = FALSE,保持升级前默认不脱敏。
  • logo_enabled = TRUE,保持升级前 Logo 行为。
  • deleted_at = NULL

增加必要索引:

  • 公司、产品、模板的 (status, deleted_at)
  • 产品的 (company_id, deleted_at)
  • 文案模板的 (status, deleted_at)

11.2 migrate_028:动态场景

  • 创建 insurance_ppt_scenarios
  • 写入现有内置场景。
  • 为已有模板的未知 scenario_tag 补建禁用的兼容场景记录,避免数据丢失。
  • 增加 base_scenario/generation_mode 校验。

11.3 迁移安全

  • 迁移只加列、加表、加索引,不删旧字段。
  • 发布前对生产库备份。
  • API 启动后健康检查返回当前迁移版本。
  • 若迁移未到 028管理后台禁用新开关和场景编辑并显示明确错误不静默失败。
  • 必须修复或补测 SQLite 兼容性;当前 migrate_022 的表/列存在检查主要面向 PostgreSQL/MySQL。

12. API 改造清单

12.1 保司与产品

  • GET companies/products 返回 maskingEnabled/deletedAt
  • 保司额外返回 logoEnabled
  • POST/PUT 支持新字段。
  • DELETE 执行软删除与引用检查。
  • 公共生成选项接口只返回 status=1 AND deleted_at IS NULL

12.2 PPT 生成

请求中移除:

useMaskedData

任务快照新增:

{
  "brandPolicy": {
    "companyMaskingEnabled": true,
    "productMaskingById": {
      "product-a": true
    },
    "logoEnabled": false,
    "policyVersion": 1
  },
  "detectedScenario": "single_savings",
  "selectedScenario": "retirement_education",
  "generationMode": "single",
  "templateAsset": {
    "templateId": "scenario_single_savings",
    "assetId": "builtin://single_savings.pptx",
    "version": 1,
    "sha256": "ba7b198e92caba6316ed8db71f0ace4231d9b570aa78f8d2ad96ce6e3c31f184",
    "rendererMode": "clone-edit-v2"
  }
}

12.3 校验

校验响应新增:

{
  "canProceed": true,
  "blockerCount": 0,
  "warningCount": 3,
  "infoCount": 1
}

兼容旧字段:

  • 一个版本内继续返回 validated = blockerCount == 0
  • 前端改用 canProceed

12.4 海报

海报创建和文案生成请求不再接收 useMaskedData

响应/任务快照增加:

{
  "planType": "savings",
  "outputMode": "single",
  "fieldProfileVersion": 1,
  "brandPolicy": {}
}

13. 代码修改范围

13.1 后端

文件/模块 改动
api/insurance/generation/celery_tasks.py P0 任务修复、校验分级、策略快照消费;固化模板资产版本/hash/渲染模式
api/insurance/generation/task_service.py 失败状态同步、stale 任务恢复
api/insurance/ppt/routes.py 移除用户脱敏控制、动态场景、校验响应
api/insurance/ppt/validator.py BLOCKER/WARNING/INFO 分级
api/insurance/ppt/normalizer.py 新字段和未知币种处理
api/insurance/ppt/prompts.py 新字段提取口径
api/insurance/ppt/regex_extractor.py 标签与币种归一化
api/insurance/ppt/renderer.py Logo/脱敏策略写入 DeckContractrendererMode 分流真实模板与通用构建器
api/insurance/ppt/scripts/fast_pptx_renderer.py 禁止在真实模板模式删除源幻灯片;按模板清单原位替换形状、图表、表格和图片
api/insurance/ppt/template_asset_service.py 模板入库校验、规范化、SHA-256、版本化和绑定清单解析
api/insurance/ppt/masking.py 从请求布尔值改为实体策略
api/insurance/poster/product_source_resolver.py 品牌策略和统一事实模型
api/insurance/poster/content_builder.py 按画像构建 ViewModel
api/insurance/poster/copy_generator.py 只消费允许字段
api/insurance/poster/image_generator.py 禁止伪 Logo执行 Logo 策略
api/insurance/poster/field_profiles.py 新增险种 × 版式画像
api/insurance/admin/ppt_admin_service.py 新开关、动态场景、删除与引用检查
api/insurance/admin/ppt_admin_routes.py 新接口
api/insurance/models/ppt_config.py 新字段、场景模型;模板资产字段扩容及版本关系
api/insurance/models/poster_copy_template.py 软删除
api/insurance/db/migrate_027.py 策略与软删除迁移
api/insurance/db/migrate_028.py 场景迁移
api/insurance/db/migrate_029.py 模板资产版本、字段扩容、三个内置模板清单与 hash 回填

13.2 前端

文件/模块 改动
PptGenerate.vue 删除脱敏开关、显示只读后台策略
PptDataReview.vue 新字段、币种、非阻断警告、返回修改
PptResult.vue 返回核验并生成新版本
PosterProductPanel.vue 删除脱敏开关
PosterConfigRail.vue 移除脱敏事件
PosterCopyPanel.vue 请求移除 useMaskedData
PosterHtmlCanvas.vue 消费服务端 ViewModel
PosterSummaryCards.vue 消费排序后的字段卡
usePosterWorkspace.ts 删除草稿脱敏字段,增加画像版本
PptCompaniesAdmin.vue 脱敏、Logo、删除开关
PptProductsAdmin.vue 脱敏、删除开关
PptTemplatesAdmin.vue 动态场景、删除
CopyTemplatesAdmin.vue 软删除交互和依赖提示
ppt-api.ts/poster-api.ts/ppt-admin-api.ts 更新类型与接口

14. 分阶段交付计划

阶段 AP0 热修(建议 0.51 人日)

  • 修复未定义 task
  • 把状态同步纳入异常捕获。
  • 清理 stale 任务。
  • 修复失败的提取器测试 mock。
  • 增加 Worker 任务回归测试。
  • 在部署环境确认 022026 迁移、队列和共享存储。

验收门槛PPT 可稳定生成,任务状态闭环。

阶段 A2P0 模板真实套用(建议 35 人日)

  • 引入 clone-edit-v2,从所选源 PPTX 开始原位编辑,不再先删光源页面。
  • 为三个内置模板建立逐页、逐形状的数据绑定清单。
  • 修复模板图表 OOXML 兼容性并完成 PowerPoint/WPS 打开测试。
  • 扩容模板资产标识,固化任务使用的版本和 SHA-256。
  • 将当前“文件存在 + 页数正确”的测试升级为模板结构和视觉保真测试。

验收门槛:三个模板各生成一次,成品设计明显互异,且均保留源模板独有视觉签名、可编辑对象和正确资产 hash。

阶段 B后台策略与 PPT 核验(建议 35 人日)

  • migrate_027
  • 保司/产品脱敏开关、保司 Logo 开关。
  • 移除 PPT/海报用户脱敏开关。
  • 新增吸烟、币种和三个条件字段。
  • 重构校验分级。
  • 支持结果页返回修改和版本化重生成。

验收门槛四种脱敏组合、Logo 开关、非阻断校验全部通过。

阶段 C海报字段画像建议 35 人日)

  • 建立统一事实模型和六套核心画像。
  • 单图/长图内容预算。
  • 预览、Prompt、导出共用 ViewModel。
  • 建立至少 12 个基准输出。

验收门槛:无跨险种字段、无虚构数字、布局不过载。

阶段 D动态场景与删除能力建议 34 人日)

  • migrate_028
  • 场景管理接口和页面。
  • 模板匹配逻辑拆分。
  • 四类对象软删除、依赖检查、审计日志和回收站。

验收门槛:自定义场景可完整使用,删除不破坏历史。

阶段 E回归与灰度建议 23 人日)

  • 全量后端测试、前端构建、容器冒烟。
  • 真实脱敏样本 UAT。
  • 灰度开启新策略。
  • 观察任务失败率、平均生成时间和 stale 任务数。

总估算:约 14.523 人日,不包含业务方准备样本、设计稿调整和外部 AI 服务不稳定等待时间。

15. 测试计划

15.1 单元测试

  • 品牌策略四种组合。
  • Logo 开关。
  • 币种别名归一化和未知币种。
  • 三个条件字段映射。
  • 各险种 BLOCKER/WARNING。
  • 利益/退保异常永不成为 BLOCKER。
  • 六套海报字段画像。
  • 自定义场景与 generation mode 兼容性。
  • 四类删除的引用检查。

15.2 API 测试

  • 用户传入 useMaskedData 不改变结果。
  • 无权限用户不能改策略或删除。
  • 删除引用冲突返回 409 和依赖详情。
  • 被删除对象不出现在生成选项。
  • 历史接口仍返回已删除对象的快照。

15.3 集成测试

  • API 创建任务 → insurance 队列 → Worker → 文件 → 下载。
  • API/Worker 共享存储。
  • PostgreSQL 迁移 016 → 028。
  • 空库迁移 001 → 028。
  • 重复执行迁移无副作用。
  • Worker 中断和恢复。

15.4 前端测试

  • PPT/海报用户端无脱敏开关。
  • 后台三个新开关保存后立即生效。
  • 核验页 WARNING 可继续。
  • 返回修改保持数据。
  • 自定义场景新增、编辑、停用、删除。
  • 删除成功、冲突、无权限三种状态。

15.5 视觉和文件测试

  • PPTX 可打开、页数正确、无破图。
  • 三个内置模板分别生成后进行逐页截图对比;不得出现三份成品共用同一深蓝基础背景的情况。
  • 校验任务记录中的 templateId/assetVersion/assetSha256/rendererMode 与 Worker 实际读取文件一致。
  • 校验源模板独有对象仍可编辑:单储蓄模板图表与摘要表、多储蓄模板双产品对比图、储蓄险 + IUL 模板图片与组合配置页。
  • 模板导入需通过 OOXML 校验及 PowerPoint/WPS 无修复提示打开;不得只依赖 python-pptx 可解析。
  • Logo 开/关像素级对比。
  • 单图常见比例不溢出。
  • 长图高度随内容增长。
  • 中英文、长产品名、大金额、字段缺失。

16. 监控与可观测性

新增指标:

  • insurance_ppt_tasks_total{status,error_code}
  • insurance_ppt_generation_duration_seconds
  • insurance_ppt_stale_tasks
  • insurance_ppt_render_failures_total
  • insurance_poster_generation_duration_seconds
  • insurance_generation_policy_snapshot_total
  • insurance_admin_delete_conflicts_total{entity}

日志必须包含:

task_id
workspace_id
user_id必要时脱敏
stage
template_id
detected_scenario
selected_scenario
generation_mode
plan_types
error_code

不得记录完整客户 PDF 文本、未脱敏姓名或完整 Prompt。

建议告警:

  • 5 分钟内 PPT 失败率 > 10%
  • 任一任务 running 超过 10 分钟;
  • Worker 未注册 insurance.generate_ppt
  • API/Worker 存储探针不一致;
  • 最新迁移版本低于应用要求。

17. 上线与回滚

17.1 上线顺序

  1. 备份数据库。
  2. 部署 P0 Worker 修复。
  3. 执行迁移 027。
  4. 部署兼容新旧字段的后端。
  5. 部署前端,移除用户脱敏开关。
  6. 执行迁移 028。
  7. 开启动态场景和删除页面。
  8. 完成冒烟后再开放真实用户。

必须先后端后前端,避免旧后端不识别新字段。

17.2 回滚

  • 新迁移只增加字段和表,应用回滚时保留数据。
  • 后端回滚后旧代码会忽略新增字段。
  • Logo 默认 TRUE、脱敏默认 FALSE回滚行为接近升级前。
  • 动态场景故障时可关闭场景管理入口,保留三个内置场景。
  • 删除全部为软删除,可通过回收站恢复。
  • P0 Worker 修复不得回滚到含未定义变量的版本。

18. 风险与待业务确认

18.1 已确定,不需要再讨论

  • PPT Worker 未定义变量必须优先修复。
  • 用户侧脱敏开关必须移除,服务端不能信任旧参数。
  • Logo 开关必须由服务端执行。
  • 利益演示和退保提取不得阻断下一步。
  • USB 应纠正为 USD
  • 删除应保护历史,不能直接级联物理删除。

18.2 开发前需要业务确认

  1. “脱敏”是否仅指保司/产品名称,还是还包括客户姓名、证件号、手机号和保费组合。
  2. 三个候选字段在真实计划书中的原文标签和业务含义。
  3. 吸烟状态未知时是否允许继续;本计划建议允许并警告。
  4. 自定义生成场景是否只用于模板分类,还是需要新增计算逻辑;本计划只开放分类和话术,不开放任意金融计算。
  5. 内置 PPT 模板是否允许彻底删除;本计划建议仅停用。
  6. 删除后的回收站保留期和源文件物理清理周期。

这些确认不会阻塞 P0 热修,可以与阶段 A 并行完成。

19. Definition of Done

本轮优化完成必须同时满足:

  • P0 根因有自动化回归测试。
  • 相关测试全部通过,无已知失败或跳过的核心链路。
  • 生产迁移版本已核验。
  • 用户端脱敏开关已移除。
  • 保司/产品脱敏策略服务端强制生效。
  • Logo 关闭后所有新生成物均无 Logo。
  • 吸烟状态、币种和三个条件字段完成前后端贯通。
  • 利益/退保问题仅为非阻断警告。
  • 结果页可以返回修改并生成新版本。
  • 三险种 × 两版式海报基准全部通过。
  • 自定义场景可以完整创建、选择、生成和停用。
  • 三个内置 PPT 模板均由 clone-edit-v2 真实套用,源模板结构和视觉特征有自动化保真测试。
  • 模板资产版本、SHA-256 和实际渲染模式进入不可变任务快照与结果元数据。
  • 四类后台对象支持安全删除和引用提示。
  • 删除、策略修改和生成均有审计记录。
  • API 文档、测试用例、部署文档同步更新。

20. 建议立即执行的前三项

  1. 立即热修 generate_ppt_task() 未定义变量,并清理 stale 任务。
  2. 在实际部署数据库和 Worker 中核验迁移、任务注册、队列与共享存储。
  3. 收集 9 份以上脱敏计划书样本,确认三个候选字段,再冻结字段规范。

在这三项完成前,不建议先做大规模界面调整;否则可能出现页面已经改完,但生成链路和字段语义仍不可靠的情况。

21. 实施记录2026-07-31

本次已按计划完成代码级修复:

  • 修复 PPT Worker 在异常捕获前访问未定义任务变量的问题;失败任务会进入 failed 并同步工作区stale 任务统一使用 stale_task
  • 新增 migrate_027.py品牌策略、Logo 开关、软删除字段、动态场景表及六个内置场景。
  • PPT/海报生成页已移除用户脱敏开关;服务端忽略旧参数,按任务创建时的后台策略快照执行。
  • PPT 核验新增吸烟状态、ISO 币种及三个条件金额字段;USB 归一化为 USDRMB/¥ 归一化为 CNY,未知币种不再默认。
  • 利益演示与退保提取问题全部为非阻断警告;结果页可返回核验数据,重新生成保留历史版本。
  • 海报新增三险种 × 单图/长图六套字段画像和内容预算,单图不会携带完整利益表。
  • PPT 场景支持后台新增、停用、删除与模板选择;计算模式仍由实际材料组合确定。
  • 保司、产品、PPT 模板、文案模板均使用软删除;保司活跃产品、内置模板和场景引用具备删除保护;策略变更和删除写入操作审计。
  • API 文档和部署文档已同步。

已完成验证:

  • Python 语法编译通过。
  • 核心新增与现有任务/路由测试:37 passed, 1 skipped
  • PPT 提取、校验、对比与状态同步扩展回归:140 passed(更新旧测试口径后)。
  • 使用工作区幻灯片运行环境补跑 PPT 渲染对比:6 passed
  • 全量测试:190 passed, 1 failed;唯一失败是既有 test_chat_logs_query 未创建 Flask application context与本次 PPT/海报改动无关。
  • Vue/Vite 生产构建通过。

环境限制与上线前待办:

  • 尚未在生产数据库实际执行 migrate_027,也未连接真实 Celery Worker、共享存储和图片模型做端到端生成。
  • 三个条件字段仍需使用脱敏真实计划书样本冻结标签语义;本实现保持可选,不将其作为全险种阻断条件。
  • 三险种 × 两版式的 12 份视觉基准需要业务样本和人工验收,不能由无样本单元测试替代。

22. 三个内置 PPT 模板专项诊断与修复方案2026-07-31 补充)

22.1 已核验事实

对源 PPTX、数据库模板记录、生成任务快照、Worker 输出和最终成品进行了逐层比对:

模板 源文件 源页数 实际成品页数 源模板主要视觉 实际成品
scenario_single_savings single_savings.pptx 20 12 白底、藏蓝与金色4 个图表、2 个摘要表 统一深蓝底、青绿色卡片
scenario_multi_savings multi_savings_comparison.pptx 17 10 白底、藏蓝与金色;双产品对比图和双摘要表 统一深蓝底、青绿色卡片
scenario_savings_iul savings_iul_comprehensive.pptx 18 15 米白/绿色5 个媒体资产、家庭与顾问照片、组合配置页 统一深蓝底,源照片全部消失

数据库中三条记录的 source_template_asset_id 均正确,且都被标记为 clone_ready=true;近期任务也记录了正确的 requestedTemplateId/appliedTemplateIdtemplateFallback=false。因此这不是“用户没有选中模板”,也不是内置文件缺失。

22.2 直接根因

真实调用链为:

PptGenerate.vue 选择 templateId
  → 生成任务快照保存 templateId
  → celery_tasks.py 查询 PptTemplate
  → resolve_template_asset() 得到正确 builtin:// 文件路径
  → renderer.py 同时写入 scenarioSlides 与 templateConfig
  → fast_pptx_renderer.py 打开源 PPTX
  → while len(prs.slides): 删除全部源幻灯片
  → SLIDE_BUILDERS + THEMES 重新生成基础页面

具体有四个叠加问题:

  1. fast_pptx_renderer.py 打开源模板后立即删除全部幻灯片。源背景、图片、形状、图表、表格、字号、间距和页面编排因此全部丢失,只可能残留母版关系。
  2. renderer.py 总是生成 scenarioSlides_resolve_page_types() 又优先使用它,导致模板解析得到的 slidesConfig 和 20/17/18 页结构被覆盖。
  3. 三个模板数据库记录的 style_preset 都是 broker;最终颜色由硬编码 THEMES['broker'] 决定,所以选择不同模板仍会生成同一深蓝风格。
  4. 当前结果中的 appliedTemplateId 只表示模板记录通过校验并进入 Worker不表示源模板视觉被保留属于误导性可观测状态。

前端恢复 draft_options.templateId 会让旧工作区继续显示上次选择,这可能造成“仍在用旧模板”的次级现象,但无法解释本次三个内置模板都退回基础样式;本次主因已经定位在渲染器。

22.3 模板资产本身还存在的兼容风险

  • 三个源文件均没有 PowerPoint 结构化占位符,当前解析器只得到标题、页面类型和 sourceSlide,没有任何“业务字段 → shape/chart/table”绑定关系。不能靠换一个主题色实现动态填充。
  • single_savings.pptx 的图表 XML 含 barGrouping=none/standardartifact-tool 2.8.36 无法导入PowerPoint 自动化需要 OpenAndRepair 才能稳定打开。资产上线前必须规范化并重新保存,不能只以 python-pptx 可读取作为合格标准。
  • source_template_asset_id 当前为 VARCHAR(50),而后台上传返回的 stored://{templateId}/{32位uuid}.pptx 在常见模板 ID 下可能超过 50 字符,造成保存失败、截断或资产记录缺失。
  • 任务只固化模板 ID没有固化资产版本和 hash管理员替换模板后排队任务无法证明自己实际使用的是新文件还是旧文件。

22.4 推荐实现:双渲染模式

保留通用构建器作为兼容回退,但把“真实模板”与“基础主题”明确分成两条链:

clone-edit-v2
  打开指定版本源 PPTX
  → 按 manifest 选择/复制源页面
  → 按 shapeId 原位改文字、表格、图表数据和图片
  → 保留母版、版式、背景、图片裁剪和可编辑对象
  → 结构/视觉 QA

generic-builder-v1
  无源资产或旧式主题模板
  → SLIDE_BUILDERS + THEMES

clone-edit-v2 的硬性约束:

  • 禁止执行“删除全部源幻灯片再重建”。
  • 每个输出页都必须映射到明确的 sourceSlide
  • 三个内置模板分别维护版本化 manifest记录页面用途、保留/可选规则和可编辑元素的 shape ID。
  • 文本必须在原形状中替换;图表更新数据源;表格更新单元格;图片替换时保留原裁剪和几何信息。
  • 源模板没有可用内容槽位时,必须调整模板或显式报错,不允许静默退回基础模板。
  • 删除样例业务数据必须按 manifest 精确执行,禁止遍历并清空所有文本形状,以免误删品牌、页脚和设计文字。

22.5 资产版本模型

新增 insurance_ppt_template_assets,至少包含:

id, template_id, version, storage_uri, sha256, file_size,
slide_count, manifest_json, validation_status, created_at

同时:

  • source_template_asset_id 扩容为 VARCHAR(255),或改为资产表外键。
  • 上传采用“先保存临时文件 → 校验/规范化 → 计算 hash → 创建新版本 → 原子切换当前版本”。
  • 旧版本在仍被 queued/running 任务或历史记录引用时不得物理删除。
  • Worker 只消费任务快照中的 assetId/version/sha256,读取后再次校验 hash不在执行时回查“当前模板版本”。
  • 新工作区默认使用当前版本;旧工作区若保留旧版本,界面必须明确显示版本和更新时间,并提供“切换到最新版”。

22.6 测试与验收

现有 test_renderer_can_reuse_builtin_template_master 只检查输出存在且为 2 页,实际上无法发现源模板页面被删光。应替换为以下门禁:

  1. 三个模板各做一套真实数据端到端测试断言请求、任务快照、Worker 文件 hash 和结果元数据一致。
  2. 断言输出页面全部来自 manifest 映射,不再固定为通用构建器的 12/10/15 页。
  3. 断言单储蓄模板的图表/摘要表、多储蓄模板的双产品图表、储蓄险 + IUL 模板的源图片和组合页面仍存在且可编辑。
  4. 对三个成品计算渲染截图感知 hash三个模板之间必须明显不同并分别通过与各自基准图的差异阈值。
  5. 扫描最终 PPTX确保示例客户、示例保费和示例公司数据已被替换或删除没有模板样例数据泄漏。
  6. PowerPoint/WPS 无修复提示打开,逐页无溢出、破图、空占位符或丢失字体。
  7. 管理员上传新版后创建两个任务:切换前任务仍使用旧 hash切换后任务使用新 hash以验证版本隔离而不是缓存猜测。

完成上述门禁后,任务结果才允许写入:

{
  "templateApplied": true,
  "templateId": "scenario_savings_iul",
  "assetVersion": 2,
  "assetSha256": "...",
  "rendererMode": "clone-edit-v2",
  "templateFallback": false
}

23. 专项修复实施记录2026-07-31

本轮已完成以下修复:

  • 三个内置模板改为 clone-edit-v2:按模板清单选择原始页面并保留背景、图片和版式,不再清空源幻灯片后生成统一深蓝基础模板。
  • 源模板中的样例业务文本、表格和图表会在动态内容写入前清理;模板页数不足或页面映射非法时明确失败,不再静默回退。
  • 模板上传后记录 assetVersionassetSha256;创建任务时固化资产 ID、版本、SHA-256 和渲染模式Worker 严格消费该快照并复核文件哈希。
  • source_template_asset_id 已扩容为 VARCHAR(255);旧模板文件在仍可能被排队任务引用时不再立即物理删除。
  • 海报工作区改为真实缩放画布并从顶部开始滚动;长图不再因垂直居中而无法查看顶部。
  • 海报完成状态增加资源加载阶段:背景、图片和字体全部可用且最终合成保存成功后,任务才显示完成;加载失败会展示可重试错误。
  • 前端生成按钮与后端 15 秒活动任务复用共同拦截快速重复提交,避免一次点击创建多条生成记录。

已完成验证:

  • 三个内置模板均使用真实历史生成契约渲染,输出模式均为 clone-edit-v2,且页面映射与模板分别对应。
  • PowerPoint 可直接打开三个生成文件,无修复提示;源模板的样例金额未出现在成品文本中。
  • IUL 模板的源图片资产在成品中保留;三套模板的最终视觉不再相同。
  • 迁移 migrate_029migrate_030 已在当前 BaoDan 数据库实际执行模板资产字段、版本、SHA-256 和 clone-edit-v2 模式标识已回填。
  • Python 编译、Vue 生产构建及 PPT/海报相关自动化回归均通过。