优秀的编程知识分享平台

网站首页 > 技术文章 正文

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

nanyue 2025-03-19 15:01:19 技术文章 3 ℃

场景:我司供应商有上万家,每家都需要签定合同,公司一般会保存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)

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

最近发表
标签列表