背景与目的

  • 背景:简道云表单的“新增或修改数据”通过 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 服务在复杂的子表、多文件、多页场景下保持稳定与可控,既保障了事务一致性与数据完整性,也提供了可读友好的命名与可观测的日志,适合生产环境与技术文章的复盘与分享。