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"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/common": "^11.0.17",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/swagger": "^11.2.5",
|
"@nestjs/swagger": "^11.2.5",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
|
"bullmq": "^5.66.6",
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"node-printer": "^1.0.4",
|
"node-printer": "^1.0.4",
|
||||||
"node-thermal-printer": "^4.5.0",
|
"node-thermal-printer": "^4.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,21 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { typeOrmConfig } from './config/typeorm.config';
|
import { typeOrmConfig } from './config/typeorm.config';
|
||||||
import { ListPrinterController } from './controller/list-printer.controller';
|
import { ListPrinterController } from './controller/list-printer.controller';
|
||||||
import { ListPrinterService } from './services/create-printer';
|
import { ListPrinterService } from './services/create-printer';
|
||||||
|
import { PrinterProcessor } from './services/printer.processor';
|
||||||
|
import { BullQueuesModule } from './config/BullModule';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forRoot(typeOrmConfig)],
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
TypeOrmModule.forRoot(typeOrmConfig),
|
||||||
|
BullQueuesModule
|
||||||
|
],
|
||||||
controllers: [AppController, ListPrinterController],
|
controllers: [AppController, ListPrinterController],
|
||||||
providers: [AppService, ListPrinterService],
|
providers: [AppService, ListPrinterService, PrinterProcessor],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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)' })
|
@ApiProperty({ example: '40mm', required: false, description: 'Altura da etiqueta (default: auto)' })
|
||||||
height?: string;
|
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 { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
|
import { Queue } from 'bullmq';
|
||||||
import { PrintDataDto } from '../dto/print-data.dto';
|
import { PrintDataDto } from '../dto/print-data.dto';
|
||||||
import { PrintHtmlDto } from '../dto/print-html.dto';
|
import { PrintHtmlDto } from '../dto/print-html.dto';
|
||||||
const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer');
|
import { ThermalPrinter, PrinterTypes } from 'node-thermal-printer';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListPrinterService {
|
export class ListPrinterService {
|
||||||
private readonly printerType = PrinterTypes.EPSON;
|
private readonly printerType = PrinterTypes.EPSON;
|
||||||
|
|
||||||
|
constructor(@InjectQueue('printer') private printerQueue: Queue) {}
|
||||||
|
|
||||||
async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
|
async printReceipt(data: PrintDataDto): Promise<{ success: boolean; message: string }> {
|
||||||
if (!data.lines?.length) {
|
if (!data.lines?.length) {
|
||||||
return { success: false, message: 'No lines provided' };
|
return { success: false, message: 'No lines provided' };
|
||||||
|
|
@ -62,77 +66,20 @@ export class ListPrinterService {
|
||||||
return printer.alignLeft();
|
return printer.alignLeft();
|
||||||
}
|
}
|
||||||
|
|
||||||
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string }> {
|
async printHtml(data: PrintHtmlDto): Promise<{ success: boolean; message: string; jobId?: string }> {
|
||||||
const puppeteer = require('puppeteer');
|
const jobOptions: any = {};
|
||||||
const pdfPrinter = require('pdf-to-printer');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
const tempFilePath = path.join(os.tmpdir(), `print-${Date.now()}.pdf`);
|
if (data.jobId) {
|
||||||
|
const safeJobId = /^\d+$/.test(data.jobId) ? `print-${data.jobId}` : data.jobId;
|
||||||
try {
|
jobOptions.jobId = safeJobId;
|
||||||
const browser = await puppeteer.launch({ headless: true });
|
|
||||||
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,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const job = await this.printerQueue.add('print-html-job', data, jobOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Tarefa enviada para a fila de impressão',
|
||||||
|
jobId: job.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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