讲师中心 微信公众号
AI工具推荐 视频效率加速

生成多个PDF并合并:Node.js路由冲突解决方案

酷涛酱_6112

酷涛酱_6112

发布时间:2025-07-31 18:24:11

|

650人浏览过

|

来源于php中文网

原创

生成多个pdf并合并:node.js路由冲突解决方案

在使用 Node.js、Express 和 Puppeteer 构建 PDF 生成服务时,可能会遇到路由冲突问题,尤其是在处理 "all" 参数时。本文档将深入分析此类问题,通过修改路由定义,避免与现有路由产生冲突,最终实现生成多个 PDF 并合并为一个文件的目标。我们将提供详细的代码示例和解决方案,帮助开发者解决类似问题。

在尝试使用 all.pdf 参数生成合并 PDF 时,遇到了 error: invalid input syntax for type bigint: "all" 错误。同时,Puppeteer 报错 Error: Evaluation failed: TypeError: Cannot read properties of null (reading 'remove')。 这表明后端路由配置存在问题,并且前端页面可能存在元素找不到的情况。

问题分析

  1. PostgreSQL 错误:invalid input syntax for type bigint: "all"

    这个错误表明,在数据库查询中,ids 参数被错误地当作一个数字(bigint)类型处理了。这是因为路由 /database/:tipo/:ids/:limite?.pdf 期望 :ids 是一个数字或者数字的逗号分隔列表,而当传入 "all" 时,PostgreSQL 无法将其转换为 bigint 类型。

  2. Puppeteer 错误:TypeError: Cannot read properties of null (reading 'remove')

    这个错误发生在 Puppeteer 的 page.evaluate() 函数中。代码尝试移除页面上的一个 ID 为 gerar-pdf 的元素,但该元素不存在,导致 document.getElementById('gerar-pdf') 返回 null,进而调用 null.remove() 报错。这可能是因为在生成所有 PDF 的情况下,对应的页面结构与单个 PDF 页面的结构不同,导致找不到该元素。

  3. 路由冲突

    根据问题解决者的描述,根本原因是路由冲突。原有的路由 router.get('/:database/:tipo/:id.pdf') 用于生成单个 PDF,而 router.get('/:database/:tipo/:ids/:limite?.pdf') 试图处理多个 PDF 的生成。当 :ids 为 "all" 时,可能会与单个 PDF 的路由发生冲突,导致请求被错误地路由到单个 PDF 生成的路由,从而引发数据库类型转换错误。

解决方案

解决问题的关键在于避免路由冲突。根据问题解决者的经验,将处理多个 PDF 的路由修改为 router.get('/:database/:tipo/all.pdfs') 即可解决问题。以下是详细的步骤和代码示例:

AI舌诊面诊问诊医疗系统+诊疗商城系统源码
AI舌诊面诊问诊医疗系统+诊疗商城系统源码

AI 舌诊面诊问诊医疗系统 + 诊疗商城系统源码是一套结合 AI 辅助诊断与商品转化的健康类程序,基于浩创云、ThinkPHP(PHP)开发,支持微信服务号与 H5 端,适合做中医舌诊面诊问答、健康建议推送以及配套诊疗商品的运营。

下载
  1. 修改路由定义

    将原来的路由定义:

    router.get('/:database/:tipo/:ids/:limite?.pdf', async (req, res) => {
        // ...
    });

    修改为:

    router.get('/:database/:tipo/all.pdfs', async (req, res) => {
        const { database, tipo } = req.params;
        const limite = req.query.limite; // 从 query 参数获取 limite
    
        let idsArray;
    
        const pool = new Pool({
          user: process.env.POSTGRES_USER,
          host: process.env.POSTGRES_HOST,
          database: database,
          password: process.env.POSTGRES_PASSWORD,
          port: process.env.POSTGRES_PORT,
        });
    
        let query = `SELECT DISTINCT id FROM controle_interno.${tipo} ORDER BY id ASC`;
        if (limite) {
          query += ` LIMIT ${limite}`;
        }
        const result = await pool.query(query);
        idsArray = result.rows.map(row => row.id);
    
        const pdfs = [];
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
    
        for (let i = 0; i < idsArray.length; i++) {
          const id = idsArray[i];
          const url = `http://localhost:${port}/${database}/${tipo}/${id}`;
          console.log(url);
          try {
          await page.goto(url, {waitUntil: 'networkidle0'});
          await page.evaluate(() => {
            const button = document.getElementById('gerar-pdf');
            if (button) { // 检查 button 是否存在
              button.remove();
            }
          });
        const pdfBytes = await page.pdf({ format: 'A4', printBackground: true, pageRanges: '1' });
        pdfs.push(await PDFDocument.load(pdfBytes));
      } catch (err) {
        console.error(err.stack);
       }
      }
        await browser.close();
    
        const mergedPdf = await PDFDocument.create();
        for (const pdf of pdfs) {
          const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
          copiedPages.forEach((page) => mergedPdf.addPage(page));
        }
    
        const pdfBytes = await mergedPdf.save();
        const filePath = path.join(__dirname, 'relatorio.pdf');
    
        await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
        await fs.promises.writeFile(filePath, pdfBytes);
    
        res.set({
          'Content-Type': 'application/pdf',
          'Content-Disposition': 'attachment; filename=relatorio.pdf',
          'Content-Length': pdfBytes.length
        });
    
        const stream = fs.createReadStream(filePath);
        stream.pipe(res);
        res.on('finish', async () => {
              // apaga o arquivo do diretório
              await fs.promises.unlink(filePath)});
        });

    同时,如果需要限制数量,可以通过 query 参数传递,例如:/database/tipo/all.pdfs?limite=15。

  2. 处理 Puppeteer 错误

    为了解决 Puppeteer 报错,需要确保在 page.evaluate() 函数中,要移除的元素存在。可以通过条件判断来避免 null 引用错误:

    await page.evaluate(() => {
      const button = document.getElementById('gerar-pdf');
      if (button) { // 检查 button 是否存在
        button.remove();
      }
    });

    这样,只有当 button 存在时,才会执行 remove() 操作。

完整代码示例

const express = require('express');
const { Pool } = require('pg');
const puppeteer = require('puppeteer');
const { PDFDocument } = require('pdf-lib');
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');

const router = express.Router();
const port = process.env.PORT || 3000;

router.get('/:database/:tipo/all.pdfs', async (req, res) => {
  const { database, tipo } = req.params;
  const limite = req.query.limite; // 从 query 参数获取 limite

  let idsArray;

  const pool = new Pool({
    user: process.env.POSTGRES_USER,
    host: process.env.POSTGRES_HOST,
    database: database,
    password: process.env.POSTGRES_PASSWORD,
    port: process.env.POSTGRES_PORT,
  });

  let query = `SELECT DISTINCT id FROM controle_interno.${tipo} ORDER BY id ASC`;
  if (limite) {
    query += ` LIMIT ${limite}`;
  }
  const result = await pool.query(query);
  idsArray = result.rows.map(row => row.id);

  const pdfs = [];
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  for (let i = 0; i < idsArray.length; i++) {
    const id = idsArray[i];
    const url = `http://localhost:${port}/${database}/${tipo}/${id}`;
    console.log(url);
    try {
      await page.goto(url, {waitUntil: 'networkidle0'});
      await page.evaluate(() => {
        const button = document.getElementById('gerar-pdf');
        if (button) { // 检查 button 是否存在
          button.remove();
        }
      });
      const pdfBytes = await page.pdf({ format: 'A4', printBackground: true, pageRanges: '1' });
      pdfs.push(await PDFDocument.load(pdfBytes));
    } catch (err) {
      console.error(err.stack);
    }
  }
  await browser.close();

  const mergedPdf = await PDFDocument.create();
  for (const pdf of pdfs) {
    const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
    copiedPages.forEach((page) => mergedPdf.addPage(page));
  }

  const pdfBytes = await mergedPdf.save();
  const filePath = path.join(__dirname, 'relatorio.pdf');

  await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
  await fs.promises.writeFile(filePath, pdfBytes);

  res.set({
    'Content-Type': 'application/pdf',
    'Content-Disposition': 'attachment; filename=relatorio.pdf',
    'Content-Length': pdfBytes.length
  });

  const stream = fs.createReadStream(filePath);
  stream.pipe(res);
  res.on('finish', async () => {
    // apaga o arquivo do diretório
    await fs.promises.unlink(filePath)});
  });

// 保持原有的单个 PDF 生成路由
router.get('/:database/:tipo/:id.pdf', async (req, res) => {
  // ... (单个 PDF 生成的逻辑)
});

// 保持原有的 ZIP 文件生成路由
router.get('/:database/:tipo/:ids/:limite?.zip', async (req, res) => {
  // ... (ZIP 文件生成的逻辑)
});

module.exports = router;

注意事项

  • 路由设计:在设计路由时,要避免产生歧义和冲突。尽量使用清晰、明确的路由规则,避免使用过于宽泛的参数匹配。
  • 错误处理:在 Puppeteer 的 page.evaluate() 函数中,要进行错误处理,避免因页面结构变化导致的元素找不到错误。
  • 数据库类型:确保传递给数据库查询的参数类型正确。如果需要传递字符串 "all",不要将其作为数字类型处理。
  • 环境变量:确保数据库连接信息(如 POSTGRES_USER, POSTGRES_HOST, POSTGRES_DATABASE 等)已正确配置在环境变量中。

总结

通过修改路由定义,避免与现有路由产生冲突,并添加适当的错误处理,可以解决在使用 Node.js 和 Puppeteer 生成多个 PDF 并合并时遇到的问题。 本文档提供了一个完整的解决方案,包括代码示例和注意事项,希望能帮助开发者解决类似的问题。

热门AI工具

更多
SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

UP简历
UP简历 Hot

一款AI办公效率工具,主要用于基于AI技术的免费在线简历制作工具,适合需要提升相关任务效率的用户。

相关专题

更多
Node.js后端开发与Express框架实践
Node.js后端开发与Express框架实践

本专题针对初中级 Node.js 开发者,系统讲解如何使用 Express 框架搭建高性能后端服务。内容包括路由设计、中间件开发、数据库集成、API 安全与异常处理,以及 RESTful API 的设计与优化。通过实际项目演示,帮助开发者快速掌握 Node.js 后端开发流程。

761

2026.02.10

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

549

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

1698

2024.03.01

scripterror怎么解决
scripterror怎么解决

scripterror的解决办法有检查语法、文件路径、检查网络连接、浏览器兼容性、使用try-catch语句、使用开发者工具进行调试、更新浏览器和JavaScript库或寻求专业帮助等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1009

2023.10.18

500error怎么解决
500error怎么解决

500error的解决办法有检查服务器日志、检查代码、检查服务器配置、更新软件版本、重新启动服务、调试代码和寻求帮助等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3120

2023.10.25

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

1798

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

2844

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

6184

2023.10.24

Vite 项目如何打包和部署
Vite 项目如何打包和部署

Vite项目打包部署专题汇集生产构建、产物预览、输出目录、资源路径、Nginx与GitHub Pages部署及常见白屏和路由404问题的已审核文章,帮助读者完成上线流程。

20

2026.10.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn