baodan/docs/海报生成问题报告复核与详细修复计划书_20260731.md

754 lines
29 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.

# 海报生成问题报告复核与详细修复计划书
日期2026-07-31
复核对象:`docs/海报生成尺寸模板与导出问题修复报告_20260731.md`
实施范围:`api/insurance/poster/`、`api/insurance/generation/` 中的海报任务、`frontend/src/components/poster/`、`frontend/src/pages/PosterPage.vue` 及对应测试
约束:不修改 BaoDan/Dify 基座文件;所有后端自研改动继续位于 `api/insurance/`;本计划不直接修改业务代码
## 1. 执行摘要
上一份报告对以下主问题判断正确:
- 单图和长图共用一套排版骨架;
- 前端尺寸选项与图片模型支持尺寸不一致;
- 模板只有风格提示能力,没有真正的版式能力;
- `pixelRatio: 2` 使实际导出尺寸失控;
- 最终文件依赖浏览器合成,任务状态与可下载状态存在错位。
但上一份报告不能直接作为开发计划使用,原因是:
1. 它虽然提出了三层尺寸概念,后续方案却再次把 **CSS 逻辑排版宽度****最终 PNG 像素宽度** 混在一起;
2. 它把服务端 Headless Chromium 当成必选修复,没有先验证较小、较快的浏览器导出修复能否满足当前业务;
3. 部分结论属于推断而不是已复现事实,例如图 4 空白一定来自当前系统、长图很容易超过 30 MB
4. 它漏掉了两个重要根因:**渲染数据来源分裂**,以及 **fallback 背景仍会绘制文字**
5. 缺少文件级任务、接口契约、兼容策略、灰度方案、回滚方案和可执行验收用例。
本计划采用“先建立可测基线,再做最小闭环修复,最后决定是否引入服务端浏览器”的顺序。推荐目标工期为 **1319 人日**;若确认必须支持“用户关闭页面后仍自动产出最终海报”,再增加服务端渲染阶段,预计额外 **47 人日**
## 2. 本计划的工作假设
在没有新的产品确认前,按以下假设推进,实施前第 0 阶段必须由产品负责人确认:
- 图 14 表示“完整方案/高信息密度”的设计语言,其中图 2 是多面板拼版参考,不直接作为连续长图的物理尺寸标准;
- 图 57 表示单屏竖版海报的设计语言;
- 单图首发只保留 `2:3``9:16` 两种规格;
- 连续长图使用固定逻辑宽度、内容自动高度,首发只保留一个导出宽度;
- 先保留当前 AI“只生成背景素材”的路线文字、数字、图表和 Logo 由确定性布局渲染;
- 第一版先修好当前浏览器导出链路,服务端 Headless Chromium 是否上线由可靠性门槛决定;
- 当前工作区已经存在其他未提交修改,实施时不得覆盖或重排无关改动。
## 3. 对上一份报告的问题复核
### 3.1 尺寸分层提出正确,但目标模型仍不够准确
报告区分了最终尺寸、HTML 画布尺寸和 AI 素材尺寸,这是正确的。但它随后建议把长图 `canvasWidth` 直接设为 `1242``1080`,仍然混淆了两种宽度。
当前海报组件使用 `28px` 标题、`1218px` 正文、`2032px` padding这套字号更像为约 `390540 CSS px` 的逻辑画布设计。如果直接在 `1080 CSS px` 画布上排版,预览缩小后文字和间距会显得过小;这正是目前视觉密度不足的重要原因之一。
正确模型应为:
```text
逻辑排版尺寸:决定文字、间距、网格和组件构图,例如 414 CSS px 宽
导出缩放倍率:例如 3
最终成品尺寸414 × 3 = 1242 px 宽
AI 素材尺寸:由 hero/full-bleed 素材槽位和供应商能力单独决定
```
因此修复中必须同时存在 `layoutWidth``outputWidth`,禁止继续使用一个 `exportSize` 表达全部含义。
### 3.2 `1242px` 只能作为候选导出宽度,不能直接当成产品标准
图 1、3、4 都是 `1242px` 宽,但这也可能来自手机截图或三倍屏导出。没有渠道规范、文件上传限制和目标设备信息,不能仅凭样图认定所有长图必须输出 `1242px`
计划中将 `1242px` 作为首选候选,同时保留在第 0 阶段改为 `1080px` 的决策点。两者不应同时首发,避免继续扩大测试矩阵。
### 3.3 图 2 和图 4 的结论过于确定
- 图 2 是 `1024×1536` 的多面板拼版。它可能是设计参考,也可能是模型错误输出;没有生成上下文时不能直接判定为失败。
- 图 4 的空白尾部确实符合错误截取范围,但尚未证明它由当前 `min-height` 直接生成。
修复时应把这两项变成复现用例,而不是把推断当成已确认根因。
### 3.4 30 MB 风险没有数据支撑
7 张样图实际文件大小约为 `0.204.60 MB`,均远低于服务端的 30 MB 限制。固定 `pixelRatio: 2` 和 data URL 的确存在浏览器内存风险,但“很容易超过 30 MB”没有被样本证明。
计划应分别记录:
- PNG 压缩后文件大小;
- 解码后的像素总量;
- 浏览器合成峰值内存;
- 上传限制。
文件大小和内存占用不是同一个问题,不能混为一谈。
### 3.5 服务端 Headless Chromium 是可选架构决策,不是第一步
服务端 Chromium 会引入浏览器二进制、字体、worker 内存、Docker 镜像体积、内部渲染鉴权和版本一致性问题。当前项目没有 Playwright/Puppeteer 依赖,直接迁移会扩大改动面。
更稳妥的顺序是:
1. 先修正布局、尺寸契约和浏览器 Blob 导出;
2. 记录真实失败率和关闭页面后的业务要求;
3. 如果必须支持无页面会话生成,或浏览器导出失败率超过门槛,再引入服务端渲染。
### 3.6 审计健康分数对本次决策帮助有限
`6/20` 混入了可访问性、主题和响应式等维度,但本次核心是成品尺寸、版式和导出可靠性。没有运行时页面和用户任务数据,分数存在伪精确感。
本计划改用可验证门槛:真实像素是否一致、内容是否裁切、空白是否超标、关闭页面后是否需要完成、下载成功率和视觉基准是否通过。
### 3.7 报告漏掉了渲染数据来源分裂
当前至少存在三份内容来源:
- 前端画布直接使用 `draft.parsedFields`
- Celery 的 `build_poster_content()` 使用 `product_rules.get("facts") or product_rules`
- 最终 `document_json` 又保存 case 的 `confirmed_data`
`poster_content` 只用于 AI prompt并没有成为 HTML 画布的统一输入。产品小册子的 features、计划书确认数据、字段画像和前端 sections 因而可能不一致。
这会造成“AI 背景理解的是一组卖点,画布展示的是另一组数据”。修复必须先建立唯一 `renderDocument`,不能只改 CSS。
### 3.8 报告漏掉了 fallback 图片会绘制文字
正常 prompt 要求 AI 只生成无文字背景,但 `generate_fallback()` 会在图片中绘制 headline、body 和 CTA。该图片随后又作为 hero 背景,前端 HTML 会再次绘制同样文案,可能产生重复文字、裁切文字或乱码。
fallback 必须改为无文字的纯背景素材,或者在背景生成失败时直接使用模板渐变,不能再生成“半成品海报”。
### 3.9 报告缺少落地约束
原报告没有明确:
- 首发到底支持哪些规格;
- 哪些旧记录继续使用 v1
- 模板数据如何迁移;
- 模式切换后 sections 和 template 如何处理;
- 如何验证 ECharts 已完成渲染;
- 如何灰度、监控和回滚;
- 每个阶段改哪些文件以及完成定义。
以下计划补齐这些内容。
## 4. 目标产品规格
### 4.1 输出模式与规格
首发建议只支持三种格式,避免继续开放不可验证的“自定义尺寸”:
| formatId | 用途 | 逻辑尺寸 | 最终像素 | 高度策略 |
|---|---|---:|---:|---|
| `single_2_3` | 朋友圈/私聊单图 | 512×768 CSS px | 1024×1536 | 固定 |
| `single_9_16` | 竖屏故事/企微 H5 | 540×960 CSS px | 1080×1920 | 固定 |
| `long_1242_auto` | 完整方案长图 | 414×auto CSS px | 1242×auto | 内容驱动 |
第 0 阶段如果确认渠道统一要求 `1080px` 长图,则把最后一项改为 `long_1080_auto`,逻辑宽度仍保持独立,不改变组件排版原则。
首发明确不支持横版、方图、A4、自定义宽高、固定 2160/3240/4320 高度。以后增加格式时,每增加一个格式都必须同时增加视觉基准和导出测试。
### 4.2 内容预算
**单图:**
- 1 个品牌区;
- 1 个主标题,建议不超过 24 个中文字符;
- 1 个副标题,最多 23 行;
- 35 个核心数据;
- 最多 3 个卖点;
- 1 个 CTA
- 1 条精简免责声明;
- 不展示完整收益折线图和多年度卡片矩阵。
**长图:**
- hero
- 计划摘要;
- 收益趋势图(有至少 3 个有效节点时);
- 里程碑数据卡;
- 最多 6 个产品卖点;
- 适配/不适配说明;
- CTA
- 完整免责声明;
- 高度完全由实际可见章节决定,不设置人为 `min-height`
### 4.3 模板定义
首发不做无限可配置模板系统,只增加解决当前问题所需的最少字段:
```text
layoutKey single_hero_data | single_story | long_editorial
supportedModes [single] | [long]
supportedFormats [single_2_3, ...]
themeTokens 背景、文字、卡片、边框、主色、强调色
backgroundSlot hero | full_bleed
version 整数
```
`section_schema`、任意拖拽布局和管理员可视化模板编辑器不进入本次范围。
## 5. 目标技术模型
### 5.1 格式注册表
在后端建立唯一格式注册表,例如:
`api/insurance/poster/format_registry.py`
职责:
- 返回首发格式列表;
- 校验 `formatId``outputMode`
- 提供逻辑尺寸、最终尺寸、最大高度和 AI asset slot
- 拒绝未知格式,不再静默回退。
新增只读接口:
```http
GET /insurance/poster/formats
```
前端从该接口加载选项,避免 TypeScript 与 Python 各维护一份尺寸清单。接口不可用时只回退到内置的三项安全规格,不开放自定义尺寸。
为了兼容旧记录,保留数据库 `export_size` 字段;新记录同时在 `document_json` 中保存 `formatId`、`requestedOutput` 和 `actualOutput`。第一期不为了实际宽高额外增加数据库列,避免不必要迁移。
### 5.2 统一渲染文档
新增后端构建器,例如:
`api/insurance/poster/render_document_builder.py`
统一输出:
```json
{
"schemaVersion": 2,
"formatId": "single_2_3",
"outputMode": "single",
"layoutKey": "single_hero_data",
"copy": {},
"facts": {},
"features": [],
"benefits": [],
"theme": {},
"sections": [],
"background": {},
"complianceRevision": ""
}
```
数据优先级固定为:
1. 人工确认的 case `confirmed_data`
2. 产品小册子规则中的产品卖点和保障规则;
3. 模板默认值;
4. 安全空状态。
Celery prompt、前端预览、最终导出和历史恢复都读取同一份文档不再分别拼装。
### 5.3 画布路由
新增轻量路由组件:
```text
PosterCanvasRouter
├── PosterSingleCanvas
└── PosterLongCanvas
```
章节组件可以复用,但容器、信息预算和排版规则必须独立。`PosterHtmlCanvas` 不再通过 `aspect-ratio + overflow:hidden` 模拟单图。
模式或格式切换时必须执行:
1. 重新计算兼容 layout
2. 过滤不兼容 template
3. 按内容预算规范化 sections
4. 告知用户哪些内容被隐藏,而不是静默裁切;
5. 标记当前最终成品为过期,需要重新导出。
### 5.4 AI 背景素材
图片模型只接收素材槽位规格,不接收最终长图高度:
```text
最终长图1242×auto
逻辑 hero414×360 CSS px
AI 素材:由 provider capability 映射到最接近的竖版或横版背景尺寸
```
图片供应商尺寸解析必须严格:
- 已支持规格明确映射;
- 未支持规格返回可诊断错误;
- 不得无提示回退到 `1024x1792`
- 记录 requested asset size 和 actual asset size
- fallback 只生成无文字渐变/纹理背景。
### 5.5 浏览器导出 v2
第一版继续使用浏览器导出,但必须改成确定性流程:
1. 等待字体完成;
2. 等待全部图片 decode
3. 等待 ECharts `finished` 事件,而不是只等待两个 animation frame
4. 读取未缩放画布的逻辑尺寸;
5. 根据 `outputWidth / layoutWidth` 计算唯一导出倍率;
6. 单图固定输出宽高;长图以真实内容高度计算输出高度;
7. 使用 Blob 导出,不经 base64 data URL 中转;
8. 客户端解码 Blob 核验宽高;
9. 服务端用 Pillow 再次解码并校验;
10. 校验通过后才写入 `export_url` 并显示“可下载”。
长图安全门槛首版建议:
- 最终像素高度超过浏览器验证上限时阻止导出并给出明确提示;
- 不在本次首发中自动降采样,因为静默降低质量会再次造成记录和成品不一致;
- 分片导出作为后续功能,只有真实案例超过上限后再实现。
## 6. 分阶段实施计划
## 阶段 0冻结基线与复现12 人日)
### 目标
把推断变成可重复证据,确认首发格式。
### 任务
1. 保存当前工作区差异清单,标记海报相关未提交改动的负责人;
2. 固定一份脱敏计划书数据、产品规则、模板和文案作为测试 fixture
3. 对当前版本分别导出:
- single 默认规格;
- long 2160
- long 4320
4. 记录选择尺寸、DOM 尺寸、PNG 真实尺寸、像素总量、Blob 大小、上传结果和下载结果;
5. 验证图 4 空白是否可由当前代码稳定复现;
6. 验证图 2 是否属于允许的多面板设计方向;
7. 确认长图首发输出宽度为 1242 或 1080
8. 确认是否存在“用户关闭页面后必须继续完成最终成品”的硬需求。
### 产出
- `tests/fixtures/poster/` 脱敏 fixture
- 当前版本导出基线表;
- 三个首发 formatId 的最终确认;
- 服务端 Headless 是否必须进入本期的决策记录。
### 完成定义
同一 fixture 在同一浏览器连续导出 3 次,能够稳定复现当前尺寸问题,并获得真实 PNG 元数据。
## 阶段 1尺寸契约止血23 人日)
### 后端任务
- 新增 `format_registry.py`
- 新增 `GET /poster/formats`
- `PosterService.generate_poster()` 改为接收 `formatId`
- 旧请求中的 `size` 仅用于兼容映射,无法映射时返回参数错误;
- `PosterImageGenerator` 将最终格式与 AI asset size 分离;
- 删除未知尺寸默认回退;
- `generate_fallback()` 改成无文字背景。
### 前端任务
- `PosterDraft` 新增 `formatId`
- `PosterCreativePanel.vue` 从 formats API 渲染规格;
- 删除 A4、横版、方图和自定义尺寸入口
- outputMode 切换时自动选择第一个兼容格式;
- `buildPosterDocument()` 和自动保存 watcher 纳入 `formatId`
### 主要文件
- `api/insurance/poster/format_registry.py`(新增)
- `api/insurance/poster/routes.py`
- `api/insurance/poster/service.py`
- `api/insurance/poster/image_generator.py`
- `frontend/src/utils/poster-api.ts`
- `frontend/src/composables/usePosterWorkspace.ts`
- `frontend/src/components/poster/workspace/PosterCreativePanel.vue`
- `frontend/src/pages/PosterPage.vue`
### 测试
- 每个 formatId 都能返回唯一逻辑/输出规格;
- 未知 formatId 返回 4xx 业务错误;
-`1024x1792` 记录可映射到安全兼容格式;
- 所有前端可见格式均被后端接受;
- fallback PNG 不包含 headline/body/CTA 绘制逻辑。
### 完成定义
UI 中不存在任何会被后端静默改成其他尺寸的选项。
## 阶段 2统一渲染文档23 人日)
### 后端任务
- 新增 `render_document_builder.py`
- case confirmed facts、product rules、copy、template、profile 合并为 schema v2
- `build_poster_content()` 明确接收 case facts 与 product rules修复当前来源混用
- record 创建时立即保存完整 renderDocument
- Celery 从 renderDocument 构建背景 prompt
- 历史恢复返回原始 schemaVersion禁止用最新规则偷偷改变旧成品。
### 前端任务
- `PosterDraft` 增加 `renderDocument` 或等价强类型字段;
- 画布从 renderDocument 读取 facts、features、benefits、theme 和 sections
- 删除 `defaultFeatures``parsedFields.key_benefits` 临时拼装的逻辑;
- 编辑操作只修改 renderDocument 对应字段。
### 主要文件
- `api/insurance/poster/render_document_builder.py`(新增)
- `api/insurance/poster/content_builder.py`
- `api/insurance/poster/service.py`
- `api/insurance/generation/celery_tasks.py`
- `frontend/src/composables/usePosterWorkspace.ts`
- `frontend/src/components/poster/workspace/PosterStage.vue`
- `frontend/src/pages/PosterPage.vue`
### 测试
- 人工确认数据覆盖 AI 原始解析数据;
- 产品 features 能进入前端画布;
- 同一 renderDocument 生成的 prompt、预览和导出使用相同事实
- schema v1 旧记录仍能恢复;
- 缺失收益表时不显示空图表。
### 完成定义
任意一个展示字段都能追溯到 renderDocument 的唯一字段,不再从三个不同对象临时拼接。
## 阶段 3拆分单图/长图版式与模板46 人日)
### 数据和模板任务
-`poster_templates` 增加最少兼容字段;迁移编号使用实施时的下一个可用编号,避免与当前未提交迁移冲突;
- 为现有模板补齐默认 `layoutKey`、supportedModes 和 supportedFormats
- 旧模板无法识别时回退到明确的 legacy layout不自动套新布局。
### 前端任务
- 新增 `PosterCanvasRouter.vue`
- 新增 `single/PosterSingleCanvas.vue`
- 将现有 `long/PosterHtmlCanvas.vue` 收敛为真正的 `PosterLongCanvas.vue`
- 抽取共享的事实格式化、免责声明和品牌组件,不抽象视觉结构;
- 建立画布级 theme CSS variables
- summary/chart/features 等组件移除硬编码绿色;
- 单图只渲染允许的信息预算;
- 长图移除 `minHeight: export height`
- 模板选择按 mode/format 过滤;
- 切换模式时显示内容调整提示。
### 单图构图要求
- 主视觉是第一视觉焦点;
- 标题、核心数据条和 3 个卖点在一屏内完整可读;
- footer 和免责声明始终可见;
- 不依赖 `overflow:hidden` 裁掉正文。
### 长图构图要求
- 章节间有明确的色块和节奏变化;
- 背景和 theme 贯穿全部章节;
- 内容结束后立即进入 footer不存在人为尾部留白
- 图表和数据卡具有适合 414 CSS px 逻辑宽度的字号。
### 主要文件
- `api/insurance/models/poster_template_model.py`
- `api/insurance/db/migrate_0xx.py`(使用实际下一个编号)
- `api/insurance/admin/ppt_admin_service.py`
- `frontend/src/components/poster/PosterCanvasRouter.vue`(新增)
- `frontend/src/components/poster/single/PosterSingleCanvas.vue`(新增)
- `frontend/src/components/poster/long/PosterHtmlCanvas.vue`
- `frontend/src/components/poster/long/*.vue`
- `frontend/src/components/poster/workspace/PosterCreativePanel.vue`
- `frontend/src/components/poster/workspace/PosterSectionPanel.vue`
### 视觉基准
至少建立三张基准图:
- 单图 2:3
- 单图 9:16
- 标准长图。
使用同一 fixture 与批准样图逐项对比信息层级、主视觉占比、文字可读性、数据完整性、章节节奏、footer 和空白尾部。
### 完成定义
单图和长图在 DOM 结构、内容预算和模板白名单上均相互独立;隐藏任意章节后不会破坏剩余布局。
## 阶段 4可靠导出 v223 人日)
### 前端任务
-`renderComposite()` 拆到独立 `poster-exporter.ts`
- 导出时使用未应用预览 transform 的画布;
- 根据 format spec 计算导出倍率;
- 单图固定宽高,长图读取真实内容高度;
- 将 ECharts ready 纳入资源屏障;
- 使用 Blob API
- 导出后在客户端校验真实像素;
- 导出状态明确区分 `preparing`、`rendering`、`uploading`、`ready`、`failed`
- 对失败给出具体错误,不直接回退下载旧文件。
### 后端任务
- `save_rendered()` 使用 Pillow `verify()` 和重新打开读取尺寸;
- 校验实际尺寸是否符合 format spec
- 将 actualOutput、byteSize 和校验结果保存到 document/extraData
- 只有验证通过才写 `export_url`
- `/download/<id>` 在最终文件未就绪时返回“成品仍在准备/需要重新导出”,不返回模糊的“文件不存在”。
### 主要文件
- `frontend/src/utils/poster-exporter.ts`(新增)
- `frontend/src/pages/PosterPage.vue`
- `frontend/src/components/poster/long/PosterBenefitChart.vue`
- `frontend/src/components/poster/workspace/PosterStage.vue`
- `api/insurance/poster/service.py`
- `api/insurance/poster/routes.py`
### 测试
- `single_2_3` 必须严格输出 1024×1536
- `single_9_16` 必须严格输出 1080×1920
- `long_1242_auto` 宽度严格为 1242高度等于内容测量值乘导出倍率
- 图表在导出图中非空;
- 服务端拒绝损坏 PNG、错误尺寸和不匹配 formatId 的文件;
- 导出失败不会覆盖上一版有效文件;
- 同一文档连续导出三次尺寸一致。
### 完成定义
UI 规格、renderDocument、PNG 真实尺寸和服务端记录四者一致。
## 阶段 5可靠性决策门0.5 人日评审)
完成阶段 4 后,使用至少 20 次真实或仿真导出评估:
- 浏览器最终合成成功率是否达到 99%
- 长图最大像素下是否出现黑图/空图/截断;
- 是否存在明确业务要求:用户提交后关闭页面,仍必须自动完成最终海报;
- worker/Docker 是否允许增加 Chromium 的资源成本。
只有满足以下任一条件,才进入服务端 Headless 阶段:
- 关闭页面后自动成品是硬需求;
- 浏览器导出成功率低于 99%
- 多端浏览器结果无法保持一致;
- 历史任务必须支持无人值守重新渲染。
## 阶段 6条件性服务端 Headless 渲染(额外 47 人日)
### 任务
- 评估 Playwright/Chromium 进入 worker 镜像的体积和内存;
- 建立只接受短时签名 token 的内部 render route
- 固定字体、浏览器版本、viewport、device scale 和 locale
- Celery 在背景素材完成后打开 render route生成最终 PNG
- 最终任务状态只有在 PNG 校验并落盘后才变为 done
- 增加超时、重试、幂等和旧成品保护;
- 保留浏览器本地导出作为人工应急能力。
### 完成定义
用户生成后立即关闭页面,任务仍能在历史记录中完成,并下载与预览一致的最终海报。
## 7. 测试计划
### 7.1 后端单元测试
新增或扩展:
- format registry 映射和拒绝规则;
- renderDocument 数据优先级;
- single/long 字段预算;
- template 兼容过滤;
- fallback 无文字;
- PNG 解码、尺寸和格式校验;
- schema v1/v2 兼容恢复。
建议文件:
- `tests/poster_format_registry_test.py`
- `tests/poster_render_document_test.py`
- `tests/poster_export_validation_test.py`
### 7.2 前端组件测试
当前前端未配置组件测试框架。阶段 3 开始前决定是否引入 Vitest若不引入至少用构建检查和 E2E 覆盖以下逻辑:
- 模式切换自动修正规格与模板;
- single 不渲染 forbidden sections
- long 高度随内容变化;
- export scale 计算;
- 资源未 ready 时禁止导出;
- 导出失败状态和重试。
### 7.3 端到端与视觉回归
固定浏览器、字体和 fixture验证
1. 新建海报;
2. 选择产品并恢复确认数据;
3. 切换 single/long
4. 选择兼容模板;
5. 生成背景;
6. 导出并下载;
7. 使用 Pillow 校验 PNG 元数据;
8. 与批准基准图做截图差异比较;
9. 刷新页面并重新下载;
10. 导出失败后旧成品仍可用。
### 7.4 性能边界
以像素总量而不是文件 MB 作为主要边界:
- 标准单图;
- 标准长图;
- 2 倍典型长图内容;
- 最大允许长图;
- 低内存移动设备只做预览,不要求在移动端导出最大长图时必须成功;如业务要求移动端导出,则服务端渲染直接升级为硬需求。
## 8. 兼容、灰度与回滚
### 8.1 旧记录
- `schemaVersion` 缺失视为 v1
- v1 记录继续使用 legacy renderer不用 v2 规则重新排版;
- v1 可下载文件保持原路径;
- 用户主动选择“升级并重新生成”时才转为 v2。
### 8.2 功能开关
增加项目级配置 `poster_render_v2`
- 关闭:继续使用旧布局和旧导出;
- 开启:新建记录使用 format registry、renderDocument v2 和新布局;
- 灰度:仅管理员/测试用户开启。
### 8.3 数据迁移
- 模板字段只做增量新增,不删除旧字段;
- 给现有模板补兼容默认值;
- 不重写旧 `document_json`
- 迁移脚本编号必须在实施时检查 `api/insurance/db/` 当前最大编号,避免与未提交的 `migrate_029.py`、`migrate_030.py` 冲突。
### 8.4 回滚
- 关闭 `poster_render_v2` 即可停止新链路;
- 新增字段保持 nullable旧代码可忽略
- 新导出文件使用新 revision 文件名,不覆盖旧文件;
- 任何阶段不得删除用户已有海报或背景素材。
## 9. 监控与诊断信息
每次导出至少记录:
```text
recordId
schemaVersion
formatId
layoutKey
logicalWidth/logicalHeight
requestedWidth/requestedHeight
actualWidth/actualHeight
pixelCount
blobByteSize
backgroundProvider/backgroundModel
backgroundRequestedSize/backgroundActualSize
renderDurationMs
uploadDurationMs
validationResult
failureStage/failureCode
```
首发观察指标:
- 背景生成成功率;
- 最终合成成功率;
- 最终文件验证通过率;
- 下载成功率;
- 各阶段 P50/P95 时长;
- 浏览器导出失败原因分布。
## 10. 文件级修改矩阵
| 文件/目录 | 计划改动 | 阶段 |
|---|---|---:|
| `api/insurance/poster/format_registry.py` | 统一格式规格与校验 | 1 |
| `api/insurance/poster/render_document_builder.py` | 构建唯一 renderDocument | 2 |
| `api/insurance/poster/content_builder.py` | 修正 case facts/product rules 合并 | 2 |
| `api/insurance/poster/image_generator.py` | asset size 严格映射、无文字 fallback | 1/2 |
| `api/insurance/poster/service.py` | format 校验、document v2、PNG 校验 | 1/2/4 |
| `api/insurance/poster/routes.py` | formats API、明确下载错误 | 1/4 |
| `api/insurance/generation/celery_tasks.py` | 使用 renderDocument 生成背景 | 2 |
| `api/insurance/models/poster_template_model.py` | 最少模板兼容字段 | 3 |
| `api/insurance/db/migrate_0xx.py` | 增量模板字段迁移 | 3 |
| `frontend/src/composables/usePosterWorkspace.ts` | formatId、renderDocument、v1/v2 恢复 | 1/2 |
| `frontend/src/utils/poster-api.ts` | formats 和 v2 document API | 1/2 |
| `frontend/src/utils/poster-exporter.ts` | 独立确定性导出器 | 4 |
| `frontend/src/components/poster/PosterCanvasRouter.vue` | 单图/长图布局路由 | 3 |
| `frontend/src/components/poster/single/` | 单图专用布局 | 3 |
| `frontend/src/components/poster/long/` | 长图布局与主题 token | 3 |
| `frontend/src/components/poster/workspace/PosterCreativePanel.vue` | 兼容格式和模板选择 | 1/3 |
| `frontend/src/components/poster/workspace/PosterStage.vue` | 预览与未缩放画布管理 | 3/4 |
| `frontend/src/pages/PosterPage.vue` | 简化为编排、状态与持久化 | 1/2/4 |
| `tests/poster_*` | 单元、契约、导出验证 | 全阶段 |
## 11. 明确不做的事项
为了控制改动范围,本期不做:
- 任意自定义宽高;
- A4/横版/方图;
- 所见即所得自由拖拽编辑器;
- 管理员可视化搭建模板;
- 自动生成多页拼版;
- 无真实案例支撑的长图自动分片;
- 在阶段 5 决策前直接引入 Chromium
- 重构无关 PPT、认证、产品推荐或基座代码。
## 12. 最终验收清单
只有以下项目全部通过,才能认为修复完成:
- [ ] 首发仅显示已确认的三种格式;
- [ ] 未知格式不会静默回退;
- [ ] CSS 逻辑尺寸与 PNG 输出尺寸明确分离;
- [ ] single 和 long 使用不同的画布组件与内容预算;
- [ ] 单图没有被 `overflow:hidden` 静默裁掉的内容;
- [ ] 长图内容结束后没有人为 `min-height` 空白;
- [ ] 模板只出现在兼容 mode/format 中;
- [ ] 模板主题贯穿所有章节,不再固定白底绿色;
- [ ] AI 和 fallback 都不绘制任何文案、数字或 Logo
- [ ] prompt、预览、导出读取同一 renderDocument
- [ ] 单图 PNG 像素严格符合规格;
- [ ] 长图 PNG 宽度严格符合规格,高度来自真实内容;
- [ ] 图表、字体和图片全部 ready 后才导出;
- [ ] 服务端能拒绝损坏或错误尺寸的 PNG
- [ ] 导出失败不会覆盖上一版有效成品;
- [ ] 历史记录能正确恢复 v1/v2 文档;
- [ ] 灰度开关与回滚路径验证通过;
- [ ] 是否引入服务端 Headless Chromium 已通过阶段 5 数据决策。
## 13. 推荐执行顺序
严格按以下顺序实施,不并行修改同一链路:
```text
复现与规格确认
→ 格式契约
→ 统一 renderDocument
→ 单图/长图布局
→ 确定性导出与服务端校验
→ 真实可靠性评估
→ 条件性服务端 Headless
```
前一阶段的完成定义未通过,不进入下一阶段。这样可以避免一边改模板、一边改尺寸、一边换渲染器,最终无法定位回归来源。