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

生成多个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’)。 这表明后端路由配置存在问题,并且前端页面可能存在元素找不到的情况。

问题分析

PostgreSQL 错误:invalid input syntax for type bigint: “all”

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

Puppeteer 错误:TypeError: Cannot read properties of null (reading ‘remove’)

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

路由冲突

根据问题解决者的描述,根本原因是路由冲突。原有的路由 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’) 即可解决问题。以下是详细的步骤和代码示例:

修改路由定义

将原来的路由定义:

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  {        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。

处理 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  {        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 并合并时遇到的问题。 本文档提供了一个完整的解决方案,包括代码示例和注意事项,希望能帮助开发者解决类似的问题。

以上就是生成多个PDF并合并:Node.js路由冲突解决方案的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1512912.html

赞 (0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
生成多页PDF时Node.js路由冲突及解决方案
上一篇 2025年12月20日 07:24:32
JavaScript 数组开头出现逗号的解决方案
下一篇 2025年12月20日 07:24:43

相关推荐

  • 如何在mysql中创建外键索引

    创建表时定义外键会自动创建索引,如CREATE TABLE orders含FOREIGN KEY(user_id)则user_id自动索引;2. 已有表添加外键前需先手动建索引,如CREATE INDEX idx_user_id ON orders(user_id),再ALTER TABLE加外键约…

    2026年9月28日
    300
  • 夸克AI最新官方主页地址 夸克AI人工智能助手直达入口链接

    夸克AI最新官方主页地址是https://www.quark.cn/,提供AI搜索、文档处理、云端存储及多端协同服务,支持格式转换、内容生成与智能摘要功能。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 夸克AI最新官方主页地址在哪里?这是…

    2026年9月28日
    000
  • 虚拟机安装以及PCL的配置(1)

    虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)虚拟机安装以及PCL的配置(1)

    在windows系统下安装虚拟机的步骤如下(这些步骤同样适用于在虚拟机中配置ubuntu系统或双系统配置pcl环境): (1) 下载VMware并进行安装(可以通过百度搜索找到多个可供下载的资源)。 (2) 安装步骤: 双击下载的安装文件,按照提示点击“下一步”,无需更改默认安装路径(当然你也可以选…

    2026年9月28日 • 用户投稿
    200
  • 续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”

    续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”续航一般多久够用?友望洗地机:长续航+强性能,全屋清洁“一劳永逸”

    在家庭清洁场景中,洗地机凭借高效省力的特点,正逐步成为现代家庭的清洁“主力军”。然而面对琳琅满目的产品型号,消费者仍有不少疑问:洗地机究竟适合多大面积的空间?选购时应重点关注哪些功能?续航时间多久才够用?今天,我们将从真实用户需求出发,结合友望最新推出的大头pro洗地机,深入解析这些常见问题。 一、…

    2026年9月28日 • 用户投稿
    200
  • 如何在 Android 中保存动态创建的复选框状态

    如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态如何在 Android 中保存动态创建的复选框状态

    本文介绍了如何在 Android 应用中保存动态创建的复选框的状态,以便用户在重新打开应用或界面后,复选框的选中状态能够保持不变。我们将探讨使用 SharedPreferences 来持久化复选框状态的方法,并提供示例代码帮助你理解和实现。 使用 SharedPreferences 持久化复选框状态…

    2026年9月28日 • 用户投稿
    000
  • 如何在Android中保存动态创建的CheckBox的状态

    如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态如何在Android中保存动态创建的CheckBox的状态

    本文旨在帮助开发者解决在Android应用中动态创建的CheckBox的状态保存问题。通过利用Shared Preferences,我们可以有效地存储CheckBox的选中状态,确保用户在重新进入应用或页面时,CheckBox的状态能够被正确恢复,从而提供更佳的用户体验。本文将提供详细的步骤和示例代…

    2026年9月28日 • 用户投稿
    100
  • 第五代高通骁龙8至尊版正式发布:全球最快移动SoC

    第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC第五代高通骁龙8至尊版正式发布:全球最快移动SoC

    在今日举行的骁龙峰会上,高通正式发布了其最新旗舰移动平台——第五代骁龙 8 至尊版(Snapdragon 8 Elite Gen 5),并宣称该芯片为“全球速度最快的移动 SoC”。 此次发布的芯片基于台积电最新的第三代3nm N3P工艺打造,在CPU架构上采用了全新的Oryon核心设计,延续了2+…

    2026年9月28日 • 用户投稿
    000
  • Perplexity AI比Google好吗 与传统搜索引擎对比

    Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比Perplexity AI比Google好吗 与传统搜索引擎对比

    perplexity ai 的最大优势在于对话式搜索与实时检索的结合,能自然理解提问意图并提供结构化答案,适合快速获取信息;2. google 在全面性、稳定性与权威性方面仍占优势,适合深度调研和查找权威资料;3. 两者使用体验各有侧重,perplexity ai 提升效率,google 保障内容深…

    2026年9月28日 • 用户投稿
    100
  • Java中ArrayList引用传递陷阱:避免数据意外修改的策略

    Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略Java中ArrayList引用传递陷阱:避免数据意外修改的策略

    本文探讨了Java中ArrayList作为引用类型在对象构造时可能导致的数据意外修改问题。当将同一个ArrayList实例传递给多个对象后,对该列表的后续操作(如清空或添加元素)会影响所有引用它的对象。核心解决方案是为每个需要独立数据副本的对象,实例化一个新的ArrayList,从而确保数据隔离和一…

    2026年9月28日 • 用户投稿
    000
  • 豆包AI如何实现图像识别?教你搭建计算机视觉模型

    豆包AI如何实现图像识别?教你搭建计算机视觉模型豆包AI如何实现图像识别?教你搭建计算机视觉模型豆包AI如何实现图像识别?教你搭建计算机视觉模型豆包AI如何实现图像识别?教你搭建计算机视觉模型

    豆包ai本身不直接提供图像识别模型训练功能,但可结合第三方工具实现。1. 准备数据集:收集高质量、多样化的图像并划分训练集与验证集,或使用公开数据集。2. 搭建模型结构:采用迁移学习方法,选用resnet等预训练模型,调整输出层并加入防止过拟合的机制,豆包ai可生成代码框架。3. 训练与调参:设置合…

    2026年9月28日 • 用户投稿
    100
  • 武侠世界起航指南:从萌新到高手的全章节精要攻略

    武侠世界起航指南:从萌新到高手的全章节精要攻略武侠世界起航指南:从萌新到高手的全章节精要攻略武侠世界起航指南:从萌新到高手的全章节精要攻略武侠世界起航指南:从萌新到高手的全章节精要攻略

    踏入江湖的第一步,如何走稳走远?这份深度章节指南助你精准规划,避开弯路,高效解锁绝世武功与隐藏机缘! 第一章:初入江湖 – 筑基破局 核心目标: 击败管家 + 两名教头(新手战力检验) 与张风对话并切磋取胜(开启江湖路) 隐藏门派的钥匙(散人必看): 在朱宇处习得一气功(基础内功)!这是…

    2026年9月28日 • 用户投稿
    000
  • Android动态复选框状态持久化:SharedPreferences实践指南

    Android动态复选框状态持久化:SharedPreferences实践指南Android动态复选框状态持久化:SharedPreferences实践指南Android动态复选框状态持久化:SharedPreferences实践指南Android动态复选框状态持久化:SharedPreferences实践指南

    本教程详细阐述了如何在Android应用中持久化动态创建的复选框状态。通过利用SharedPreferences这一轻量级数据存储机制,我们能够确保用户在勾选或取消勾选动态生成的复选框后,其状态即使在应用重启或Activity重建后也能得以保留。文章将提供具体的代码示例和实现步骤,帮助开发者构建更具…

    2026年9月28日 • 用户投稿
    000
  • 笔尖AI语音识别不灵敏:灵敏度调整与方言适配技巧

    笔尖AI语音识别不灵敏:灵敏度调整与方言适配技巧笔尖AI语音识别不灵敏:灵敏度调整与方言适配技巧笔尖AI语音识别不灵敏:灵敏度调整与方言适配技巧笔尖AI语音识别不灵敏:灵敏度调整与方言适配技巧

    笔尖ai语音识别不灵敏可通过调整灵敏度、优化环境设置、进行方言适配等方式解决。首先,检查设置中的语音识别选项,通过滑块或数值逐步提高或降低灵敏度,根据使用场景选择合适的配置文件,并确保麦克风位置正确或更换高质量麦克风;其次,进行方言适配时,先检查语言设置是否有方言选项,若无则可自定义词汇并建立方言与…

    2026年9月28日 • 用户投稿
    000
  • ChatSonic 创作 SEO 文案?关键词嵌入指令技巧​

    ChatSonic 创作 SEO 文案?关键词嵌入指令技巧​ChatSonic 创作 SEO 文案?关键词嵌入指令技巧​ChatSonic 创作 SEO 文案?关键词嵌入指令技巧​ChatSonic 创作 SEO 文案?关键词嵌入指令技巧​

    要写出高质量、能排名的 seo 文案,不能只依赖 chatsonic,还需掌握关键词嵌入技巧并对内容进行深度加工。1. 明确目标关键词与长尾关键词,专注几个核心词;2. 在 prompt 中明确指定关键词及出现位置,如标题、段首段尾等,但避免堆砌;3. 对生成内容进行润色,使其更自然流畅,并加入个人…

    2026年9月28日 • 用户投稿
    100
  • VSCode如何集成Git版本控制 VSCode中Git操作的便捷技巧

    首先确认git已安装并配置好用户名和邮箱;2. vscode通常自动检测git,若未检测到可手动在设置中指定git.path;3. 在vscode中打开项目并使用内置终端运行git init初始化仓库;4. 通过左侧源代码管理图标暂存、提交和推送更改;5. 遇到提交乱码时将files.encodin…

    2026年9月28日
    200
  • 和豆包一样的ai图片生成工具2025推荐top10

    2025年AI图片生成工具选择多样,boardmix因支持文生图、图生图、AI抠图、多种风格及在线协作,适合初学者与团队使用,且提供免费版,成为易用性高、功能全面的优选之一。 ☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜ 2025年,想找个…

    2026年9月28日
    000
  • Android RecyclerView优化:通过DiffUtil实现增量更新

    Android RecyclerView优化:通过DiffUtil实现增量更新Android RecyclerView优化:通过DiffUtil实现增量更新Android RecyclerView优化:通过DiffUtil实现增量更新Android RecyclerView优化:通过DiffUtil实现增量更新

    本教程旨在解决RecyclerView在数据更新时(尤其是新增数据)出现的全量刷新和闪烁问题。通过详细介绍Android DiffUtil机制,我们将学习如何高效地进行列表项的增量更新,从而提升用户体验,避免不必要的UI重绘,特别适用于实时聊天等频繁数据变动的场景。 在开发Android应用时,Re…

    2026年9月28日 • 用户投稿
    100
  • 豆包AI安装需要哪些运行时库 豆包AI系统依赖项完整清单

    豆包AI安装需要哪些运行时库 豆包AI系统依赖项完整清单豆包AI安装需要哪些运行时库 豆包AI系统依赖项完整清单豆包AI安装需要哪些运行时库 豆包AI系统依赖项完整清单豆包AI安装需要哪些运行时库 豆包AI系统依赖项完整清单

    #%#$#%@%@%$#%$#%#%#$%@_b05121b5eff2c++ee27d5b7d6a4dd8f2af运行需要python 3.8+、numpy、pandas、requests、torch/tensorflow、transformers、gradio/streamlit等核心库;操作系统…

    2026年9月28日 • 用户投稿
    100
  • 将Java或Groovy中的字符串转换为JSON对象

    将Java或Groovy中的字符串转换为JSON对象将Java或Groovy中的字符串转换为JSON对象将Java或Groovy中的字符串转换为JSON对象将Java或Groovy中的字符串转换为JSON对象

    将Java或Groovy中的字符串转换为JSON对象,需要根据实际情况进行分析。如果字符串是标准的JSON格式,可以直接使用JSON解析库进行转换。但如果字符串不是标准的JSON格式,则需要自定义解析器。 理解JSON格式 首先,我们需要明确标准的JSON格式。一个JSON对象是由键值对组成的,键和…

    2026年9月28日 • 用户投稿
    000
  • 多模态AI可以生成视频吗 视频创作能力实测

    多模态AI可以生成视频吗 视频创作能力实测多模态AI可以生成视频吗 视频创作能力实测多模态AI可以生成视频吗 视频创作能力实测多模态AI可以生成视频吗 视频创作能力实测

    多模态ai确实能生成视频,但目前主要限于几秒到十几秒的短片段。其常见方式包括:1. 文本驱动生成,如输入描述生成森林日出画面;2. 图像扩展成视频,让静态图动态化;3. 图文混合引导生成更精准视频序列。当前生成视频存在长度有限、帧间不连贯、画质不稳定等问题,但适合社交媒体、创意样片等场景。建议创作者…

    2026年9月28日 • 用户投稿
    000

发表回复

登录后才能评论
关注微信