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

1284 lines
54 KiB
Markdown
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 与海报优化修复计划书
> 版本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. P0PPT 无法生成专项修复
### 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 字段矩阵
#### 储蓄险
单图:
- 必需:产品名、保司名、币种、年缴保费、缴费年期。
- 推荐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.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. 分阶段交付计划
### 阶段 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}`
日志必须包含:
```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/海报相关自动化回归均通过。