eggjs如何实现批量替换word合同模板中的变量?
场景:我司供应商有上万家,每家都需要签定合同,公司一般会保存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 };
}
安全注意事项
- 输入校验:
if (!/^[\u4e00-\u9fa5a-zA-Z0-9]{2,20}$/.test(supplierName)) { throw new Error('供应商名称包含非法字符'); }
- 模板沙箱:限制模板目录访问权限
- 防注入攻击:禁止使用 {{ }} 外的动态语法
部署建议
- 使用 Docker 部署保证 LibreOffice 环境一致性
- 模板文件存储到云存储(如 OSS)实现分布式访问
- 添加 API 限流(egg-ratelimiter)
通过以上方案,可实现每小时处理数千份合同的生成需求,且能保证文档格式的严格一致性。