feat: implement bullmq for print queue and bull-board integration
This commit is contained in:
parent
0da156ce08
commit
6e45369855
File diff suppressed because it is too large
Load Diff
|
|
@ -23,12 +23,18 @@
|
|||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.16.4",
|
||||
"@bull-board/express": "^6.16.4",
|
||||
"@bull-board/nestjs": "^6.16.4",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/common": "^11.0.17",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.2.5",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"bullmq": "^5.66.6",
|
||||
"multer": "^2.0.2",
|
||||
"node-printer": "^1.0.4",
|
||||
"node-thermal-printer": "^4.5.0",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { typeOrmConfig } from './config/typeorm.config';
|
||||
import { ListPrinterController } from './controller/list-printer.controller';
|
||||
import { ListPrinterService } from './services/create-printer';
|
||||
import { PrinterProcessor } from './services/printer.processor';
|
||||
import { BullQueuesModule } from './config/BullModule';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forRoot(typeOrmConfig)],
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRoot(typeOrmConfig),
|
||||
BullQueuesModule
|
||||
],
|
||||
controllers: [AppController, ListPrinterController],
|
||||
providers: [AppService, ListPrinterService],
|
||||
providers: [AppService, ListPrinterService, PrinterProcessor],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
connection: {
|
||||
host: configService.get<string>('REDIS_HOST'),
|
||||
port: configService.get<number>('REDIS_PORT') || 6379,
|
||||
password: configService.get<string>('REDIS_PASSWORD'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
||||
BullBoardModule.forRoot({
|
||||
route: '/queues',
|
||||
adapter: ExpressAdapter,
|
||||
}),
|
||||
|
||||
BullModule.registerQueue({
|
||||
name: 'printer',
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
removeOnComplete: false,
|
||||
removeOnFail: { count: 500 },
|
||||
},
|
||||
}),
|
||||
|
||||
BullBoardModule.forFeature({
|
||||
name: 'printer',
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
],
|
||||
exports: [BullModule],
|
||||
})
|
||||
export class BullQueuesModule {}
|
||||
|
|
@ -18,4 +18,11 @@ export class PrintHtmlDto {
|
|||
|
||||
@ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' })
|
||||
height?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'impresso-12345',
|
||||
required: false,
|
||||
description: 'ID único do job para idempotência (evita duplicados)'
|
||||
})
|
||||
jobId?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
import { PrintDataDto } from '../dto/print-data.dto';
|
||||
import { PrintHtmlDto } from '../dto/print-html.dto';
|
||||
const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
|
||||
import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer';
|
||||
|
||||
@Injectable()
|
||||
export class ListPrinterService {
|
||||
private readonly printerType = PrinterTypes.EPSON;
|
||||
|
||||
constructor(@InjectQueue('printer') private printerQueue: Queue) {}
|
||||
|
||||
async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
|
||||
if (!data.lines?.length) {
|
||||
return { success: false, message: 'No lines provided' };
|
||||
|
|
@ -62,77 +66,20 @@ export class ListPrinterService {
|
|||
return printer.alignLeft();
|
||||
}
|
||||
|
||||
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string }> {
|
||||
const puppeteer = require('puppeteer');
|
||||
const pdfPrinter = require('pdf-to-printer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string; jobId?: string }> {
|
||||
const jobOptions: any = {};
|
||||
|
||||
const tempFilePath = path.join(os.tmpdir(), `print-${Date.now()}.pdf`);
|
||||
if (data.jobId) {
|
||||
const safeJobId = /^\d+$/.test(data.jobId) ? `print-${data.jobId}` : data.jobId;
|
||||
jobOptions.jobId = safeJobId;
|
||||
}
|
||||
|
||||
try {
|
||||
const browser = await puppeteer.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
const job = await this.printerQueue.add('print-html-job', data, jobOptions);
|
||||
|
||||
const width = data.width || '80mm';
|
||||
const height = data.height;
|
||||
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body { zoom: 1.4; transform-origin: top left; }
|
||||
@page { size: ${width} ${height || 'auto'}; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${data.html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfOptions: any = {
|
||||
path: tempFilePath,
|
||||
printBackground: true,
|
||||
width: width,
|
||||
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
|
||||
return {
|
||||
success: true,
|
||||
message: 'Tarefa enviada para a fila de impressão',
|
||||
jobId: job.id
|
||||
};
|
||||
|
||||
if (height) {
|
||||
pdfOptions.height = height;
|
||||
}
|
||||
|
||||
await page.pdf(pdfOptions);
|
||||
|
||||
await browser.close();
|
||||
|
||||
await pdfPrinter.print(tempFilePath, {
|
||||
printer: data.printerName,
|
||||
win32: ['-nointterrupt'] // Tenta evitar comandos extras, mas o pdf-to-printer tem opções limitadas de corte.
|
||||
// Na verdade, o pdf-to-printer usa SumatraPDF, que não tem flag explícita de "no-cut".
|
||||
// O corte geralmente é configuração do driver ou do tamanho do papel.
|
||||
// Mas vamos garantir que não estamos enviando nada extra.
|
||||
});
|
||||
|
||||
return { success: true, message: 'HTML convertido (com Tailwind) e enviado para impressão!' };
|
||||
} catch (error) {
|
||||
console.error('Erro na impressão HTML:', error);
|
||||
return { success: false, message: `Erro ao imprimir HTML: ${error.message}` };
|
||||
} finally {
|
||||
try {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Não foi possível deletar arquivo temp:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Job } from 'bullmq';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as pdfPrinter from 'pdf-to-printer';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
@Processor('printer')
|
||||
export class PrinterProcessor extends WorkerHost {
|
||||
|
||||
async process(job: Job<any>): Promise<any> {
|
||||
const data = job.data;
|
||||
const tempFilePath = path.join(os.tmpdir(), `print-${job.id}-${Date.now()}.pdf`);
|
||||
let browser;
|
||||
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const width = data.width || '80mm';
|
||||
const height = data.height;
|
||||
|
||||
const fullHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body { zoom: 1.4; transform-origin: top left; }
|
||||
@page { size: ${width} ${height || 'auto'}; margin: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${data.html}</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await page.setContent(fullHtml, { waitUntil: 'networkidle0' });
|
||||
|
||||
const pdfOptions: any = {
|
||||
path: tempFilePath,
|
||||
printBackground: true,
|
||||
width: width,
|
||||
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' }
|
||||
};
|
||||
|
||||
if (height) pdfOptions.height = height;
|
||||
|
||||
await page.pdf(pdfOptions);
|
||||
await browser.close();
|
||||
|
||||
await pdfPrinter.print(tempFilePath, {
|
||||
printer: data.printerName,
|
||||
silent: true
|
||||
});
|
||||
|
||||
return { status: 'success', jobId: job.id };
|
||||
|
||||
} catch (error) {
|
||||
if (browser) await browser.close();
|
||||
console.error(`Erro no Job ${job.id}:`, error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue