当前位置:首页 > 技术分析 > 正文内容

eggjs如何实现批量替换word合同模板中的变量?

ruisui883周前 (04-11)技术分析15

场景:我司供应商有上万家,每家都需要签定合同,公司一般会保存word合同模板,有新供应商需要签约时,希望可以自动替换word合同的乙方名称并生成一份合同PDF,请问通过eggjs该如何实现?

在 Egg.js 中实现 Word 合同模板的自动替换并生成PDF,可以通过以下方案高效完成:


技术方案

使用 docxtemplater 库(专为模板替换设计,支持复杂格式保留) + 文件流处理,具体步骤如下:


实现步骤

1. 创建 Word 模板

在 Word 模板中定义占位符(如 {{supplierName}}):

合同甲方:XX公司
合同乙方:{{supplierName}}
合同条款:...

2. 安装依赖

npm install docxtemplater pizzip  # 核心依赖

3. 编写核心替换逻辑

// app/service/docService.js
const Service = require('egg').Service;
const Docxtemplater = require('docxtemplater');
const PizZip = require('pizzip');
const fs = require('fs').promises;
const path = require('path');

class DocService extends Service {
  async generateContract(supplierName, templateName = 'default.docx') {
    // 读取模板文件
    const templatePath = path.join(this.config.baseDir, 'app/templates', templateName);
    const fileBuffer = await fs.readFile(templatePath);

    // 初始化文档处理器
    const zip = new PizZip(fileBuffer);
    const doc = new Docxtemplater(zip, {
      paragraphLoop: true,
      linebreaks: true,
    });

    // 注入数据并渲染
    doc.setData({ supplierName });
    try {
      doc.render();
    } catch (error) {
      this.ctx.logger.error('模板渲染失败:', error);
      throw new Error('合同生成失败,请检查模板格式');
    }

    // 生成最终文件
    return doc.getZip().generate({ type: 'nodebuffer' });
  }
}

module.exports = DocService;

4. 控制器处理请求

// app/controller/contract.js
const Controller = require('egg').Controller;

class ContractController extends Controller {
  async generate() {
    const { ctx } = this;
    const { supplierName, templateName } = ctx.request.body;

    // 校验参数
    if (!supplierName) {
      ctx.status = 400;
      return ctx.body = { error: '供应商名称不能为空' };
    }

    try {
      // 调用服务生成文档
      const buffer = await ctx.service.docService.generateContract(supplierName, templateName);

      // 返回文件流
      ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
      ctx.set('Content-Disposition', `attachment; filename="${encodeURIComponent(supplierName)}_合同.docx"`);
      ctx.body = buffer;
    } catch (error) {
      ctx.status = 500;
      ctx.body = { error: error.message || '合同生成失败' };
    }
  }
}

module.exports = ContractController;

5. 路由配置

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.post('/api/generate-contract', controller.contract.generate);
};

6. 配置文件存储

  • 将模板文件存放在 app/templates 目录
  • 配置安全限制(防止非法模板访问):
// config/config.default.js
config.static = {
  dir: [
    { prefix: '/templates', dir: path.join(appInfo.baseDir, 'app/templates') },
  ],
  // 禁止直接访问模板文件
  dynamic: false,
};

高级优化方案

1. 模板管理功能

// 添加模板上传接口
router.post('/api/upload-template', controller.contract.uploadTemplate);

// 控制器代码
async uploadTemplate() {
  const { ctx } = this;
  const file = ctx.request.files[0];
  const { name } = ctx.request.body;

  // 移动文件到模板目录
  await fs.move(file.filepath, path.join(this.config.baseDir, 'app/templates', name));
  ctx.body = { success: true };
}

2. 结合 PDF 转换

复用之前的 LibreOffice 方案:

async generatePdf() {
  // 生成 Word 文件
  const wordBuffer = await this.generateContract(...);

  // 保存临时文件
  const tempPath = '/tmp/contract.docx';
  await fs.writeFile(tempPath, wordBuffer);

  // 调用 LibreOffice 转换
  const cmd = `libreoffice --headless --convert-to pdf --outdir /tmp ${tempPath}`;
  await execPromise(cmd);

  // 读取并返回 PDF
  return fs.readFile('/tmp/contract.pdf');
}

3. 异步队列处理

使用 egg-bull 插件处理高并发:

// 创建任务队列
app.bull.define('generate_contract', async job => {
  const { supplierName } = job.data;
  return ctx.service.docService.generateContract(supplierName);
});

// 控制器中提交任务
async generateAsync() {
  const job = await this.app.bull.add('generate_contract', {
    supplierName: ctx.request.body.supplierName,
  });
  ctx.body = { jobId: job.id };
}

安全注意事项

  1. 输入校验

if (!/^[\u4e00-\u9fa5a-zA-Z0-9]{2,20}$/.test(supplierName)) { throw new Error('供应商名称包含非法字符'); }

  1. 模板沙箱:限制模板目录访问权限
  2. 防注入攻击:禁止使用 {{ }} 外的动态语法

部署建议

  • 使用 Docker 部署保证 LibreOffice 环境一致性
  • 模板文件存储到云存储(如 OSS)实现分布式访问
  • 添加 API 限流(egg-ratelimiter)

通过以上方案,可实现每小时处理数千份合同的生成需求,且能保证文档格式的严格一致性。

扫描二维码推送至手机访问。

版权声明:本文由ruisui88发布,如需转载请注明出处。

本文链接:http://www.ruisui88.com/post/3442.html

标签: js文档
分享给朋友:

“eggjs如何实现批量替换word合同模板中的变量?” 的相关文章

三维家-系统快捷键使用

快键件使用:通过简单的键盘+鼠标操作,快速完成搭配。1.基础快捷键1) Ctrl+V:复制选中对象第一步:鼠标左击物体,按下Ctrl+V 即可复制选中对象。2) Ctrl+G:组合多选对象第一步:按住Ctrl键多选对象--按住Ctrl+G--确定。3) Ctrl+B:解组选中对象第一步:左击选中对象...

有效地简化导航-Part 1:信息架构

「四步走」——理想的导航系统要做一个可用的导航系统,网页设计师必须按顺序回答以下4个问题:1. 如何组织内容?2. 如何解释导航的选项?3. 哪种导航菜单最适合容纳这些选项?4. 如何设计导航菜单?前两个问题关注构建和便签内容,通常称为信息架构。信息架构师通常用网站地图(site map diagr...

数组、去重、排序、合并、过滤、删除

ES6数字去重 Array.from(new Set([1,2,3,3,4,4])) //[1,2,3,4] [...new Set([1,2,3,3,4,4])] //[1,2,3,4]2、ES6数字排序 [1,2,3,4].sort(); // [1, 2,3,4],默认是升序...

vue2中路由的使用步骤,你学会了吗?

今天我们来整理下关于vue2中路由的使用步骤:1. 导入 vue 文件和Vue-router文件(注意:vue-router是依赖vue运行的,所以一定在vue后引入vue-router)2. 定义路由组件模板3. 创建路由实例并定义路由规则4. 将路由实例挂载给Vue实例5. 在结构区域定义控制路...

慕课 SpringBoot2.X+Vue+UniAPP,全栈开发医疗小程序

本课程以业务驱动技术栈,打造业务相对完整的掌上医疗小程序,解决大家没有好的毕设项目或者求职项目的困境。本课程案例采用前后端分离架构,业务功能完善(既有WEB管理端,也有移动用户端),界面美观,无需艰涩的技术也能做出亮眼的作品。SpringBoot2.X+Vue+UniAPP,全栈开发医疗小程序 |...

在vue项目中封装WebSockets请求

在Vue项目中封装WebSocket请求包括以下步骤:1. 安装WebSocket库:首先,导入WebSocket库,例如`vue-native-websocket`或`socket.io-client`。根据项目需求选择适当的库,并根据官方文档进行安装和配置。2. 创建WebSocket服务:在V...