一句话
合成流水线是一条**”用户提交 → 后台轮询 → Puppeteer 渲染 → 上传成品 → 状态回写”**的异步管道,由 pf-service 内嵌的 Job 驱动;它把”模板 sceneJson + 用户填值”变成可下载、可打印的高清图片。
入口与时序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| t=0 用户在 pf-app 点"提交合成" │ ▼ t=0+ε POST /pf-app/order-template-submissions │ 入库 order_template_submissions(status=pending) │ + N 条 order_template_submission_items(pending) │ t=Δ(≤10s) orderTemplateCompositionJob 轮询命中 │ submission.status = processing │ ▼ t=Δ+T 遍历 items: ├─ 准备 sceneJson + uploads + texts ├─ Puppeteer 渲染 ├─ 截图 → 上传 MinIO └─ item.status = completed │ t=Δ+T+ε 汇总: 全部 completed → submission.status = completed 任一 failed → submission.status = failed │ ▼ t=... 管理员在 pf-manage 看到结果可审核
|
状态机
提交主表 + 明细表共用四态:
1 2 3 4 5
| pending ──► processing ──► completed │ └───────────► failed (item.failReason 写入原因)
pending ──► processing 之后异常退出 → 下一轮被 Job 检测为"僵尸"重新置回 pending
|
任何一条明细失败不影响其它明细继续合成——提交主表只在所有明细处理完后汇总状态。
核心源码
| 文件 |
作用 |
apps/pf-service/src/jobs/orderTemplateCompositionJob.ts |
轮询调度、并发控制、状态汇总 |
apps/pf-service/src/utils/jobProcessor.ts |
合成主流程:合并 sceneJson、调 Puppeteer、上传 |
apps/pf-service/src/utils/puppeteerEventBus.ts |
Puppeteer 实例间事件总线,便于浏览器侧回传日志/错误 |
apps/pf-service/src/utils/exportImg.ts |
截图/导出(PNG/PDF)封装 |
Job 调度细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| let timer = null; let isRunning = false;
export function startOrderTemplateCompositionJob(intervalMs = 10_000) { timer = setInterval(async () => { if (isRunning) return; isRunning = true; try { await runOnce(); } finally { isRunning = false; } }, intervalMs); }
async function runOnce() { }
|
- 单进程内串行:
isRunning 锁确保同一进程不并发
- 多进程时:依赖数据库 row-level lock / Redis 抢占(建议生产期加
SELECT ... FOR UPDATE SKIP LOCKED,当前实现保留扩展点)
- 轮询间隔:
config.server.jobs.composition.intervalMs,默认 10s;可在配置中设 enabled = false 完全停掉
合成主流程(jobProcessor)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| processSubmission(submission) ├─ load items (Drizzle) ├─ for each item: │ ├─ load template.sceneJson 快照 (优先用 item.sceneJson) │ ├─ buildPreviewSceneJson(sceneJson, uploads, texts) │ │ ├─ 按 elementKey 替换 Image.url │ │ ├─ 按 elementKey 替换 Text.text │ │ └─ 对 faceSafe Image 节点: │ │ ├─ 调 pf-face-service 取人脸框 │ │ └─ 重算 cropBox 使脸居中且不被裁掉 │ ├─ processComposition(scene): │ │ ├─ 启动/复用 Puppeteer 实例 │ │ ├─ 加载内嵌的 Leafer 渲染页面 │ │ ├─ window.__renderScene(scene) │ │ ├─ 等待字体/图片加载完成 │ │ ├─ page.screenshot 或 PDF │ │ └─ 上传 MinIO → 得到 url │ ├─ 更新 item: │ │ status=completed │ │ composedImageUrls=[url1, url2, ...] │ └─ 失败: │ status=failed, failReason=err.message └─ 汇总 submission 状态
|
buildPreviewSceneJson 是关键——它是同一份”sceneJson + 用户填值合并”逻辑,编辑器预览、用户端预览、后端合成共用它,保证三端视觉一致。
Puppeteer 实例管理
- 启动时复用一个/几个长连接 Browser,按需开 Page
- 每张图渲染走独立 Page,避免上下文污染
- 内嵌静态 HTML(Leafer 已打包进去),不依赖外网 CDN
- 字体通过
page.evaluate 注入 FontFace,确保与编辑器同源
- 通过
puppeteerEventBus.ts 把浏览器端 console / error 桥接回 Node 日志
错误分类
jobProcessor 给每个错误附 CompositionErrorContext,便于排障:
1 2 3 4 5 6 7 8 9 10 11
| { scope: "process-item" | "process-submission-item" | "run-once", orderNo?: string, submissionId?: number, itemId?: number, instanceKey?: string, compositionKey?: string, uploadCount?: number, textCount?: number, submissionsCount?: number, }
|
常见 failReason:
素材 URL 不可访问 — MinIO 权限/网络
字体加载超时 — font URL 不通或 woff2 损坏
face-service 异常 — 模型未加载或被打挂
Puppeteer 渲染超时 — 模板复杂或大图未压缩
重试策略
- 单 item 重试:管理员在
pf-manage 把 item.status 改回 pending → 下一轮 Job 自动处理
- 整 submission 重试:把所有失败 item 一起置回
pending
- 自动重试:当前实现未默认开启自动重试,避免”错误内容反复跑”占资源——建议结合监控按
failReason 分类做有限次重试
性能与扩展
| 维度 |
默认 |
升级路径 |
| 调度并发 |
1(单进程串行) |
多进程 + DB 抢占锁 / Redis 队列 |
| 渲染并发 |
Browser 数 × Page 数 |
拓展 Browser 池 / 拓 GPU 节点 |
| 渲染时间 |
单张约 0.5–3 s(视模板复杂度) |
提前抽帧 / 复用素材缓存 |
| 输出格式 |
PNG / PDF |
增加 TIFF / 高 DPI 模式 |
监控建议
最少接两个指标:
pending 队列长度 — 反映堆积
- 单条 item 处理耗时 P95 — 反映健康
加上 failReason 分组统计,能让你在不打开数据库的情况下知道”今天是 face-service 挂了还是模板里有死链”。
下一步