背景与目的
- 背景:简道云表单的“新增或修改数据”通过 webhook 推送过来,子表中“发票 PDF 附件”需要自动转成图片,并回填到同一条记录的“图片字段”,兼容一行多个 PDF、多行每行多个 PDF 的场景。
- 目标:在不丢失原附件、不覆盖子表其他字段的前提下,稳定完成 PDF→PNG 转换、文件上传与子表图片字段回填,并具备可观测的日志与可维护的常驻部署。
- 最终目的:采用简道云的自定义打印功能,把表格式申请单数据和其中的发票附件一次同时打印或下载到本地。
总体逻辑与关键链路
- 验签:服务端按 SHA-1 验签规则校验 x-jdy-signature(content=nonce:payload:secret:timestamp),保障来源与完整性。
- 快速 ACK + 后台异步:立即返回 200,后台执行下载→转换→上传→回填,避免简道云 5 秒超时导致重试。
- 子表“全量合并”:必须按文档整体回写子表;先拉完整记录,保留每行的全部子字段,仅更新该行的图片字段,附件字段用有效 key 保留,避免误删。
- 文件事务上传(v5):每个文件都独立申请上传凭证(绑定 filename + content_type),用同一 transaction_id 完成上传与 data/update 回填,杜绝 614 “file exists”。
- 友好命名:沿用原 PDF 的显示名作为附件 filename;图片命名用“对应 PDF 基础名 + 页序”,如 发票A_p1.png、发票B_p1.png,冲突时附短后缀重试。
- 日志:结构化记录“入站/验签、加载与子表行数、逐行处理、PDF 转换、每文件上传、写回、错误与耗时”,便于快速排错。
实现要点(代码层面)
-
依赖与工具
- Node 16(nvm)与 Express、node-fetch@2、form-data。
- PDF 转图片:Poppler 的 pdftoppm(CentOS 7 安装 poppler-utils)。
-
验签
- 从 query 取 nonce/timestamp,对原始请求体字符串 req.rawBody 与 secret 组合,用 SHA-1 计算;GET(连接测试)放行,POST 正式推送验签。
-
子表全量合并
- 用 data/get 拉取完整值(而非 widgets),拿到子表每行的附件对象与行 _id。
- 对每行:保留全部子字段(用 { value: 原值 } 包裹);仅替换图片字段为新 key;附件字段设为有效 key 数组。
- 若 data/get 返回附件没有 key/fileId,仅有 url,则逐个下载原 PDF 并用 v5 上传拿到 key,再写回。
-
上传凭证“一文件一凭证”
- 调用 get_upload_token(app_id, entry_id, transaction_id, filename, content_type) 为每个文件单独申请 token/url。
- 上传时若 614,则重新申请凭证并在文件名末尾加短随机后缀重试一次。
-
友好命名与文本字段同步
- PDF:filename 使用原附件名(保留 .pdf);图片:<pdf基础名>_p<页序>.png。
- 同步写入子表文本字段 pdf_name、images_name;数量写入 pdf_num;行号 row_num 保留原值。
-
分组命名与上传
- 将每份 PDF 的页 Buffer 分组保存(baseName + pages),按该 PDF 的 baseName 为该组页命名并上传,避免所有图片名继承同一个 PDF 名称。
部署与网络
-
进程与端口:pdf-to-image 监听 3001(vip-card-bridge 用 3000 分离)。
-
反向代理(Apache 443):
- /webhook/pdf-to-image → http://127.0.0.1:3001/webhook
- /webhook/vip-card → http://127.0.0.1:3000/webhook
- 保留 SSL 与 ProxyPass/ProxyPassReverse;避免 80→443 重定向造成验签体差异。
-
常驻与日志:systemd + journald
- 服务单元:WorkingDirectory=/opt/pdf-to-image;ExecStart 指向 nvm 的 node 与 server.cjs;EnvironmentFile 指向 .env;Restart=always。
- journald 持久化与上限:/etc/systemd/journald.conf 设置 SystemMaxUse 与 SystemMaxFileSize;journalctl –vacuum-size 或 –vacuum-time 做周期清理。
- 不在 ExecStart 重定向日志文件,统一由 journald 接管。
.env 配置(字段与密钥)
- 端口:PORT=3001。
- 验签密钥:JIANDC_WEBHOOK_SIGNATURE_KEY=…(与简道云数据推送一致)。
- API:JIANDC_API_BASE=https://api.jiandaoyun.com;JIANDC_API_TOKEN=…(含文件与数据更新权限)。
- 主表:JIANDC_APP_ID=…;JIANDC_TABLE_ID=…。
- 子表与字段别名(按你的表单一致):SUBFORM_FIELD_ID=sub_form;SUBFORM_PDF_FIELD_ID=invoice_pdf;SUBFORM_IMAGE_FIELD_ID=invoice_images。
日志建议(结构化)
- 入站/验签:inbound_webhook、sig_check。
- 加锁与加载:data_received、lock_acquired、record_loaded(hasSubform、subRowsCount)。
- 行处理:row_start(rowId、pdfCount)。
- 转换与上传:pdf_converted(pagesCount、timeMs)、upload_pdf_ok/upload_png_ok(filename、key、timeMs);upload_retry_614。
- 写回与完成:subform_update_ok(rowsUpdated、txid、timeMs)、data_done;异常:bg_error;释放锁:lock_released。
常见问题与修复
- 404:反向代理路径不匹配或写在 80 而非 443;确保 /webhook/pdf-to-image 映射到 3001 的 /webhook。
- 验签失败:确认 secret 一致、原始体 req.rawBody 未被中间件改写。
- 614 “file exists”:确保每文件单独申请凭证并绑定 filename+content_type;冲突时改名重试。
- 子表字段被清空:按“全量合并”写回,附件必须是 key 数组;图片合并旧 key;其他字段用 { value: 原值 } 包裹。
运行流程(复盘)
- 简道云表单新增/修改 → webhook(POST,携带签名)→ Apache 443 反代 → pdf-to-image 验签与 ACK → data/get 拉取完整值 → 逐行:下载 PDF→pdftoppm 转 PNG→每文件独立凭证上传(绑定 filename+content_type)→整张子表全量合并回写(附件 key、图片 key、文本字段)→ journald 记录关键节点与耗时。
通过以上方案,pdf-to-image 服务在复杂的子表、多文件、多页场景下保持稳定与可控,既保障了事务一致性与数据完整性,也提供了可读友好的命名与可观测的日志,适合生产环境与技术文章的复盘与分享。