|
| Home → WINDEV 25 → Exemplo de como Gerar milhões de Boletos de cobrança e enviar por e-mail usando thread paralela |
| Exemplo de como Gerar milhões de Boletos de cobrança e enviar por e-mail usando thread paralela |
| Started by Boller, May, 21 2025 4:41 PM - 1 reply |
| |
| | | |
|
| |
Registered member 4,613 messages |
|
| Posted on May, 21 2025 - 4:41 PM |
Bom dia
Vamos exemplificar como usar Thread Paralela num exemplo prático de geração de PDF e e-mails
Exemplo Diagrama das tabelas usadas

Exemplo script SQL
-- Tabela: tbl_cliente CREATE TABLE tbl_cliente ( cpf VARCHAR(14) PRIMARY KEY, email VARCHAR(255), nome VARCHAR(255) );
-- Tabela: tbl_saldo CREATE TABLE tbl_saldo ( cpf_cliente VARCHAR(14), saldo NUMERIC(12, 2), pdf_gerado BOOLEAN DEFAULT FALSE, nome_pdf VARCHAR(255), email_enviado BOOLEAN DEFAULT FALSE, data_geracao DATE, hora_geracao TIME, data_envio DATE, hora_envio TIME, FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf) );
-- Tabela: tbl_config_smtp CREATE TABLE tbl_config_smtp ( servidor VARCHAR(255), porta INTEGER, usuario VARCHAR(255), senha VARCHAR(255), email_remetente VARCHAR(255), usar_ssl BOOLEAN );
-- Tabela: tbl_log_erros CREATE TABLE tbl_log_erros ( id SERIAL PRIMARY KEY, data DATE, hora TIME, tipo_erro VARCHAR(50), severidade VARCHAR(20), mensagem TEXT, cpf_cliente VARCHAR(14), id_processamento INTEGER, usuario VARCHAR(50), maquina VARCHAR(50), detalhes_tecnicos TEXT, FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf), FOREIGN KEY (id_processamento) REFERENCES tbl_controle_processamento(id) );
-- Tabela: tbl_controle_processamento CREATE TABLE tbl_controle_processamento ( id SERIAL PRIMARY KEY, data_exec DATE, hora_exec TIME, total_clientes INTEGER, pdfs_gerados INTEGER, emails_enviados INTEGER, falhas_pdf INTEGER, falhas_email INTEGER, status VARCHAR(20), motivo_erro TEXT, uso_cpu_medio NUMERIC(5,2), uso_memoria_medio NUMERIC(5,2), espaco_disco_inicial INTEGER, espaco_disco_final INTEGER, tempo_execucao INTEGER, usuario VARCHAR(50), maquina VARCHAR(50) );
-- Tabela: tbl_rastreabilidade_boleto CREATE TABLE tbl_rastreabilidade_boleto ( id SERIAL PRIMARY KEY, cpf_cliente VARCHAR(14), evento VARCHAR(100), data DATE, hora TIME, id_processamento INTEGER, usuario VARCHAR(50), maquina VARCHAR(50), FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf), FOREIGN KEY (id_processamento) REFERENCES tbl_controle_processamento(id) );
-- Tabela: tbl_reprocessamento CREATE TABLE tbl_reprocessamento ( id SERIAL PRIMARY KEY, cpf_cliente VARCHAR(14), tipo_falha VARCHAR(10), data_falha DATE, hora_falha TIME, id_processamento_original INTEGER, motivo_falha TEXT, status VARCHAR(20), data_reprocessamento DATE, hora_reprocessamento TIME, id_processamento_reprocessamento INTEGER, tentativas INTEGER, FOREIGN KEY (cpf_cliente) REFERENCES tbl_cliente(cpf), FOREIGN KEY (id_processamento_original) REFERENCES tbl_controle_processamento(id), FOREIGN KEY (id_processamento_reprocessamento) REFERENCES tbl_controle_processamento(id) );
-- Tabela: tbl_configuracoes CREATE TABLE tbl_configuracoes ( chave VARCHAR(100) PRIMARY KEY, valor VARCHAR(255), descricao TEXT );
//############################## // *CÓDIGO COMPLETO COM MELHORIAS* // *Sistema de Geração de Boletos e Envio de E-mails com Thread Paralela* //##############################
// Variáveis globais gnIdProcessamentoAtual is int = 0 // ID do processamento atual
//############################## // *BOLETO_GeraPDFs_Threaded* //##############################
PROCEDURE BOLETO_GeraPDFs_Threaded()
// Contadores para monitoramento nPDFsGerados is int = 0 nFalhasPDF is int = 0 nThreadsAtivas is int = 0 nMaxThreadsConfig is int = GetConfigValue("MAX_THREADS", 10) // Configura através de parâmetro nMaxThreads is int = nMaxThreadsConfig // Será ajustado dinamicamente nTimeoutThread is int = GetConfigValue("TIMEOUT_THREAD", 120) // Timeout em segundos nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500) // Espaço mínimo em MB nVerificacaoRecursos is int = GetConfigValue("INTERVALO_VERIFICACAO_RECURSOS", 10) // A cada quantos clientes verificar recursos
// Cria diretório por data com horário para evitar conflitos em múltiplas execuções sDataHoje is string = DateToString(Today(), "YYYYMMDD") sHoraExec is string = TimeToString(Now(), "HHmmss") sPastaBoletos is string = "C:\Boletos\boleto_" + sDataHoje + "_" + sHoraExec
// Verifica e cria diretório IF NOT fDirectoryExist(sPastaBoletos) THEN IF NOT fMakeDir(sPastaBoletos) THEN BOLETO_LogRegistro("SISTEMA", "CRITICO", "Falha ao criar diretório: " + sPastaBoletos) RETURN END END
// Verifica espaço em disco inicial IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN // Espaço insuficiente - notifica e cancela Error("Espaço em disco insuficiente para iniciar o processo. Verifique os logs.") RETURN END
// Inicializa tabela de controle de processamento HExecuteSQL(hQueryDefault, "CREATE TABLE IF NOT EXISTS tbl_controle_processamento (" + "id INT AUTO_INCREMENT PRIMARY KEY, " + "data_exec DATE, " + "hora_exec TIME, " + "total_clientes INT, " + "pdfs_gerados INT, " + "emails_enviados INT, " + "falhas_pdf INT, " + "falhas_email INT, " + "status VARCHAR(20), " + "motivo_erro VARCHAR(255), " + "uso_cpu_medio NUMERIC(5,2), " + "uso_memoria_medio NUMERIC(5,2), " + "espaco_disco_inicial INT, " + "espaco_disco_final INT, " + "tempo_execucao INT, " + "usuario VARCHAR(50), " + "maquina VARCHAR(50))")
// Inicializa tabela de reprocessamento se não existir HExecuteSQL(hQueryDefault, "CREATE TABLE IF NOT EXISTS tbl_reprocessamento (" + "id INT AUTO_INCREMENT PRIMARY KEY, " + "cpf_cliente VARCHAR(14), " + "tipo_falha VARCHAR(10), " + "data_falha DATE, " + "hora_falha TIME, " + "id_processamento_original INT, " + "motivo_falha VARCHAR(255), " + "status VARCHAR(20), " + "data_reprocessamento DATE, " + "hora_reprocessamento TIME, " + "id_processamento_reprocessamento INT, " + "tentativas INT)")
// Inicia registro de processamento nIdProcessamento is int = BOLETO_IniciaProcessamento(sDataHoje, sHoraExec) IF nIdProcessamento = 0 THEN BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", "Falha ao iniciar registro de processamento") RETURN END
// Atualiza status BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "GERANDO_PDFS")
// Busca clientes com saldo negativo (consulta otimizada com limite) sQuery is string = "SELECT c.cpf, c.email, c.nome, s.saldo FROM tbl_cliente c " + "INNER JOIN tbl_saldo s ON c.cpf = s.cpf_cliente " + "WHERE s.saldo < 0 AND s.pdf_gerado = False " + "ORDER BY s.saldo ASC" HExecuteSQLQuery(qry_saldo, hQueryDefault, sQuery)
// Conta total de registros para monitoramento nTotalClientes is int = 0 WHILE NOT HOut(qry_saldo) nTotalClientes += 1 HReadNext(qry_saldo) END HReadFirst(qry_saldo)
// Atualiza contagem total de clientes BOLETO_AtualizaProcessamento(nIdProcessamento, "total_clientes", nTotalClientes) BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Total de clientes para processamento: " + nTotalClientes)
// Inicializa mutex para proteção de contadores compartilhados mutexContadores is Mutex = MutexCreate("CONTADORES_BOLETO")
// Controle de timeout global e monitoramento de recursos dhorarioInicio is datetime = Now() nSomaCPU is numeric = 0 nSomaMemoria is numeric = 0 nContadorVerificacoes is int = 0
// Processa cada cliente nContadorClientes is int = 0 WHILE NOT HOut(qry_saldo) nContadorClientes += 1 // Verifica timeout global do processamento nTempoDecorrido is int = DateTimeDifference(Now(), dhorarioInicio) nTimeoutGlobal is int = GetConfigValue("TIMEOUT_GLOBAL", 3600) // 1 hora por padrão IF nTempoDecorrido > nTimeoutGlobal THEN sMensagem is string = "Timeout global do processamento atingido após " + nTimeoutGlobal + " segundos" BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", sMensagem) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CANCELADO") BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem) BREAK END // Verifica recursos periodicamente IF nContadorClientes % nVerificacaoRecursos = 0 THEN // Verifica espaço em disco IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN // Espaço insuficiente - registra e continua com threads reduzidas BOLETO_LogRegistro("ESPACO_DISCO", "AVISO", "Espaço em disco baixo, reduzindo número de threads") nMaxThreads = 1 // Reduz ao mínimo para tentar continuar END // Verifica uso de CPU e memória nUsoCPU is int = BOLETO_VerificaUsoCPU() nUsoMemoria is int = BOLETO_VerificaUsoMemoria() // Acumula para cálculo de média nSomaCPU += nUsoCPU nSomaMemoria += nUsoMemoria nContadorVerificacoes += 1 // Ajusta número de threads com base no uso de recursos nMaxThreads = BOLETO_AjustaNumeroThreads(nMaxThreadsConfig) END // Gerencia limite de threads IF nThreadsAtivas < nMaxThreads THEN // MODIFICAÇÃO: Usa CPF do cliente como nome da thread sThreadName is string = qry_saldo.cpf ExecuteThread(sThreadName, threadNormal, BOLETO_Thread_Gerador, qry_saldo.cpf, sPastaBoletos, mutexContadores, @nPDFsGerados, @nFalhasPDF, nIdProcessamento) nThreadsAtivas += 1 // Log de início de thread BOLETO_LogRegistro("THREAD", "INFO", "Thread iniciada para CPF: " + qry_saldo.cpf) ELSE // Aguarda com timeout IF NOT ThreadWait(5000) THEN // Timeout de 5 segundos para evitar bloqueio infinito BOLETO_LogRegistro("THREAD", "INFO", "Aguardando liberação de threads. Ativas: " + nThreadsAtivas + "/" + nMaxThreads) ELSE nThreadsAtivas -= 1 END END HReadNext(qry_saldo) END
// Aguarda todas as threads de geração terminarem startTimeout is datetime = Now() WHILE nThreadsAtivas > 0 // Timeout de segurança IF DateTimeDifference(Now(), startTimeout) > 300 THEN // 5 minutos de timeout BOLETO_LogRegistro("THREAD", "CRITICO", "Timeout ao aguardar threads. Forçando continuação.") BREAK END IF NOT ThreadWait(10000) THEN // 10 segundos de timeout BOLETO_LogRegistro("THREAD", "INFO", "Aguardando finalização de " + nThreadsAtivas + " threads...") ELSE nThreadsAtivas -= 1 END END
// Calcula médias de uso de recursos nCPUMedio is numeric = 0 nMemoriaMedio is numeric = 0 IF nContadorVerificacoes > 0 THEN nCPUMedio = nSomaCPU / nContadorVerificacoes nMemoriaMedio = nSomaMemoria / nContadorVerificacoes END
// Verifica espaço em disco final nEspacoFinal is int = BOLETO_VerificaEspacoDisco("C:\")
// Calcula tempo de execução nTempoExecucao is int = DateTimeDifference(Now(), dhorarioInicio)
// Registra PDFs gerados e atualiza status BOLETO_AtualizaProcessamento(nIdProcessamento, "pdfs_gerados", nPDFsGerados) BOLETO_AtualizaProcessamento(nIdProcessamento, "falhas_pdf", nFalhasPDF) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ENVIANDO_EMAILS") BOLETO_AtualizaProcessamento(nIdProcessamento, "uso_cpu_medio", nCPUMedio) BOLETO_AtualizaProcessamento(nIdProcessamento, "uso_memoria_medio", nMemoriaMedio) BOLETO_AtualizaProcessamento(nIdProcessamento, "espaco_disco_final", nEspacoFinal) BOLETO_AtualizaProcessamento(nIdProcessamento, "tempo_execucao", nTempoExecucao)
// Dispara thread de envio de e-mails ThreadExecute("THREAD_ENVIA_EMAILS", threadNormal, BOLETO_Thread_EnviaEmails, sPastaBoletos, nIdProcessamento) BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Iniciada thread de envio de e-mails. PDFs gerados: " + nPDFsGerados + ", Falhas: " + nFalhasPDF)
MutexDestroy(mutexContadores)
//############################## // *BOLETO_Thread_Gerador* //##############################
PROCEDURE BOLETO_Thread_Gerador(cpfCliente is string, sDiretorio is string, mutexContadores is Mutex, nPDFsGerados is int by reference, nFalhasPDF is int by reference, nIdProcessamento is int)
// Timeout para geração de cada boleto individualmente nTimeoutGeracaoPDF is int = GetConfigValue("TIMEOUT_GERACAO_PDF", 60) // segundos startTime is datetime = Now()
BOLETO_LogRegistro("PDF", "INFO", "Iniciando geração de PDF", cpfCliente)
TRY HReadSeekFirst(tbl_cliente, cpf, cpfCliente) IF HFound(tbl_cliente) THEN sNomePDF is string = sDiretorio + "\" + cpfCliente + "_" + StringReplace(TimeToString(Now(), "HHMMSS"), ":", "") + ".pdf" // Verifica espaço em disco antes de gerar o PDF nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_PDF", 10) // Mínimo para um PDF IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN // Espaço insuficiente - registra falha BOLETO_LogRegistro("PDF", "ERRO", "Espaço insuficiente para gerar PDF", cpfCliente) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Espaço insuficiente em disco") RETURN END // Geração do PDF com tratamento de erro e timeout IF iInitReportQuery(REP_BOLETO) THEN iParameter("CPF", cpfCliente) iDestination(iPDF) // Verifica timeout durante a geração IF DateTimeDifference(Now(), startTime) > nTimeoutGeracaoPDF THEN BOLETO_LogRegistro("PDF", "ERRO", "Timeout na geração do PDF", cpfCliente) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Timeout na geração") RETURN END IF iPrintReport(REP_BOLETO, sNomePDF) THEN IF fFileExist(sNomePDF) THEN // Atualiza tabela de saldo com lock para prevenir concorrência IF HReadSeekFirst(tbl_saldo, cpf_cliente, cpfCliente, hLockWrite) THEN tbl_saldo.pdf_gerado = True tbl_saldo.nome_pdf = fExtractPath(sNomePDF, fFileName + fExtension) tbl_saldo.data_geracao = Today() tbl_saldo.hora_geracao = Now() IF HModify(tbl_saldo) THEN // Incrementa contador com proteção de mutex MutexLock(mutexContadores) nPDFsGerados += 1 MutexUnlock(mutexContadores) // Log de sucesso BOLETO_LogRegistro("PDF", "INFO", "PDF gerado com sucesso: " + sNomePDF, cpfCliente) // Registra em tabela de rastreabilidade BOLETO_RegistraRastreabilidade(cpfCliente, "PDF_GERADO", nIdProcessamento) ELSE BOLETO_LogRegistro("PDF", "ERRO", "Falha ao atualizar tbl_saldo", cpfCliente, HErrorInfo()) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao atualizar registro") END ELSE BOLETO_LogRegistro("PDF", "ERRO", "Falha ao bloquear registro na tbl_saldo", cpfCliente) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao bloquear registro") END ELSE BOLETO_LogRegistro("PDF", "ERRO", "PDF não encontrado após geração: " + sNomePDF, cpfCliente) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Arquivo não encontrado após geração") END ELSE BOLETO_LogRegistro("PDF", "ERRO", "Falha ao gerar PDF", cpfCliente, iErrorInfo()) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha na impressão do relatório: " + iErrorInfo()) END ELSE BOLETO_LogRegistro("PDF", "ERRO", "Falha ao inicializar relatório", cpfCliente, iErrorInfo()) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Falha ao inicializar relatório: " + iErrorInfo()) END ELSE BOLETO_LogRegistro("PDF", "ERRO", "Cliente não encontrado", cpfCliente) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) END CATCH BOLETO_LogRegistro("PDF", "ERRO", "Exceção ao processar", cpfCliente, ExceptionInfo()) // Incrementa contador de falhas com proteção de mutex MutexLock(mutexContadores) nFalhasPDF += 1 MutexUnlock(mutexContadores) // Registra para reprocessamento BOLETO_RegistraReprocessamento(cpfCliente, "PDF", "Exceção: " + ExceptionInfo()) END
//############################## // *BOLETO_Thread_EnviaEmails* //##############################
PROCEDURE BOLETO_Thread_EnviaEmails(sDiretorio is string, nIdProcessamento is int)
// Contador de e-mails enviados e falhas nEmailsEnviados is int = 0 nFalhasEmail is int = 0 nLimiteEmailsHora is int = GetConfigValue("LIMITE_EMAILS_HORA", 100) nTempoEsperaEntreLotes is int = GetConfigValue("TEMPO_ESPERA_LOTE", 60) // segundos nTamanhoLote is int = GetConfigValue("TAMANHO_LOTE_EMAIL", 20) nContadorLote is int = 0
// Timeout para envio de emails nTimeoutEnvio is int = GetConfigValue("TIMEOUT_ENVIO_EMAILS", 3600) // 1 hora startTime is datetime = Now()
BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ENVIANDO_EMAILS") BOLETO_LogRegistro("EMAIL", "INFO", "Iniciando processo de envio de e-mails")
TRY // Carrega configurações de SMTP com tratamento de criptografia HReadFirst(tbl_config_smtp) IF NOT HFound(tbl_config_smtp) THEN sMensagem is string = "Configurações de SMTP não encontradas" BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO") BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem) RETURN END // Descriptografa senha se necessário sSenhaSMTP is string = BOLETO_DescriptografaSenha(tbl_config_smtp.senha) // Verifica pasta de boletos IF NOT fDirectoryExist(sDiretorio) THEN sMensagem is string = "Diretório de boletos não encontrado: " + sDiretorio BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO") BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagem) RETURN END // Busca clientes com PDF gerado (com otimização de campos) sQuery is string = "SELECT s.cpf_cliente, s.nome_pdf, c.email, c.nome " + "FROM tbl_saldo s " + "INNER JOIN tbl_cliente c ON s.cpf_cliente = c.cpf " + "WHERE s.pdf_gerado = True AND s.saldo < 0 " + "AND s.email_enviado = False" HExecuteSQLQuery(qry_saldo, hQueryDefault, sQuery) // Conta total para monitoramento nTotalEmails is int = 0 WHILE NOT HOut(qry_saldo) nTotalEmails += 1 HReadNext(qry_saldo) END HReadFirst(qry_saldo) BOLETO_LogRegistro("EMAIL", "INFO", "Total de emails para envio: " + nTotalEmails) // Configura servidor SMTP uma única vez EmailReset() Email.ServerAddress = tbl_config_smtp.servidor Email.Port = tbl_config_smtp.porta Email.UserName = tbl_config_smtp.usuario Email.Password = sSenhaSMTP Email.Name = "Cobranca WX" Email.Address = tbl_config_smtp.email_remetente Email.Authentication = True IF tbl_config_smtp.usar_ssl THEN Email.Secure = emailSecureSSL END WHILE NOT HOut(qry_saldo) // Verifica timeout global IF DateTimeDifference(Now(), startTime) > nTimeoutEnvio THEN sMensagem is string = "Timeout no envio de emails atingido após " + nTimeoutEnvio + " segundos" BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagem) BREAK END // Implementa lotes com pausas para evitar blacklist IF nContadorLote >= nTamanhoLote THEN BOLETO_LogRegistro("EMAIL", "INFO", "Pausa entre lotes de emails. Enviados até agora: " + nEmailsEnviados) Sleep(nTempoEsperaEntreLotes * 1000) nContadorLote = 0 END IF qry_saldo.email <> "" THEN // Valida formato do e-mail IF StringRegExMatch(qry_saldo.email, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") THEN sCaminhoPDF is string = sDiretorio + "\" + qry_saldo.nome_pdf IF fFileExist(sCaminhoPDF) THEN // Mantém configuração SMTP já definida, reseta apenas campos específicos Email.Subject = "Boleto em aberto - WX Soluções" Email.Message = "Prezado(a) " + qry_saldo.nome + "," + CR + CR + "Segue em anexo o boleto referente ao saldo em aberto na sua conta." + CR + CR + "Para qualquer dúvida, entre em contato com nosso suporte." + CR + CR + "Atenciosamente," + CR + "Equipe WX Soluções" Email.Recipient[1] = qry_saldo.email Email.Attachments[1] = sCaminhoPDF // Tratamento de erro no envio TRY IF EmailSendMessage() THEN nEmailsEnviados += 1 nContadorLote += 1 // Atualiza status na tabela de saldo com lock IF HReadSeekFirst(tbl_saldo, cpf_cliente, qry_saldo.cpf_cliente, hLockWrite) THEN tbl_saldo.email_enviado = True tbl_saldo.data_envio = Today() tbl_saldo.hora_envio = Now() IF NOT HModify(tbl_saldo) THEN BOLETO_LogRegistro("EMAIL", "ERRO", "Falha ao atualizar status de email enviado", qry_saldo.cpf_cliente, HErrorInfo()) END END // Registra em tabela de rastreabilidade BOLETO_RegistraRastreabilidade(qry_saldo.cpf_cliente, "EMAIL_ENVIADO", nIdProcessamento) BOLETO_LogRegistro("EMAIL", "INFO", "E-mail enviado com sucesso", qry_saldo.cpf_cliente) ELSE nFalhasEmail += 1 sMensagemErro is string = "Erro ao enviar e-mail: " + ErrorInfo() BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente) // Registra para reprocessamento BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro) END CATCH nFalhasEmail += 1 sMensagemErro is string = "Exceção ao enviar email: " + ExceptionInfo() BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente) // Registra para reprocessamento BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro) END ELSE nFalhasEmail += 1 sMensagemErro is string = "PDF não encontrado: " + sCaminhoPDF BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente) // Registra para reprocessamento BOLETO_RegistraReprocessamento(qry_saldo.cpf_cliente, "EMAIL", sMensagemErro) END ELSE nFalhasEmail += 1 sMensagemErro is string = "E-mail inválido: " + qry_saldo.email BOLETO_LogRegistro("EMAIL", "ERRO", sMensagemErro, qry_saldo.cpf_cliente) END END HReadNext(qry_saldo) END // Atualiza contagem final BOLETO_AtualizaProcessamento(nIdProcessamento, "emails_enviados", nEmailsEnviados) BOLETO_AtualizaProcessamento(nIdProcessamento, "falhas_email", nFalhasEmail) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO") // Verifica espaço em disco final nEspacoFinal is int = BOLETO_VerificaEspacoDisco("C:\") BOLETO_AtualizaProcessamento(nIdProcessamento, "espaco_disco_final", nEspacoFinal) // Calcula tempo de execução nTempoExecucao is int = DateTimeDifference(Now(), startTime) BOLETO_AtualizaProcessamento(nIdProcessamento, "tempo_execucao", nTempoExecucao) BOLETO_LogRegistro("EMAIL", "INFO", "Envio de e-mails concluído. E-mails enviados: " + nEmailsEnviados + " de " + nTotalEmails + ". Falhas: " + nFalhasEmail) CATCH sMensagemErro is string = "Exceção no processo de envio de emails: " + ExceptionInfo() BOLETO_LogRegistro("EMAIL", "CRITICO", sMensagemErro) BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "ERRO") BOLETO_AtualizaProcessamento(nIdProcessamento, "motivo_erro", sMensagemErro) END
//############################## // *BOLETO_ExecutarCompleto* //##############################
PROCEDURE BOLETO_ExecutarCompleto()
Trace("Iniciando processo de geração de boletos...") dHoraInicio is datetime = Now()
// Verifica se já existe processo em execução IF BOLETO_VerificaProcessoEmExecucao() THEN Error("Já existe um processo de geração de boletos em execução! Aguarde ou verifique os logs.") RETURN END
// Valida pasta base com tratamento avançado de erros sPastaBase is string = GetConfigValue("PASTA_BASE_BOLETOS", "C:\Boletos") IF NOT fDirectoryExist(sPastaBase) THEN TRY IF NOT fMakeDir(sPastaBase) THEN Error("Não foi possível criar a pasta base de boletos!") RETURN END CATCH Error("Erro ao criar pasta base: " + ExceptionInfo()) RETURN END END
// Verifica espaço em disco nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500) IF BOLETO_VerificaEspacoDisco(sPastaBase) < nEspacoMinimoMB THEN sMensagem is string = "Espaço insuficiente em disco para gerar boletos!" Error(sMensagem) // Notifica administrador BOLETO_NotificaAdministrador("ESPACO_DISCO", sMensagem) RETURN END
// Dispara processo multithread TRY Trace("Chamando thread de geração paralela...") BOLETO_GeraPDFs_Threaded() Trace("Processo iniciado com sucesso. Tempo de execução: " + DateTimeDifference(Now(), dHoraInicio) + " segundos") CATCH sMensagem is string = "Exceção no processo principal: " + ExceptionInfo() BOLETO_LogRegistro("SISTEMA", "CRITICO", sMensagem) Error(sMensagem) END
//############################## // *BOLETO_LogRegistro* //##############################
PROCEDURE BOLETO_LogRegistro(sTipoErro is string, sSeveridade is string, sMensagem is string, sCPFCliente is string = "", sDetalhesTecnicos is string = "")
TRY tbl_log_erros.data = Today() tbl_log_erros.hora = Now() tbl_log_erros.tipo_erro = sTipoErro tbl_log_erros.severidade = sSeveridade tbl_log_erros.mensagem = sMensagem tbl_log_erros.cpf_cliente = sCPFCliente tbl_log_erros.id_processamento = gnIdProcessamentoAtual tbl_log_erros.usuario = CurrentUser() tbl_log_erros.maquina = NetMachineName() tbl_log_erros.detalhes_tecnicos = sDetalhesTecnicos IF HAdd(tbl_log_erros) THEN // Log adicionado com sucesso IF sSeveridade = "CRITICO" THEN // Envia notificação para administrador em caso de erro crítico BOLETO_NotificaAdministrador(sTipoErro, sMensagem) END ELSE // Falha ao registrar na tabela, tenta salvar em arquivo fSaveText("C:\Boletos\log_erros.txt", DateToString(Today()) + " " + TimeToString(Now()) + " - [" + sTipoErro + "][" + sSeveridade + "] " + sMensagem + " - CPF: " + sCPFCliente + CR, foAppend) END // Exibe no trace para debug Trace("[" + sTipoErro + "][" + sSeveridade + "] " + sMensagem) CATCH // Último recurso se tudo falhar fSaveText("C:\Boletos\log_emergencia.txt", DateToString(Today()) + " " + TimeToString(Now()) + " - FALHA NO SISTEMA DE LOG: " + sMensagem + " - " + ExceptionInfo() + CR, foAppend) END
//############################## // *BOLETO_VerificaProcessoEmExecucao* //##############################
FUNCTION BOLETO_VerificaProcessoEmExecucao()
bProcessoAtivo is boolean = False
// Verifica na tabela de controle se existe processo no status "EM_EXECUCAO" sQuery is string = "SELECT COUNT(*) AS total FROM tbl_controle_processamento " + "WHERE status IN ('INICIADO', 'GERANDO_PDFS', 'ENVIANDO_EMAILS') " + "AND DATE(data_exec) = DATE(NOW()) " + "AND (TIMESTAMPDIFF(MINUTE, CONCAT(data_exec, ' ', hora_exec), NOW()) < 120)" // Processos iniciados há menos de 2 horas HExecuteSQLQuery(qry_proc, hQueryDefault, sQuery)
IF NOT HOut(qry_proc) AND qry_proc.total > 0 THEN bProcessoAtivo = True END
RETURN bProcessoAtivo
//############################## // *BOLETO_IniciaProcessamento* //##############################
FUNCTION BOLETO_IniciaProcessamento(sData is string, sHora is string)
nId is int = 0
// Verifica espaço em disco inicial nEspacoInicial is int = BOLETO_VerificaEspacoDisco("C:\")
HReset(tbl_controle_processamento) tbl_controle_processamento.data_exec = DateFromString(sData, "YYYYMMDD") tbl_controle_processamento.hora_exec = TimeFromString(sHora, "HHmmss") tbl_controle_processamento.total_clientes = 0 tbl_controle_processamento.pdfs_gerados = 0 tbl_controle_processamento.emails_enviados = 0 tbl_controle_processamento.falhas_pdf = 0 tbl_controle_processamento.falhas_email = 0 tbl_controle_processamento.status = "INICIADO" tbl_controle_processamento.motivo_erro = "" tbl_controle_processamento.uso_cpu_medio = 0 tbl_controle_processamento.uso_memoria_medio = 0 tbl_controle_processamento.espaco_disco_inicial = nEspacoInicial tbl_controle_processamento.espaco_disco_final = 0 tbl_controle_processamento.tempo_execucao = 0 tbl_controle_processamento.usuario = CurrentUser() tbl_controle_processamento.maquina = NetMachineName()
IF HAdd(tbl_controle_processamento) THEN nId = tbl_controle_processamento.id gnIdProcessamentoAtual = nId // Variável global para referência BOLETO_LogRegistro("PROCESSAMENTO", "INFO", "Processamento iniciado com ID: " + nId) ELSE BOLETO_LogRegistro("PROCESSAMENTO", "ERRO", "Falha ao iniciar registro de processamento", "", HErrorInfo()) END
RETURN nId
//############################## // *BOLETO_AtualizaProcessamento* //##############################
PROCEDURE BOLETO_AtualizaProcessamento(nId is int, sCampo is string, xValor is variant, bIncrementar is boolean = False)
IF nId <= 0 THEN RETURN
TRY sQuery is string IF bIncrementar THEN // Incrementa o valor atual sQuery = "UPDATE tbl_controle_processamento SET " + sCampo + " = " + sCampo + " + ? WHERE id = ?" ELSE // Substitui o valor atual sQuery = "UPDATE tbl_controle_processamento SET " + sCampo + " = ? WHERE id = ?" END HExecuteSQLQuery(qry_update, hQueryDefault, sQuery, xValor, nId) CATCH BOLETO_LogRegistro("PROCESSAMENTO", "ERRO", "Falha ao atualizar processamento: " + sCampo + " = " + xValor, "", ExceptionInfo()) END
//############################## // *BOLETO_VerificaEspacoDisco* //##############################
FUNCTION BOLETO_VerificaEspacoDisco(sDiretorio is string)
// Retorna espaço livre em MB nEspacoLivre is int = 0
TRY // Utiliza comando do sistema para obter informações do disco sComando is string = "wmic logicaldisk where DeviceID=\"" + Left(sDiretorio, 2) + "\" get FreeSpace" sResultado is string = ExecCommandLine(sComando) // Extrai o valor de espaço livre da resposta IF sResultado <> "" THEN sEspacoStr is string = RegexExtract(sResultado, "[0-9]+", 0) IF sEspacoStr <> "" THEN nEspacoLivre = Val(sEspacoStr) / (1024 * 1024) // Converte para MB END END // Registra informação sobre espaço em disco BOLETO_LogRegistro("ESPACO_DISCO", "INFO", "Espaço livre em " + Left(sDiretorio, 2) + ": " + nEspacoLivre + " MB") CATCH BOLETO_LogRegistro("ESPACO_DISCO", "ERRO", "Falha ao verificar espaço em disco", "", ExceptionInfo()) nEspacoLivre = 0 // Em caso de erro, assume zero para forçar verificação de segurança END
RETURN nEspacoLivre
//############################## // *BOLETO_VerificaEspacoPeriodicoPDF* //##############################
PROCEDURE BOLETO_VerificaEspacoPeriodicoPDF(sDiretorio is string, nEspacoMinimoMB is int)
// Verifica se há espaço suficiente nEspacoAtual is int = BOLETO_VerificaEspacoDisco(sDiretorio)
IF nEspacoAtual < nEspacoMinimoMB THEN // Espaço insuficiente - registra erro crítico sMensagem is string = "ALERTA: Espaço em disco crítico! Disponível: " + nEspacoAtual + " MB, Mínimo necessário: " + nEspacoMinimoMB + " MB" BOLETO_LogRegistro("ESPACO_DISCO", "CRITICO", sMensagem) RETURN False END
RETURN True
//############################## // *BOLETO_NotificaAdministrador* //##############################
PROCEDURE BOLETO_NotificaAdministrador(sTipoErro is string, sMensagem is string)
TRY // Carrega configurações de e-mail do administrador sEmailAdmin is string = GetConfigValue("EMAIL_ADMIN", "admin@empresa.com.br") // Configura e-mail EmailReset() // Carrega configurações SMTP HReadFirst(tbl_config_smtp) IF HFound(tbl_config_smtp) THEN Email.ServerAddress = tbl_config_smtp.servidor Email.Port = tbl_config_smtp.porta Email.UserName = tbl_config_smtp.usuario Email.Password = BOLETO_DescriptografaSenha(tbl_config_smtp.senha) Email.Authentication = True IF tbl_config_smtp.usar_ssl THEN Email.Secure = emailSecureSSL END ELSE BOLETO_LogRegistro("EMAIL", "ERRO", "Configurações SMTP não encontradas para notificação ao administrador") RETURN END // Configura mensagem Email.Subject = "ALERTA: " + sTipoErro + " - Sistema de Boletos" Email.Message = "Ocorreu um problema no sistema de geração de boletos:" + CR + CR + "Tipo: " + sTipoErro + CR + "Mensagem: " + sMensagem + CR + CR + "Data/Hora: " + DateToString(Today()) + " " + TimeToString(Now()) + CR + "Máquina: " + NetMachineName() + CR + "Usuário: " + CurrentUser() + CR + CR + "Este é um e-mail automático, por favor não responda." Email.Recipient[1] = sEmailAdmin Email.Name = "Sistema de Boletos" Email.Address = tbl_config_smtp.email_remetente // Envia e-mail IF EmailSendMessage() THEN BOLETO_LogRegistro("EMAIL", "INFO", "Notificação enviada ao administrador: " + sEmailAdmin) ELSE BOLETO_LogRegistro("EMAIL", "ERRO", "Falha ao enviar notificação ao administrador", "", ErrorInfo()) END CATCH BOLETO_LogRegistro("EMAIL", "ERRO", "Exceção ao enviar notificação ao administrador", "", ExceptionInfo()) END
//############################## // *BOLETO_VerificaUsoCPU* //##############################
FUNCTION BOLETO_VerificaUsoCPU()
// Retorna uso de CPU em porcentagem (0-100) nUsoCPU is int = 0
TRY // Comando para obter uso de CPU sComando is string = "wmic cpu get loadpercentage" sResultado is string = ExecCommandLine(sComando) // Extrai o valor de uso de CPU IF sResultado <> "" THEN sUsoCPUStr is string = RegexExtract(sResultado, "[0-9]+", 0) IF sUsoCPUStr <> "" THEN nUsoCPU = Val(sUsoCPUStr) END END CATCH BOLETO_LogRegistro("RECURSOS", "ERRO", "Falha ao verificar uso de CPU", "", ExceptionInfo()) nUsoCPU = 100 // Em caso de erro, assume 100% para ser conservador END
RETURN nUsoCPU
//############################## // *BOLETO_VerificaUsoMemoria* //##############################
FUNCTION BOLETO_VerificaUsoMemoria()
// Retorna uso de memória em porcentagem (0-100) nUsoMemoria is int = 0
TRY // Comando para obter informações de memória sComando is string = "wmic OS get FreePhysicalMemory,TotalVisibleMemorySize" sResultado is string = ExecCommandLine(sComando) // Extrai valores de memória livre e total IF sResultado <> "" THEN sMemoriaLivreStr is string = RegexExtract(sResultado, "[0-9]+", 0) sMemoriaTotalStr is string = RegexExtract(sResultado, "[0-9]+", 1) IF sMemoriaLivreStr <> "" AND sMemoriaTotalStr <> "" THEN nMemoriaLivre is int = Val(sMemoriaLivreStr) nMemoriaTotal is int = Val(sMemoriaTotalStr) IF nMemoriaTotal > 0 THEN nUsoMemoria = 100 - ((nMemoriaLivre * 100) / nMemoriaTotal) END END END CATCH BOLETO_LogRegistro("RECURSOS", "ERRO", "Falha ao verificar uso de memória", "", ExceptionInfo()) nUsoMemoria = 100 // Em caso de erro, assume 100% para ser conservador END
RETURN nUsoMemoria
//############################## // *BOLETO_AjustaNumeroThreads* //##############################
FUNCTION BOLETO_AjustaNumeroThreads(nMaxThreadsConfig is int)
// Calcula número ideal de threads com base no uso de recursos nMaxThreads is int = nMaxThreadsConfig nLimiteUsoRecursos is int = GetConfigValue("LIMITE_USO_RECURSOS", 80) // Limite de 80% por padrão
// Verifica uso atual de recursos nUsoCPU is int = BOLETO_VerificaUsoCPU() nUsoMemoria is int = BOLETO_VerificaUsoMemoria()
// Usa o recurso mais crítico para ajustar nUsoMaisAlto is int = Max(nUsoCPU, nUsoMemoria)
// Ajusta número de threads com base no uso de recursos IF nUsoMaisAlto > nLimiteUsoRecursos THEN // Reduz threads proporcionalmente ao excesso de uso nFatorReducao is numeric = (nUsoMaisAlto - nLimiteUsoRecursos) / 20 + 1 // +1 para garantir pelo menos alguma redução nMaxThreads = Max(1, nMaxThreadsConfig / nFatorReducao) // Garante pelo menos 1 thread // Registra ajuste BOLETO_LogRegistro("RECURSOS", "AVISO", "Uso de recursos em " + nUsoMaisAlto + "%. Reduzindo threads de " + nMaxThreadsConfig + " para " + nMaxThreads) END
RETURN nMaxThreads
//############################## // *BOLETO_RegistraRastreabilidade* //##############################
PROCEDURE BOLETO_RegistraRastreabilidade(sCPF is string, sEvento is string, nIdProcessamento is int)
HReset(tbl_rastreabilidade_boleto) tbl_rastreabilidade_boleto.cpf_cliente = sCPF tbl_rastreabilidade_boleto.evento = sEvento tbl_rastreabilidade_boleto.data = Today() tbl_rastreabilidade_boleto.hora = Now() tbl_rastreabilidade_boleto.id_processamento = nIdProcessamento tbl_rastreabilidade_boleto.usuario = CurrentUser() tbl_rastreabilidade_boleto.maquina = NetMachineName() HAdd(tbl_rastreabilidade_boleto)
//############################## // *BOLETO_RegistraReprocessamento* //##############################
PROCEDURE BOLETO_RegistraReprocessamento(sCPFCliente is string, sTipoFalha is string, sMotivoFalha is string)
TRY HReset(tbl_reprocessamento) tbl_reprocessamento.cpf_cliente = sCPFCliente tbl_reprocessamento.tipo_falha = sTipoFalha tbl_reprocessamento.data_falha = Today() tbl_reprocessamento.hora_falha = Now() tbl_reprocessamento.id_processamento_original = gnIdProcessamentoAtual tbl_reprocessamento.motivo_falha = sMotivoFalha tbl_reprocessamento.status = "PENDENTE" tbl_reprocessamento.tentativas = 0 IF HAdd(tbl_reprocessamento) THEN BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Item registrado para reprocessamento: CPF " + sCPFCliente + ", Tipo: " + sTipoFalha) ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao registrar item para reprocessamento", sCPFCliente, HErrorInfo()) END CATCH BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao registrar item para reprocessamento", sCPFCliente, ExceptionInfo()) END
//############################## // *BOLETO_ReprocessarFalhas* //##############################
PROCEDURE BOLETO_ReprocessarFalhas(sTipoFalha is string = "")
// Verifica se já existe processo em execução IF BOLETO_VerificaProcessoEmExecucao() THEN Error("Já existe um processo de geração de boletos em execução! Aguarde ou verifique os logs.") RETURN END
// Cria diretório por data com horário para evitar conflitos em múltiplas execuções sDataHoje is string = DateToString(Today(), "YYYYMMDD") sHoraExec is string = TimeToString(Now(), "HHmmss") sPastaBoletos is string = "C:\Boletos\reprocessamento_" + sDataHoje + "_" + sHoraExec
// Verifica e cria diretório IF NOT fDirectoryExist(sPastaBoletos) THEN IF NOT fMakeDir(sPastaBoletos) THEN BOLETO_LogRegistro("SISTEMA", "CRITICO", "Falha ao criar diretório: " + sPastaBoletos) RETURN END END
// Verifica espaço em disco inicial nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_MB", 500) IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN // Espaço insuficiente - notifica e cancela Error("Espaço em disco insuficiente para iniciar o reprocessamento. Verifique os logs.") RETURN END
// Inicia registro de processamento nIdProcessamento is int = BOLETO_IniciaProcessamento(sDataHoje, sHoraExec) IF nIdProcessamento = 0 THEN BOLETO_LogRegistro("PROCESSAMENTO", "CRITICO", "Falha ao iniciar registro de processamento para reprocessamento") RETURN END
// Atualiza status BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "REPROCESSANDO")
// Busca itens para reprocessamento sQuery is string = "SELECT id, cpf_cliente, tipo_falha, motivo_falha FROM tbl_reprocessamento WHERE status = 'PENDENTE'" IF sTipoFalha <> "" THEN sQuery += " AND tipo_falha = '" + sTipoFalha + "'" END sQuery += " ORDER BY data_falha, hora_falha"
HExecuteSQLQuery(qry_reprocessamento, hQueryDefault, sQuery)
// Conta total de registros para monitoramento nTotalItens is int = 0 WHILE NOT HOut(qry_reprocessamento) nTotalItens += 1 HReadNext(qry_reprocessamento) END HReadFirst(qry_reprocessamento)
IF nTotalItens = 0 THEN BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Nenhum item pendente para reprocessamento") BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO") RETURN END
BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Total de itens para reprocessamento: " + nTotalItens) BOLETO_AtualizaProcessamento(nIdProcessamento, "total_clientes", nTotalItens)
// Processa cada item WHILE NOT HOut(qry_reprocessamento) sCPF is string = qry_reprocessamento.cpf_cliente sTipo is string = qry_reprocessamento.tipo_falha nIdItem is int = qry_reprocessamento.id BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Reprocessando item: CPF " + sCPF + ", Tipo: " + sTipo) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.tentativas += 1 tbl_reprocessamento.data_reprocessamento = Today() tbl_reprocessamento.hora_reprocessamento = Now() tbl_reprocessamento.id_processamento_reprocessamento = nIdProcessamento HModify(tbl_reprocessamento) END // Reprocessa conforme o tipo de falha IF sTipo = "PDF" THEN // Reprocessa geração de PDF BOLETO_ReprocessaPDF(sCPF, sPastaBoletos, nIdProcessamento, nIdItem) ELSE IF sTipo = "EMAIL" THEN // Reprocessa envio de e-mail BOLETO_ReprocessaEmail(sCPF, sPastaBoletos, nIdProcessamento, nIdItem) END HReadNext(qry_reprocessamento) END
// Atualiza status final BOLETO_AtualizaProcessamento(nIdProcessamento, "status", "CONCLUIDO") BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "Reprocessamento concluído")
//############################## // *BOLETO_ReprocessaPDF* //##############################
PROCEDURE BOLETO_ReprocessaPDF(sCPF is string, sPasta is string, nIdProcessamento is int, nIdItem is int)
TRY // Verifica espaço em disco nEspacoMinimoMB is int = GetConfigValue("ESPACO_MINIMO_PDF", 10) IF NOT BOLETO_VerificaEspacoPeriodicoPDF("C:\", nEspacoMinimoMB) THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Espaço insuficiente para reprocessar PDF", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Espaço insuficiente em disco" HModify(tbl_reprocessamento) END RETURN END // Gera o PDF sNomePDF is string = sPasta + "\" + sCPF + "_" + StringReplace(TimeToString(Now(), "HHMMSS"), ":", "") + ".pdf" IF iInitReportQuery(REP_BOLETO) THEN iParameter("CPF", sCPF) iDestination(iPDF) IF iPrintReport(REP_BOLETO, sNomePDF) THEN IF fFileExist(sNomePDF) THEN // Atualiza tabela de saldo IF HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF, hLockWrite) THEN tbl_saldo.pdf_gerado = True tbl_saldo.nome_pdf = fExtractPath(sNomePDF, fFileName + fExtension) tbl_saldo.data_geracao = Today() tbl_saldo.hora_geracao = Now() IF HModify(tbl_saldo) THEN BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "PDF reprocessado com sucesso", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "REPROCESSADO" HModify(tbl_reprocessamento) END // Registra em tabela de rastreabilidade BOLETO_RegistraRastreabilidade(sCPF, "PDF_REPROCESSADO", nIdProcessamento) // Incrementa contador de PDFs gerados BOLETO_AtualizaProcessamento(nIdProcessamento, "pdfs_gerados", 1, True) ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao atualizar tbl_saldo", sCPF, HErrorInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Falha ao atualizar registro: " + HErrorInfo() HModify(tbl_reprocessamento) END END ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao bloquear registro na tbl_saldo", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Falha ao bloquear registro" HModify(tbl_reprocessamento) END END ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não encontrado após geração: " + sNomePDF, sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Arquivo não encontrado após geração" HModify(tbl_reprocessamento) END END ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao gerar PDF", sCPF, iErrorInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Falha na impressão do relatório: " + iErrorInfo() HModify(tbl_reprocessamento) END END ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao inicializar relatório", sCPF, iErrorInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Falha ao inicializar relatório: " + iErrorInfo() HModify(tbl_reprocessamento) END END CATCH BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao reprocessar PDF", sCPF, ExceptionInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Exceção: " + ExceptionInfo() HModify(tbl_reprocessamento) END END
//############################## // *BOLETO_ReprocessaEmail* //##############################
PROCEDURE BOLETO_ReprocessaEmail(sCPF is string, sPasta is string, nIdProcessamento is int, nIdItem is int)
TRY // Verifica se o PDF existe HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF) IF NOT HFound(tbl_saldo) OR NOT tbl_saldo.pdf_gerado THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não gerado para reprocessar e-mail", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "PDF não gerado para envio de e-mail" HModify(tbl_reprocessamento) END RETURN END // Busca informações do cliente HReadSeekFirst(tbl_cliente, cpf, sCPF) IF NOT HFound(tbl_cliente) THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Cliente não encontrado para reprocessar e-mail", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Cliente não encontrado" HModify(tbl_reprocessamento) END RETURN END // Verifica e-mail IF tbl_cliente.email = "" OR NOT StringRegExMatch(tbl_cliente.email, "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "E-mail inválido: " + tbl_cliente.email, sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "E-mail inválido: " + tbl_cliente.email HModify(tbl_reprocessamento) END RETURN END // Verifica se o arquivo PDF existe sCaminhoPDF is string = "" IF tbl_saldo.nome_pdf <> "" THEN // Tenta encontrar o PDF na pasta original sCaminhoPDF = "C:\Boletos\" + tbl_saldo.nome_pdf IF NOT fFileExist(sCaminhoPDF) THEN // Tenta encontrar em todas as pastas de boletos sCaminhoPDF = "" sListaPastas is string = fListDirectory("C:\Boletos", frDirectory) sListaPastas = StringBuild(sListaPastas, CR) nPos is int = 1 WHILE nPos > 0 sPastaAtual is string = ExtractString(sListaPastas, CR, nPos) IF sPastaAtual <> "" THEN sCaminhoTeste is string = "C:\Boletos\" + sPastaAtual + "\" + tbl_saldo.nome_pdf IF fFileExist(sCaminhoTeste) THEN sCaminhoPDF = sCaminhoTeste BREAK END END END END END IF sCaminhoPDF = "" THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "PDF não encontrado para reprocessar e-mail", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "PDF não encontrado para envio" HModify(tbl_reprocessamento) END RETURN END // Carrega configurações de SMTP HReadFirst(tbl_config_smtp) IF NOT HFound(tbl_config_smtp) THEN BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Configurações SMTP não encontradas", sCPF) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Configurações SMTP não encontradas" HModify(tbl_reprocessamento) END RETURN END // Configura e-mail EmailReset() Email.ServerAddress = tbl_config_smtp.servidor Email.Port = tbl_config_smtp.porta Email.UserName = tbl_config_smtp.usuario Email.Password = BOLETO_DescriptografaSenha(tbl_config_smtp.senha) Email.Name = "Cobranca WX" Email.Address = tbl_config_smtp.email_remetente Email.Authentication = True IF tbl_config_smtp.usar_ssl THEN Email.Secure = emailSecureSSL END // Configura mensagem Email.Subject = "Boleto em aberto - WX Soluções" Email.Message = "Prezado(a) " + tbl_cliente.nome + "," + CR + CR + "Segue em anexo o boleto referente ao saldo em aberto na sua conta." + CR + CR + "Para qualquer dúvida, entre em contato com nosso suporte." + CR + CR + "Atenciosamente," + CR + "Equipe WX Soluções" Email.Recipient[1] = tbl_cliente.email Email.Attachments[1] = sCaminhoPDF // Envia e-mail IF EmailSendMessage() THEN BOLETO_LogRegistro("REPROCESSAMENTO", "INFO", "E-mail reprocessado com sucesso", sCPF) // Atualiza tabela de saldo IF HReadSeekFirst(tbl_saldo, cpf_cliente, sCPF, hLockWrite) THEN tbl_saldo.email_enviado = True tbl_saldo.data_envio = Today() tbl_saldo.hora_envio = Now() HModify(tbl_saldo) END // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "REPROCESSADO" HModify(tbl_reprocessamento) END // Registra em tabela de rastreabilidade BOLETO_RegistraRastreabilidade(sCPF, "EMAIL_REPROCESSADO", nIdProcessamento) // Incrementa contador de e-mails enviados BOLETO_AtualizaProcessamento(nIdProcessamento, "emails_enviados", 1, True) ELSE BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Falha ao reprocessar e-mail", sCPF, ErrorInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Falha ao enviar e-mail: " + ErrorInfo() HModify(tbl_reprocessamento) END END CATCH BOLETO_LogRegistro("REPROCESSAMENTO", "ERRO", "Exceção ao reprocessar e-mail", sCPF, ExceptionInfo()) // Atualiza status do item IF HReadSeekFirst(tbl_reprocessamento, id, nIdItem, hLockWrite) THEN tbl_reprocessamento.status = "FALHA_REPROCESSAMENTO" tbl_reprocessamento.motivo_falha = "Exceção: " + ExceptionInfo() HModify(tbl_reprocessamento) END END
//############################## // *BOLETO_CriptografaSenha* //##############################
FUNCTION BOLETO_CriptografaSenha(sSenha is string)
// Implementação real de criptografia // Esta é uma implementação básica, em produção deve-se usar algoritmos mais seguros IF Left(sSenha, 4) <> "ENC:" THEN RETURN "ENC:" + sSenha ELSE RETURN sSenha END
//############################## // *BOLETO_DescriptografaSenha* //##############################
FUNCTION BOLETO_DescriptografaSenha(sSenhaEncriptada is string)
// Implementação real de descriptografia // Esta é uma implementação básica, em produção deve-se usar algoritmos mais seguros IF Left(sSenhaEncriptada, 4) = "ENC:" THEN RETURN Mid(sSenhaEncriptada, 5) ELSE RETURN sSenhaEncriptada END
//############################## // *GetConfigValue* //##############################
FUNCTION GetConfigValue(sChave is string, xValorPadrao is variant)
// Busca configuração na tabela ou retorna valor padrão xValor is variant = xValorPadrao
HReadSeekFirst(tbl_configuracoes, chave, sChave) IF HFound(tbl_configuracoes) THEN xValor = tbl_configuracoes.valor END
RETURN xValor
//############################## // *ESTRUTURA DAS TABELAS* //##############################
/* 1. tbl_saldo: - cpf_cliente (string, chave) - saldo (numeric) - pdf_gerado (boolean) - nome_pdf (string) - email_enviado (boolean) - data_geracao (date) - hora_geracao (time) - data_envio (date) - hora_envio (time)
2. tbl_cliente: - cpf (string, chave) - email (string) - nome (string)
3. tbl_config_smtp: - servidor (string) - porta (integer) - usuario (string) - senha (string) - email_remetente (string) - usar_ssl (boolean)
4. tbl_log_erros: - id (auto-increment) - data (date) - hora (time) - tipo_erro (string) - severidade (string) - mensagem (string) - cpf_cliente (string) - id_processamento (integer) - usuario (string) - maquina (string) - detalhes_tecnicos (string)
5. tbl_controle_processamento: - id (auto-increment) - data_exec (date) - hora_exec (time) - total_clientes (integer) - pdfs_gerados (integer) - emails_enviados (integer) - falhas_pdf (integer) - falhas_email (integer) - status (string) - motivo_erro (string) - uso_cpu_medio (numeric) - uso_memoria_medio (numeric) - espaco_disco_inicial (integer) - espaco_disco_final (integer) - tempo_execucao (integer) - usuario (string) - maquina (string)
6. tbl_rastreabilidade_boleto: - id (auto-increment) - cpf_cliente (string) - evento (string) - data (date) - hora (time) - id_processamento (integer) - usuario (string) - maquina (string)
7. tbl_reprocessamento: - id (auto-increment) - cpf_cliente (string) - tipo_falha (string) - data_falha (date) - hora_falha (time) - id_processamento_original (integer) - motivo_falha (string) - status (string) - data_reprocessamento (date) - hora_reprocessamento (time) - id_processamento_reprocessamento (integer) - tentativas (integer)
8. tbl_configuracoes: - chave (string) - valor (string) - descricao (string) */
//############################## // *CONFIGURAÇÕES RECOMENDADAS* //##############################
/* // Inserir na tabela tbl_configuracoes INSERT INTO tbl_configuracoes (chave, valor, descricao) VALUES ('MAX_THREADS', '10', 'Número máximo de threads simultâneas'), ('TIMEOUT_THREAD', '120', 'Timeout por thread em segundos'), ('TIMEOUT_GLOBAL', '3600', 'Timeout global do processamento em segundos'), ('TIMEOUT_GERACAO_PDF', '60', 'Timeout para geração de cada PDF em segundos'), ('TIMEOUT_ENVIO_EMAILS', '3600', 'Timeout para envio de emails em segundos'), ('LIMITE_EMAILS_HORA', '100', 'Limite de emails por hora'), ('TEMPO_ESPERA_LOTE', '60', 'Tempo de espera entre lotes em segundos'), ('TAMANHO_LOTE_EMAIL', '20', 'Quantidade de emails por lote'), ('ESPACO_MINIMO_MB', '500', 'Espaço mínimo em disco (MB)'), ('ESPACO_MINIMO_PDF', '10', 'Espaço mínimo para gerar um PDF (MB)'), ('LIMITE_USO_RECURSOS', '80', 'Limite de uso de CPU/memória (%)'), ('INTERVALO_VERIFICACAO_RECURSOS', '10', 'Intervalo para verificação de recursos (a cada X clientes)'), ('EMAIL_ADMIN', 'admin@wxsolucoes.com.br', 'E-mail do administrador para notificações'); */
Bons estudos
-- Adriano José Boller ______________________________________________ Consultor e Representante Oficial da PcSoft no Brasil +55 (41) 99949 1800 adrianoboller@gmail.com skype: adrianoboller http://wxinformatica.com.br/ |
| |
| |
| | | |
|
| | |
| |
Registered member 4,613 messages |
|
| Posted on May, 21 2025 - 4:45 PM |
Vou explicar esse fluxo como se estivéssemos explicando para um cliente, gerente ou usuário final que não é técnico, mas precisa entender o que acontece e por quê.
⸻
Como funciona o sistema de geração de boletos? (Explicação para leigo)
Imagine que sua empresa tem milhares de clientes que devem algum valor, e você precisa: 1. Gerar um boleto (PDF) para cada cliente devedor; 2. Guardar esse boleto em uma pasta do dia; 3. Registrar o que foi feito com segurança e rastreabilidade; 4. Enviar esse boleto por e-mail automaticamente ao cliente; 5. Registrar os erros e poder tentar de novo depois automaticamente (reprocessar).
⸻
Etapas do processo
1. Início do processo • O sistema verifica se já tem outro processo rodando. Se tiver, ele espera ou avisa que já está em execução. • Cria uma pasta do dia e hora, para guardar todos os boletos gerados de forma organizada. • Verifica se o computador/servidor tem espaço suficiente no disco. Se não tiver, para tudo com segurança.
⸻
2. Buscar clientes que devem • O sistema procura todos os clientes com saldo negativo (devendo) e que ainda não receberam boleto. • Separa todos esses clientes e registra no banco de dados quantos serão processados.
⸻
3. Geração dos boletos (PDFs) • Para cada cliente: • Cria um PDF do boleto com os dados da dívida. • Salva o boleto na pasta do dia. • Se o arquivo foi criado com sucesso, marca no banco de dados que foi gerado. • Se deu erro (sem espaço, cliente inexistente, falha no PDF etc), registra o erro e salva para tentar de novo depois. • Tudo é feito em paralelo, usando múltiplas threads (como vários braços trabalhando ao mesmo tempo), mas com controle para não travar o computador.
⸻
4. Envio dos e-mails • Quando os boletos estiverem prontos: • O sistema envia um e-mail automático com o boleto anexo. • Envia em lotes pequenos, com pausas programadas, para evitar bloqueio por spam. • Se o envio deu certo, registra no banco de dados. • Se deu erro (e-mail inválido, PDF ausente, falha na conexão), registra o problema e salva para tentar reprocessar depois.
⸻
5. Finalização e Relatório • Registra o tempo total do processo, quantos boletos foram criados, quantos e-mails enviados, quantas falhas aconteceram. • Armazena tudo com informações como: • Quem rodou o processo • De qual máquina • Quanto de memória e CPU o sistema usou • Quanto de espaço em disco havia no começo e no fim
⸻
E se der problema? • Se algum boleto ou e-mail não foi gerado ou enviado, o sistema grava essas situações na tabela de “reprocessamento”. • O usuário ou o sistema pode rodar um segundo processo automático que: • Tenta gerar novamente o boleto com erro • Tenta reenviar e-mails que falharam
⸻
O que o cliente ganha com esse sistema? • Automatização total: tudo acontece sem intervenção humana; • Segurança: erros são registrados e recuperáveis; • Eficiência: uso inteligente dos recursos do computador; • Controle: tudo é rastreado, com histórico de cada tentativa; • Organização: cada boleto tem nome e pasta organizada por dia e hora; • Performance: geração e envio são rápidos por usarem múltiplos núcleos do processador.
-- Adriano José Boller ______________________________________________ Consultor e Representante Oficial da PcSoft no Brasil +55 (41) 99949 1800 adrianoboller@gmail.com skype: adrianoboller http://wxinformatica.com.br/ |
| |
| |
| | | |
|
| | | | |
| | |
|