# 海报生成问题报告复核与详细修复计划书 日期: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. 缺少文件级任务、接口契约、兼容策略、灰度方案、回滚方案和可执行验收用例。 本计划采用“先建立可测基线,再做最小闭环修复,最后决定是否引入服务端浏览器”的顺序。推荐目标工期为 **13~19 人日**;若确认必须支持“用户关闭页面后仍自动产出最终海报”,再增加服务端渲染阶段,预计额外 **4~7 人日**。 ## 2. 本计划的工作假设 在没有新的产品确认前,按以下假设推进,实施前第 0 阶段必须由产品负责人确认: - 图 1~4 表示“完整方案/高信息密度”的设计语言,其中图 2 是多面板拼版参考,不直接作为连续长图的物理尺寸标准; - 图 5~7 表示单屏竖版海报的设计语言; - 单图首发只保留 `2:3` 和 `9:16` 两种规格; - 连续长图使用固定逻辑宽度、内容自动高度,首发只保留一个导出宽度; - 先保留当前 AI“只生成背景素材”的路线,文字、数字、图表和 Logo 由确定性布局渲染; - 第一版先修好当前浏览器导出链路,服务端 Headless Chromium 是否上线由可靠性门槛决定; - 当前工作区已经存在其他未提交修改,实施时不得覆盖或重排无关改动。 ## 3. 对上一份报告的问题复核 ### 3.1 尺寸分层提出正确,但目标模型仍不够准确 报告区分了最终尺寸、HTML 画布尺寸和 AI 素材尺寸,这是正确的。但它随后建议把长图 `canvasWidth` 直接设为 `1242` 或 `1080`,仍然混淆了两种宽度。 当前海报组件使用 `28px` 标题、`12~18px` 正文、`20~32px` padding,这套字号更像为约 `390~540 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.20~4.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 个副标题,最多 2~3 行; - 3~5 个核心数据; - 最多 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 逻辑 hero:414×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:冻结基线与复现(1~2 人日) ### 目标 把推断变成可重复证据,确认首发格式。 ### 任务 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:尺寸契约止血(2~3 人日) ### 后端任务 - 新增 `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:统一渲染文档(2~3 人日) ### 后端任务 - 新增 `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:拆分单图/长图版式与模板(4~6 人日) ### 数据和模板任务 - 为 `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:可靠导出 v2(2~3 人日) ### 前端任务 - 将 `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/` 在最终文件未就绪时返回“成品仍在准备/需要重新导出”,不返回模糊的“文件不存在”。 ### 主要文件 - `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 渲染(额外 4~7 人日) ### 任务 - 评估 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 ``` 前一阶段的完成定义未通过,不进入下一阶段。这样可以避免一边改模板、一边改尺寸、一边换渲染器,最终无法定位回归来源。