# 保险智能客服系统 PPT 与海报优化修复计划书 > 版本:v1.1 > 日期:2026-07-31 > 范围:PPT 生成、海报生成、保司管理、产品管理、PPT 模板管理、文案模板管理 > 原则:先恢复可用性,再统一配置与数据口径,最后补齐管理能力;仅修改 `api/insurance/`、`frontend/`、`tests/` 和自定义迁移,不修改 BaoDan 基座业务文件。 ## 1. 执行摘要 本轮需求应拆成四个层次推进: 1. **P0 恢复 PPT 生成**:当前 PPT 一直生成不出来存在已确认的代码级直接根因。`generate_ppt_task()` 在异常捕获块之前访问未定义变量 `task.workspace_id`,Worker 领取任务后会立即抛出 `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_id` 仅 `VARCHAR(50)`,而 `stored://{templateId}/{uuid}.pptx` 可超过 50 字符;任务也未固化资产版本/hash | 扩容字段并建立版本化模板资产与任务快照 | P0/P1 | ## 4. P0:PPT 无法生成专项修复 ### 4.1 已确认直接根因 调用链: ```text 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`,同步工作区状态。 推荐的最小修复结构: ```python 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_022`~`migrate_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 数据模型 在新迁移中增加: ```text 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 服务端策略 新增统一解析函数,例如: ```text 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 必填与非阻断规则 建议将校验分为三级: ```text 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.py`:`validated` 只由 BLOCKER 数量决定。 - `celery_tasks.py`:生成阶段不得重新把 WARNING 当成 error 阻断。 - `PptDataReview.vue`:补字段、选项、来源和非阻断交互。 - `fast_pptx_renderer.py`:字段缺失时隐藏相关卡片,不制造默认值。 ## 8. 海报按险种和单图/长图的字段画像 ### 8.1 当前问题 当前海报前端主要根据 `parsedFields` 中是否存在字段决定展示;`outputMode=single/long` 主要只控制画布固定比例或自动高度。系统还没有“险种 × 版式”的字段选择层,因此会出现: - 单图塞入过多表格和事实,信息密度过高; - 长图仍只显示单图级摘要,信息不足; - 储蓄险、重疾险、IUL 使用同一套卡片; - 某险种不存在的字段被误判为缺失; - 生成 Prompt 和 HTML 画布可能使用不同字段。 ### 8.2 推荐架构 新增单一字段画像配置,不在多个组件中各写一套判断: ```text PosterFieldProfile planType: savings | ci | iul | other outputMode: single | long required: [] recommended: [] optional: [] forbidden: [] maxFeatureCount milestoneYears ``` 流程: ```text 产品资料/计划书 → 统一事实模型 → 按险种选择 FieldProfile → 按 single/long 裁剪内容 → 同一 ViewModel 同时供 HTML 预览、AI Prompt、最终导出使用 ``` ### 8.3 字段矩阵 #### 储蓄险 单图: - 必需:产品名、保司名、币种、年缴保费、缴费年期。 - 推荐:2~3 个已审核产品卖点、一个可核验里程碑数据。 - 可选:基本计划名义金额、保障期、投保年龄。 - 不展示:完整利益演示表、完整退保提取表、过多年度数据。 长图: - 必需:单图必需字段。 - 推荐:投保条件、总计划保费、10/20/30 年里程碑、保证/非保证拆分、回本提示。 - 可选:首年应缴金额、提取方案、完整产品亮点。 - 利益演示不足时隐藏图表,不阻断长图生成。 #### 重疾险(CI) 单图: - 必需:产品名、保司名、币种、基本保额。 - 推荐:年缴保费、缴费年期、核心疾病/保障层级、等待期。 - 条件字段:投保年龄、性别、吸烟状态。 - 不展示:储蓄险回本倍数、退保价值营销结论。 长图: - 必需:单图必需字段。 - 推荐:保障项目列表、早期/严重疾病结构、额外赔付、等待期、保障期限。 - 可选:豁免、身故保障、特定疾病、年龄/吸烟条件。 - 保障列表为空时只允许简版长图,并标记 WARNING。 #### IUL 单图: - 必需:产品名、保司名、币种、名义保额/身故保障。 - 推荐:目标保费或计划保费、缴费方式、一个指数账户核心机制。 - 条件字段:投保年龄、性别、吸烟状态。 - 不展示:未经确认的收益率承诺。 长图: - 必需:单图必需字段。 - 推荐:目标/最低保费、指数账户、封顶率/参与率/保底机制、关键年度账户价值和退保价值。 - 可选:贷款/提取说明、费用说明、首年应缴金额。 - 所有非保证数据必须带演示口径和免责声明。 #### 其他险种 - 使用通用保守画像:产品名、保司名、保障对象、核心保障、适用人群、免责声明。 - 未建立专属画像前,不展示收益图表或推导指标。 ### 8.4 单图与长图的内容预算 | 项目 | 单图 | 长图 | |---|---:|---:| | 主标题 | 1 | 1 | | 副标题/正文 | 1~2 段 | 2~4 段 | | 核心数字卡 | 2~4 个 | 4~8 个 | | 卖点 | 2~3 个 | 3~6 个 | | 图表 | 0~1 个 | 0~3 个 | | 年度数据 | 1 个里程碑 | 3~6 个里程碑 | | 免责声明 | 必须 | 必须、可更完整 | ### 8.5 改造位置 - 后端新增 `poster/field_profiles.py` 或等价配置模块。 - `product_source_resolver.py` 输出统一事实模型,不直接决定视觉字段。 - `content_builder.py` 根据画像构建 Poster ViewModel。 - `copy_generator.py` 和 `image_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 接口 ```text 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. 根据实际产品计算 `detectedScenario` 和 `generationMode`。 2. 自动推荐: - 先匹配 `base_scenario=detectedScenario`; - 再按适用保司、产品和状态过滤。 3. 手动选择自定义场景模板: - 不再要求 `template.scenario_tag == detectedScenario`; - 校验模板场景的 `generation_mode` 与实际 mode 一致; - 继续校验适用保司和适用产品。 4. 任务快照同时保存: - `detectedScenario` - `selectedScenario` - `generationMode` 这样既允许业务新增场景,又不破坏底层计算规则。 ## 10. 四类后台删除能力 ### 10.1 删除策略 这些对象会被历史记录、生成任务、产品、Logo、模板适用范围或文件引用。直接物理删除会破坏追溯,因此统一采用: ```text 默认:软删除(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 与交互 新增: ```text 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} # 改软删除语义 ``` 统一响应: ```json { "code": 0, "message": "success", "data": { "id": "xxx", "deleted": true, "deletedAt": "2026-07-31T12:00:00" } } ``` 引用冲突: ```json { "code": 1409, "message": "该保司仍有 3 个产品,无法删除", "data": { "reason": "COMPANY_HAS_PRODUCTS", "references": [] } } ``` 前端要求: - 仅 `config_manage` 权限可见删除按钮。 - 二次确认显示对象名称和影响。 - 禁止只用无上下文的“确定删除?”。 - 删除成功后刷新当前分页,不跳回第一页。 - 409 时展示具体依赖和处理入口。 - 所有删除写入 `system_operation_logs`。 ## 11. 数据库迁移计划 建议分两个迁移,降低回滚复杂度。 ### 11.1 `migrate_027`:策略与软删除 新增: ```text 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 生成 请求中移除: ```text useMaskedData ``` 任务快照新增: ```json { "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 校验 校验响应新增: ```json { "canProceed": true, "blockerCount": 0, "warningCount": 3, "infoCount": 1 } ``` 兼容旧字段: - 一个版本内继续返回 `validated = blockerCount == 0`。 - 前端改用 `canProceed`。 ### 12.4 海报 海报创建和文案生成请求不再接收 `useMaskedData`。 响应/任务快照增加: ```json { "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/脱敏策略写入 DeckContract;按 `rendererMode` 分流真实模板与通用构建器 | | `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. 分阶段交付计划 ### 阶段 A:P0 热修(建议 0.5~1 人日) - 修复未定义 `task`。 - 把状态同步纳入异常捕获。 - 清理 stale 任务。 - 修复失败的提取器测试 mock。 - 增加 Worker 任务回归测试。 - 在部署环境确认 022~026 迁移、队列和共享存储。 验收门槛:PPT 可稳定生成,任务状态闭环。 ### 阶段 A2:P0 模板真实套用(建议 3~5 人日) - 引入 `clone-edit-v2`,从所选源 PPTX 开始原位编辑,不再先删光源页面。 - 为三个内置模板建立逐页、逐形状的数据绑定清单。 - 修复模板图表 OOXML 兼容性并完成 PowerPoint/WPS 打开测试。 - 扩容模板资产标识,固化任务使用的版本和 SHA-256。 - 将当前“文件存在 + 页数正确”的测试升级为模板结构和视觉保真测试。 验收门槛:三个模板各生成一次,成品设计明显互异,且均保留源模板独有视觉签名、可编辑对象和正确资产 hash。 ### 阶段 B:后台策略与 PPT 核验(建议 3~5 人日) - `migrate_027`。 - 保司/产品脱敏开关、保司 Logo 开关。 - 移除 PPT/海报用户脱敏开关。 - 新增吸烟、币种和三个条件字段。 - 重构校验分级。 - 支持结果页返回修改和版本化重生成。 验收门槛:四种脱敏组合、Logo 开关、非阻断校验全部通过。 ### 阶段 C:海报字段画像(建议 3~5 人日) - 建立统一事实模型和六套核心画像。 - 单图/长图内容预算。 - 预览、Prompt、导出共用 ViewModel。 - 建立至少 12 个基准输出。 验收门槛:无跨险种字段、无虚构数字、布局不过载。 ### 阶段 D:动态场景与删除能力(建议 3~4 人日) - `migrate_028`。 - 场景管理接口和页面。 - 模板匹配逻辑拆分。 - 四类对象软删除、依赖检查、审计日志和回收站。 验收门槛:自定义场景可完整使用,删除不破坏历史。 ### 阶段 E:回归与灰度(建议 2~3 人日) - 全量后端测试、前端构建、容器冒烟。 - 真实脱敏样本 UAT。 - 灰度开启新策略。 - 观察任务失败率、平均生成时间和 stale 任务数。 总估算:约 14.5~23 人日,不包含业务方准备样本、设计稿调整和外部 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}` 日志必须包含: ```text 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` 归一化为 `USD`,`RMB/¥` 归一化为 `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/appliedTemplateId` 和 `templateFallback=false`。因此这不是“用户没有选中模板”,也不是内置文件缺失。 ### 22.2 直接根因 真实调用链为: ```text 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/standard`;artifact-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 推荐实现:双渲染模式 保留通用构建器作为兼容回退,但把“真实模板”与“基础主题”明确分成两条链: ```text 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`,至少包含: ```text 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,以验证版本隔离而不是缓存猜测。 完成上述门禁后,任务结果才允许写入: ```json { "templateApplied": true, "templateId": "scenario_savings_iul", "assetVersion": 2, "assetSha256": "...", "rendererMode": "clone-edit-v2", "templateFallback": false } ``` ## 23. 专项修复实施记录(2026-07-31) 本轮已完成以下修复: - 三个内置模板改为 `clone-edit-v2`:按模板清单选择原始页面并保留背景、图片和版式,不再清空源幻灯片后生成统一深蓝基础模板。 - 源模板中的样例业务文本、表格和图表会在动态内容写入前清理;模板页数不足或页面映射非法时明确失败,不再静默回退。 - 模板上传后记录 `assetVersion` 与 `assetSha256`;创建任务时固化资产 ID、版本、SHA-256 和渲染模式,Worker 严格消费该快照并复核文件哈希。 - `source_template_asset_id` 已扩容为 `VARCHAR(255)`;旧模板文件在仍可能被排队任务引用时不再立即物理删除。 - 海报工作区改为真实缩放画布并从顶部开始滚动;长图不再因垂直居中而无法查看顶部。 - 海报完成状态增加资源加载阶段:背景、图片和字体全部可用且最终合成保存成功后,任务才显示完成;加载失败会展示可重试错误。 - 前端生成按钮与后端 15 秒活动任务复用共同拦截快速重复提交,避免一次点击创建多条生成记录。 已完成验证: - 三个内置模板均使用真实历史生成契约渲染,输出模式均为 `clone-edit-v2`,且页面映射与模板分别对应。 - PowerPoint 可直接打开三个生成文件,无修复提示;源模板的样例金额未出现在成品文本中。 - IUL 模板的源图片资产在成品中保留;三套模板的最终视觉不再相同。 - 迁移 `migrate_029`、`migrate_030` 已在当前 BaoDan 数据库实际执行,模板资产字段、版本、SHA-256 和 `clone-edit-v2` 模式标识已回填。 - Python 编译、Vue 生产构建及 PPT/海报相关自动化回归均通过。