From 8d52e5202af47334d20ef71043899fe3baf508ab Mon Sep 17 00:00:00 2001 From: Luis Eduardo Estevao Date: Tue, 5 May 2026 14:08:34 -0300 Subject: [PATCH] feat: implement Redis caching with locking and optimized product search in SalesService and update compression dependency --- package-lock.json | 45 ++++--- package.json | 2 +- src/sales/sales/sales.service.ts | 193 ++++++++++--------------------- 3 files changed, 89 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2c7a79..fe99af3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@types/cache-manager": "^3.4.3", "cache-manager": "^3.6.3", "cache-manager-redis-store": "^3.0.1", - "compression": "^1.7.4", + "compression": "^1.8.1", "crc": "^4.3.2", "fs": "0.0.1-security", "guid-typescript": "^1.0.9", @@ -4181,16 +4181,17 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -4198,17 +4199,22 @@ } }, "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -9810,9 +9816,10 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", "engines": { "node": ">= 0.8" } diff --git a/package.json b/package.json index b3b12d9..aacb306 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@types/cache-manager": "^3.4.3", "cache-manager": "^3.6.3", "cache-manager-redis-store": "^3.0.1", - "compression": "^1.7.4", + "compression": "^1.8.1", "crc": "^4.3.2", "fs": "0.0.1-security", "guid-typescript": "^1.0.9", diff --git a/src/sales/sales/sales.service.ts b/src/sales/sales/sales.service.ts index ac6183b..5ca8954 100644 --- a/src/sales/sales/sales.service.ts +++ b/src/sales/sales/sales.service.ts @@ -27,8 +27,39 @@ export class SalesService { async GetProducts2(store: string, pageSize: number, pageNumber: number, filter: FilterProduct = null,) { - const connectionDb = new Connection(connectionOptions); - await connectionDb.connect(); + const cacheKey = `GetProducts2:${store}:${pageSize}:${pageNumber}:${JSON.stringify(filter)}`; + const lockKey = `lock:${cacheKey}`; + const lockTimeout = 30; + + // Fase 1: Verificar cache Redis + try { + const cachedResult = await this.redisClient.get(cacheKey); + if (cachedResult) { + console.log('GetProducts2 - Retornando resultado do cache'); + return JSON.parse(cachedResult); + } + } catch (err) { + console.error('Erro ao acessar o Redis no GetProducts2:', err?.message || err); + } + + // Lock para evitar thundering herd + const lockValue = Date.now() + lockTimeout * 1000 + 1; + let acquiredLock: string | null = null; + try { + acquiredLock = await this.redisClient.set(lockKey, lockValue, 'NX', 'EX', lockTimeout); + } catch (err) { + console.error('Erro ao adquirir lock no Redis (GetProducts2):', err?.message || err); + acquiredLock = 'OK'; // Em caso de erro no Redis, prosseguir sem lock + } + + if (acquiredLock !== 'OK') { + console.log('Lock não adquirido (GetProducts2), aguardando...'); + await this.sleep(1000); + return this.GetProducts2(store, pageSize, pageNumber, filter); + } + + // Fase 2: Reutilizar conexão via pool + const connectionDb = getConnection(); const queryRunner = connectionDb.createQueryRunner(); await queryRunner.connect(); try { @@ -38,55 +69,11 @@ export class SalesService { pageNumber = 1; const offSet = (pageNumber - 1) * pageSize; - const sqlProduto = `SELECT - esvlistaprodutos.CODPROD as "idProduct", - esvlistaprodutos.SEQ as "seq", - esvlistaprodutos.DESCRICAO as "smallDescription", - esvlistaprodutos.NOMEECOMMERCE as "title", - esvlistaprodutos.CODFAB as "idProvider", - esvlistaprodutos.CODAUXILIAR as "ean", - esvlistaprodutos.TIPOPRODUTO as "productType", - esvlistaprodutos.DADOSTECNICOS as "technicalData", - esvlistaprodutos.INFORMACOESTECNICAS as "description", - esvlistaprodutos.URLIMAGEM as "urlImage", - esvlistaprodutos.NOMEMARCA as "brand", - esvlistaprodutos.NOMEDEPARTAMENTO as "department", - esvlistaprodutos.NOMESECAO as "section", - esvlistaprodutos.NOMECATEGORIA as "category", - esvlistaprodutos.NOMEFORNECEDOR as "supplier", - esvlistaprodutos.CODIGOFILIAL as "store", - esvlistaprodutos.CLASSEVENDA as "saleAbc", - esvlistaprodutos.CLASSEESTOQUE as "stockAbc", - esvlistaprodutos.FORALINHA as "outLine", - esvlistaprodutos.PRECOVENDA as "listPrice", - esvlistaprodutos.PRECOPROMOCIONAL as "salePrice", - esvlistaprodutos.PRECOPROMOCIONAL as "salePromotion", - esvlistaprodutos.PRECOPROMOCIONAL_AVISTA as "salePromotion_cash", - esvlistaprodutos.PERCENTUALDESCONTO as "offPercent", - esvlistaprodutos.QTESTOQUE_DISPONIVEL as "stock", - esvlistaprodutos.QTCAIXAS as "boxStock", - esvlistaprodutos.ESTOQUE_DISP_LOJA as "store_stock", - esvlistaprodutos.ESTOQUE_DISP_CAIXA_LOJA as "store_boxStock", - esvlistaprodutos.MULTIPLO as "mutiple", - esvlistaprodutos.UNIDADE as "unity", - esvlistaprodutos.URLDEPARTAMENTO as "urlDepartment", - esvlistaprodutos.URLSECAO as "urlSection", - esvlistaprodutos.PRODUTO_COM_REDUCAO_PRECO as "downPrice", - esvlistaprodutos.PRODUTO_EM_CAMPANHA as "compaing", - esvlistaprodutos.BASETINTOMETRICO as "base", - esvlistaprodutos.LETRABASETINTOMETRICO as "letter", - esvlistaprodutos.LINHATINTOMETRICO as "line", - esvlistaprodutos.LITRAGEM as "can", - esvlistaprodutos.QUANTIDADE_ESTOQUE_GERAL as "full_stock", - esvlistaprodutos.TEM_PRODUTO_SIMILAR as "similar", - NVL(esvlistaprodutos.TIPO, 'SEM') as "type_campaing" - FROM ESVLISTAPRODUTOS - WHERE 1 = 1 `; - if (filter && filter.text.length > 0) { const description = filter.text.toUpperCase(); - console.log('consultando por codigo de fabrica'); + console.log('consultando por texto: ' + description); + // Fase 3: Query unificada - busca por CODFAB OU DESCRICAO em uma única query let products = await queryRunner.manager .getRepository(SalesProduct) .createQueryBuilder('esvlistaprodutos') @@ -97,8 +84,6 @@ export class SalesService { .addSelect("\"esvlistaprodutos\".CODFAB", "idProvider") .addSelect("\"esvlistaprodutos\".CODAUXILIAR", "ean") .addSelect("\"esvlistaprodutos\".TIPOPRODUTO", "productType") - .addSelect("\"esvlistaprodutos\".DADOSTECNICOS", "technicalData") - .addSelect("\"esvlistaprodutos\".INFORMACOESTECNICAS", "description") .addSelect("\"esvlistaprodutos\".URLIMAGEM", "urlImage") .addSelect("\"esvlistaprodutos\".NOMEMARCA", "brand") .addSelect("\"esvlistaprodutos\".NOMEDEPARTAMENTO", "department") @@ -131,13 +116,13 @@ export class SalesService { .addSelect("\"esvlistaprodutos\".QUANTIDADE_ESTOQUE_GERAL", "full_stock") .addSelect("\"esvlistaprodutos\".TEM_PRODUTO_SIMILAR", "similar") .addSelect("\"esvlistaprodutos\".TIPO_CAMPANHA", "type_campaing") - .where("UPPER(\"esvlistaprodutos\".CODFAB) LIKE '%'||REPLACE(:description, '@', '%')||'%'", { description }) + .where("(UPPER(\"esvlistaprodutos\".CODFAB) LIKE '%'||REPLACE(:description, '@', '%')||'%' OR UPPER(\"esvlistaprodutos\".descricao) LIKE '%'||REPLACE(:description, '@', '%')||'%')", { description }) .andWhere("(\"esvlistaprodutos\".codfilial = :codfilial OR :codfilial = '99')", { codfilial: store }) .andWhere("(\"esvlistaprodutos\".produto_com_reducao_preco = :produtoComReducaoPreco OR :produtoComReducaoPreco = 'N')", { produtoComReducaoPreco: (filter.markdown) ? 'S' : 'N' }) .andWhere("(\"esvlistaprodutos\".produto_em_campanha = :campaign OR :campaign = 'N')", { campaign: (filter.campaign) ? 'I' : 'N' }) - .andWhere("(\"esvlistaprodutos\".produto_em_campanha = :oportunity OR :oportunity = 'N')", + .andWhere("(\"esvlistaprodutos\".produto_oportunidade = :oportunity OR :oportunity = 'N')", { oportunity: (filter.oportunity) ? 'O' : 'N' }) .andWhere("(\"esvlistaprodutos\".produto_em_promocao = :produtoEmPromocao OR :produtoEmPromocao = 'N')", { produtoEmPromocao: (filter.promotion) ? 'S' : 'N' }) @@ -145,69 +130,9 @@ export class SalesService { .offset(offSet) .orderBy("\"esvlistaprodutos\".DESCRICAO", "ASC") .getRawMany(); - if (products.length == 0) { - products = await queryRunner.manager - .getRepository(SalesProduct) - .createQueryBuilder('esvlistaprodutos') - .select("\"esvlistaprodutos\".CODPROD", "idProduct") - .addSelect("\"esvlistaprodutos\".SEQ", "seq") - .addSelect("\"esvlistaprodutos\".DESCRICAO", "smallDescription") - .addSelect("\"esvlistaprodutos\".NOMEECOMMERCE", "title") - .addSelect("\"esvlistaprodutos\".CODFAB", "idProvider") - .addSelect("\"esvlistaprodutos\".CODAUXILIAR", "ean") - .addSelect("\"esvlistaprodutos\".TIPOPRODUTO", "productType") - .addSelect("\"esvlistaprodutos\".DADOSTECNICOS", "technicalData") - .addSelect("\"esvlistaprodutos\".INFORMACOESTECNICAS", "description") - .addSelect("\"esvlistaprodutos\".URLIMAGEM", "urlImage") - .addSelect("\"esvlistaprodutos\".NOMEMARCA", "brand") - .addSelect("\"esvlistaprodutos\".NOMEDEPARTAMENTO", "department") - .addSelect("\"esvlistaprodutos\".NOMESECAO", "section") - .addSelect("\"esvlistaprodutos\".NOMECATEGORIA", "category") - .addSelect("\"esvlistaprodutos\".NOMEFORNECEDOR", "supplier") - .addSelect("\"esvlistaprodutos\".CODIGOFILIAL", "store") - .addSelect("\"esvlistaprodutos\".CLASSEVENDA", "saleAbc") - .addSelect("\"esvlistaprodutos\".CLASSEESTOQUE", "stockAbc") - .addSelect("\"esvlistaprodutos\".FORALINHA", "outLine") - .addSelect("\"esvlistaprodutos\".PRECOVENDA", "listPrice") - .addSelect("\"esvlistaprodutos\".PRECOPROMOCIONAL", "salePrice") - .addSelect("\"esvlistaprodutos\".PRECOPROMOCIONAL", "salePromotion") - .addSelect("\"esvlistaprodutos\".PRECOPROMOCIONAL_AVISTA", "salePromotion_cash") - .addSelect("\"esvlistaprodutos\".PERCENTUALDESCONTO", "offPercent") - .addSelect("\"esvlistaprodutos\".QTESTOQUE_DISPONIVEL", "stock") - .addSelect("\"esvlistaprodutos\".QTCAIXAS", "boxStock") - .addSelect("\"esvlistaprodutos\".ESTOQUE_DISP_LOJA", "store_stock") - .addSelect("\"esvlistaprodutos\".ESTOQUE_DISP_CAIXA_LOJA", "store_boxStock") - .addSelect("\"esvlistaprodutos\".MULTIPLO", "mutiple") - .addSelect("\"esvlistaprodutos\".UNIDADE", "unity") - .addSelect("\"esvlistaprodutos\".URLDEPARTAMENTO", "urlDepartment") - .addSelect("\"esvlistaprodutos\".URLSECAO", "urlSection") - .addSelect("\"esvlistaprodutos\".PRODUTO_COM_REDUCAO_PRECO", "downPrice") - .addSelect("\"esvlistaprodutos\".PRODUTO_EM_CAMPANHA", "compaing") - .addSelect("\"esvlistaprodutos\".BASETINTOMETRICO", "base") - .addSelect("\"esvlistaprodutos\".LETRABASETINTOMETRICO", "letter") - .addSelect("\"esvlistaprodutos\".LINHATINTOMETRICO", "line") - .addSelect("\"esvlistaprodutos\".LITRAGEM", "can") - .addSelect("\"esvlistaprodutos\".QUANTIDADE_ESTOQUE_GERAL", "full_stock") - .addSelect("\"esvlistaprodutos\".TEM_PRODUTO_SIMILAR", "similar") - .addSelect("\"esvlistaprodutos\".TIPO_CAMPANHA", "type_campaing") - .where("UPPER(\"esvlistaprodutos\".descricao) LIKE '%'||REPLACE(:description, '@', '%')||'%'", { description }) - .andWhere("(\"esvlistaprodutos\".codfilial = :codfilial OR :codfilial = '99')", { codfilial: store }) - .andWhere("(\"esvlistaprodutos\".produto_com_reducao_preco = :produtoComReducaoPreco OR :produtoComReducaoPreco = 'N')", - { produtoComReducaoPreco: (filter.markdown) ? 'S' : 'N' }) - .andWhere("(\"esvlistaprodutos\".produto_em_campanha = :campaign OR :campaign = 'N')", - { campaign: (filter.campaign) ? 'I' : 'N' }) - .andWhere("(\"esvlistaprodutos\".produto_oportunidade = :oportunity OR :oportunity = 'N')", - { oportunity: (filter.oportunity) ? 'O' : 'N' }) - .andWhere("(\"esvlistaprodutos\".produto_em_promocao = :produtoEmPromocao OR :produtoEmPromocao = 'N')", - { produtoEmPromocao: (filter.promotion) ? 'S' : 'N' }) - .orderBy("\"esvlistaprodutos\".CLASSEVENDA") - .limit(pageSize) - .offset(offSet) - .orderBy("\"esvlistaprodutos\".DESCRICAO", "ASC") - .getRawMany(); - } products = this.createListImages(products); console.log(products.length + ' produtos'); + try { await this.redisClient.set(cacheKey, JSON.stringify(products), 'EX', 900); } catch (cacheErr) { console.error('Erro ao salvar cache GetProducts2:', cacheErr?.message || cacheErr); } return products; } @@ -234,8 +159,6 @@ export class SalesService { .addSelect("\"esvlistaprodutos\".CODFAB", "idProvider") .addSelect("\"esvlistaprodutos\".CODAUXILIAR", "ean") .addSelect("\"esvlistaprodutos\".TIPOPRODUTO", "productType") - .addSelect("\"esvlistaprodutos\".DADOSTECNICOS", "technicalData") - .addSelect("\"esvlistaprodutos\".INFORMACOESTECNICAS", "description") .addSelect("\"esvlistaprodutos\".URLIMAGEM", "urlImage") .addSelect("\"esvlistaprodutos\".NOMEMARCA", "brand") .addSelect("\"esvlistaprodutos\".NOMEDEPARTAMENTO", "department") @@ -285,6 +208,7 @@ export class SalesService { .getRawMany(); products = this.createListImages(products); console.log(products.length + ' produtos'); + try { await this.redisClient.set(cacheKey, JSON.stringify(products), 'EX', 900); } catch (cacheErr) { console.error('Erro ao salvar cache GetProducts2:', cacheErr?.message || cacheErr); } return products; } else { @@ -300,8 +224,6 @@ export class SalesService { .addSelect("\"esvlistaprodutos\".CODFAB", "idProvider") .addSelect("\"esvlistaprodutos\".CODAUXILIAR", "ean") .addSelect("\"esvlistaprodutos\".TIPOPRODUTO", "productType") - .addSelect("\"esvlistaprodutos\".DADOSTECNICOS", "technicalData") - .addSelect("\"esvlistaprodutos\".INFORMACOESTECNICAS", "description") .addSelect("\"esvlistaprodutos\".URLIMAGEM", "urlImage") .addSelect("\"esvlistaprodutos\".NOMEMARCA", "brand") .addSelect("\"esvlistaprodutos\".NOMEDEPARTAMENTO", "department") @@ -349,6 +271,7 @@ export class SalesService { .getRawMany(); products = this.createListImages(products); console.log(products.length + ' produtos'); + try { await this.redisClient.set(cacheKey, JSON.stringify(products), 'EX', 900); } catch (cacheErr) { console.error('Erro ao salvar cache GetProducts2:', cacheErr?.message || cacheErr); } return products; } @@ -357,7 +280,15 @@ export class SalesService { throw error; } finally { await queryRunner.release(); - await connectionDb.close(); + // Não fecha a conexão - reutilizada pelo pool + try { + const currentLockValue = await this.redisClient.get(lockKey); + if (currentLockValue === lockValue.toString()) { + await this.redisClient.del(lockKey); + } + } catch (lockErr) { + console.error('Erro ao liberar lock Redis (GetProducts2):', lockErr?.message || lockErr); + } } } @@ -1114,17 +1045,18 @@ export class SalesService { } createListImages(products: SalesProduct[]): SalesProduct[] { - products.forEach(product => { + for (let i = 0; i < products.length; i++) { + const product = products[i]; product.images = []; - if (product.urlImage != null) { - let urlImage = product.urlImage; - if (urlImage.indexOf(";") == -1) { - urlImage += ";"; - } - const images = product.urlImage.split(";"); - product.images = images.map(i => i.trim()); + if (product.urlImage != null && product.urlImage.length > 0) { + product.images = product.urlImage + .split(';') + .map(img => img.trim()) + .filter(img => img.length > 0); } - }); + // Adiciona thumbnail para acesso rápido no frontend + (product as any).thumbnail = product.images.length > 0 ? product.images[0] : null; + } return products; } @@ -1415,8 +1347,7 @@ export class SalesService { const skipReg = ((pageNumber - 1) * pageSize); const pagination = ` OFFSET ${skipReg} ROWS FETCH NEXT ${pageSize} ROWS ONLY`; - const connectionDb = new Connection(connectionOptions); - await connectionDb.connect(); + const connectionDb = getConnection(); const queryRunner = connectionDb.createQueryRunner(); await queryRunner.connect(); @@ -1425,7 +1356,7 @@ export class SalesService { products = this.createListImages(products); console.log("Total de produtos: " + products.length); - await this.redisClient.set(cacheKey, JSON.stringify(products), 'EX', 3600); + await this.redisClient.set(cacheKey, JSON.stringify(products), 'EX', 900); return products; } catch (err) { @@ -1433,7 +1364,7 @@ export class SalesService { throw err; } finally { await queryRunner.release(); - await connectionDb.close(); + // Não fecha a conexão - reutilizada pelo pool } }