File manager - Edit - /var/www/html/portal/database/seeders/ImportacaoProdataContaAngraSeeder.php
Back
<?php namespace Database\Seeders; use App\Http\Resources\Importacoes\MapeamentoUnidadeResource; use App\Http\Services\Importacoes\ImportacaoProdataContaAngraService; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; use Symfony\Component\Console\Helper\ProgressBar; use Throwable; /** * Seeder de importacao incremental e idempotente dos servidores municipais * (CSV exportado da Prodata) como usuarios do Conta Angra. * * Ver documentacao completa em docs/tests/importacao-prodata-contaangra/README.md */ class ImportacaoProdataContaAngraSeeder extends Seeder { public const CSV_RELATIVO = 'temp/importacao-prodata-contaangra/dados.csv'; public const CHUNK_SIZE = 500; public const STORAGE_DISK = 'local'; public const RELATORIO_DIR = 'importacao-prodata-contaangra'; protected ImportacaoProdataContaAngraService $service; protected MapeamentoUnidadeResource $mapeamento; /** @var array<string,int> */ protected array $contadores = [ 'processados' => 0, 'criados' => 0, 'atualizados' => 0, 'sincronizados' => 0, 'invalidos' => 0, 'erros' => 0, ]; /** @var array<int,array{linha:int,cpf:?string,nome:?string,motivo:string}> */ protected array $invalidos = []; /** @var array<int,array{linha:int,cpf:?string,nome:?string,erro:string}> */ protected array $erros = []; protected ?ProgressBar $barraProgresso = null; protected int $totalLinhasCsv = 0; public function __construct( ?ImportacaoProdataContaAngraService $service = null, ?MapeamentoUnidadeResource $mapeamento = null ) { $this->service = $service ?? new ImportacaoProdataContaAngraService(); $this->mapeamento = $mapeamento ?? new MapeamentoUnidadeResource(); } public function run(): void { $inicio = microtime(true); $caminho = $this->resolverCsv(); $this->info(__('Iniciando importacao a partir de :csv', ['csv' => $caminho])); Log::channel(ImportacaoProdataContaAngraService::LOG_CHANNEL)->info('importacao_iniciada', [ 'csv' => $caminho, 'chunk_size' => self::CHUNK_SIZE, ]); $this->info(__('Fase 1/4: carregando DE-PARA...')); $this->mapeamento->carregar(); if (is_file($this->mapeamento->caminhoPredefinido())) { $this->info(__('DE-PARA pré-definido: :path', ['path' => $this->mapeamento->caminhoPredefinido()])); } $this->info(__('Fase 2/4: contando registros do CSV (pode levar alguns segundos)...')); $this->totalLinhasCsv = $this->contarLinhasDadosCsv($caminho); $this->info(__('Total de registros a importar: :total', ['total' => $this->totalLinhasCsv])); $this->info(__('Fase 3/4: validando mapeamento de unidades...')); $unidadesDistintas = $this->coletarUnidadesDistintas($caminho); $this->info(__('Unidades distintas: :total', ['total' => count($unidadesDistintas)])); $this->mapeamento->prepararParaUnidades($unidadesDistintas, $this->comandoParaPromptDePara()); $this->mapeamento->salvar(); $this->info(__('Fase 4/4: importando usuarios (lotes de :chunk)...', ['chunk' => self::CHUNK_SIZE])); $this->iniciarBarraProgresso(); $this->processarArquivo($caminho); $this->finalizarBarraProgresso(); $duracao = round(microtime(true) - $inicio, 2); $this->gerarRelatorios($duracao); Log::channel(ImportacaoProdataContaAngraService::LOG_CHANNEL)->info('importacao_concluida', [ 'duracao_segundos' => $duracao, 'contadores' => $this->contadores, ]); $this->exibirRelatorioConsole($duracao); } /** * Prompt DE-PARA apenas na fase preparatoria. Em ambiente `testing` ou * sem comando anexado, retorna null (usa so JSON predefinido, storage e * match automatico). */ protected function comandoParaPromptDePara(): ?\Illuminate\Console\Command { if ($this->command === null) { return null; } if (app()->environment('testing')) { return null; } $argv = $_SERVER['argv'] ?? []; if (in_array('--no-interaction', $argv, true) || in_array('-n', $argv, true)) { return null; } return $this->command; } /** * Le o CSV em chunks de :CHUNK_SIZE linhas, abrindo uma transacao por * chunk para minimizar locks em producao. Erros em linhas especificas nao * interrompem o processamento do restante. */ protected function processarArquivo(string $caminho): void { $chunk = []; $numeroLinha = 1; foreach ($this->lerCsv($caminho) as $linha) { $numeroLinha++; $chunk[] = [$numeroLinha, $linha]; if (count($chunk) >= self::CHUNK_SIZE) { $this->processarChunk($chunk); $chunk = []; } } if ($chunk !== []) { $this->processarChunk($chunk); } } /** * Processa um chunk inteiro dentro de uma transacao isolada. Excecoes em * uma linha sao capturadas e logadas, mas a transacao do chunk continua. * * @param array<int,array{0:int,1:array<string,string>}> $chunk */ protected function processarChunk(array $chunk): void { DB::beginTransaction(); try { foreach ($chunk as [$numeroLinha, $linha]) { $this->contadores['processados']++; try { $structureId = $this->mapeamento->resolver( $linha['unidade'] ?? null, null ); $resultado = $this->service->processarLinha($linha, $structureId, $numeroLinha); switch ($resultado['resultado']) { case ImportacaoProdataContaAngraService::RESULTADO_CRIADO: $this->contadores['criados']++; break; case ImportacaoProdataContaAngraService::RESULTADO_ATUALIZADO: $this->contadores['atualizados']++; break; case ImportacaoProdataContaAngraService::RESULTADO_SINCRONIZADO: $this->contadores['sincronizados']++; break; case ImportacaoProdataContaAngraService::RESULTADO_INVALIDO: $this->contadores['invalidos']++; $this->invalidos[] = [ 'linha' => $numeroLinha, 'cpf' => $resultado['cpf'] ?? null, 'nome' => $this->nomeDaLinhaCsv($linha), 'motivo' => (string) ($resultado['mensagem'] ?? __('Invalido')), ]; break; } } catch (Throwable $e) { $this->contadores['erros']++; $this->erros[] = [ 'linha' => $numeroLinha, 'cpf' => $linha['cpf'] ?? null, 'nome' => $this->nomeDaLinhaCsv($linha), 'erro' => $e->getMessage(), ]; Log::channel(ImportacaoProdataContaAngraService::LOG_CHANNEL)->error('erro_linha', [ 'linha' => $numeroLinha, 'cpf' => $linha['cpf'] ?? null, 'erro' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); } $this->barraProgresso?->advance(); } DB::commit(); $this->atualizarMensagemBarraProgresso(); } catch (Throwable $e) { DB::rollBack(); $this->contadores['erros']++; Log::channel(ImportacaoProdataContaAngraService::LOG_CHANNEL)->critical('erro_chunk', [ 'erro' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $this->command?->error(__('Erro critico no chunk: :msg', ['msg' => $e->getMessage()])); } } /** * Conta linhas de dados (exclui cabecalho e linhas vazias) para dimensionar a barra de progresso. */ protected function contarLinhasDadosCsv(string $caminho): int { $handle = fopen($caminho, 'r'); if ($handle === false) { throw new \RuntimeException("Nao foi possivel abrir o CSV: {$caminho}"); } $total = 0; try { if (fgetcsv($handle) === false) { return 0; } while (($row = fgetcsv($handle)) !== false) { if (!$this->linhaVazia($row)) { $total++; } } } finally { fclose($handle); } return $total; } protected function iniciarBarraProgresso(): void { if ($this->command === null || $this->totalLinhasCsv <= 0) { return; } $this->barraProgresso = $this->command->getOutput()->createProgressBar($this->totalLinhasCsv); $this->barraProgresso->setFormat( ' %current%/%max% [%bar%] %percent:3s%% | %elapsed:6s% | %memory:6s% | %message%' ); $this->barraProgresso->setMessage($this->formatarMensagemProgresso()); $this->barraProgresso->start(); } protected function finalizarBarraProgresso(): void { if ($this->barraProgresso === null) { return; } $this->barraProgresso->finish(); $this->command?->newLine(2); $this->barraProgresso = null; } protected function atualizarMensagemBarraProgresso(): void { $this->barraProgresso?->setMessage($this->formatarMensagemProgresso()); } protected function formatarMensagemProgresso(): string { return sprintf( 'criados:%d atualizados:%d sincronizados:%d invalidos:%d erros:%d', $this->contadores['criados'], $this->contadores['atualizados'], $this->contadores['sincronizados'], $this->contadores['invalidos'], $this->contadores['erros'] ); } /** * Le o CSV via generator, devolvendo cada linha como array associativo com * chaves padronizadas em minusculo: cpf, matricula, nome, unidade, email. * * @return \Generator<int,array<string,string>> */ protected function lerCsv(string $caminho): \Generator { $handle = fopen($caminho, 'r'); if ($handle === false) { throw new \RuntimeException("Nao foi possivel abrir o CSV: {$caminho}"); } try { $header = fgetcsv($handle); if ($header === false) { return; } $header = $this->normalizarHeader($header); while (($row = fgetcsv($handle)) !== false) { if ($this->linhaVazia($row)) { continue; } $linha = []; foreach ($header as $indice => $coluna) { $linha[$coluna] = isset($row[$indice]) ? trim((string) $row[$indice]) : ''; } yield $linha; } } finally { fclose($handle); } } /** * Normaliza o cabecalho do CSV para chaves curtas em minusculo, suportando * variacoes como "CPF (int8)", "EMAIL (varchar)" etc. * * @param array<int,string> $header * @return array<int,string> */ protected function normalizarHeader(array $header): array { $aliases = [ 'cpf' => 'cpf', 'matricula' => 'matricula', 'matr' => 'matricula', 'nome' => 'nome', 'unidade' => 'unidade', 'orgao' => 'unidade', 'email' => 'email', 'e mail' => 'email', ]; $resultado = []; foreach ($header as $coluna) { $coluna = preg_replace('/\([^\)]*\)/', '', (string) $coluna) ?? ''; $coluna = trim($coluna, " \t\n\r\0\x0B\""); $semAcentos = @iconv('UTF-8', 'ASCII//TRANSLIT', $coluna); $base = mb_strtolower($semAcentos !== false ? $semAcentos : $coluna); $base = preg_replace('/[^a-z0-9 ]+/', ' ', $base); $base = preg_replace('/\s+/', ' ', (string) $base); $base = trim((string) $base); $resultado[] = $aliases[$base] ?? $base; } return $resultado; } /** * Coleta antecipadamente todas as unidades distintas do CSV para que o * DE-PARA possa ser construido de uma so vez no inicio. * * @return array<int,string> */ protected function coletarUnidadesDistintas(string $caminho): array { $unidades = []; foreach ($this->lerCsv($caminho) as $linha) { $valor = trim((string) ($linha['unidade'] ?? '')); if ($valor !== '') { $unidades[$valor] = true; } } return array_keys($unidades); } protected function linhaVazia(array $row): bool { foreach ($row as $valor) { if (trim((string) $valor) !== '') { return false; } } return true; } protected function resolverCsv(): string { $caminho = base_path(self::CSV_RELATIVO); if (!file_exists($caminho)) { throw new \RuntimeException("CSV nao encontrado em " . self::CSV_RELATIVO); } return $caminho; } /** * Persiste relatorio em JSON (estrutural) e em Markdown (humano) para * consulta posterior. */ protected function gerarRelatorios(float $duracao): void { $payload = [ 'gerado_em' => now()->toIso8601String(), 'duracao_segundos' => $duracao, 'contadores' => $this->contadores, 'invalidos' => $this->invalidos, 'erros' => $this->erros, ]; $timestamp = now()->format('Ymd_His'); $diskName = self::STORAGE_DISK; Storage::disk($diskName)->put( self::RELATORIO_DIR . "/relatorio-{$timestamp}.json", json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); Storage::disk($diskName)->put( self::RELATORIO_DIR . "/relatorio-{$timestamp}.md", $this->renderizarRelatorioMarkdown($duracao) ); $conflitosEmail = $this->service->getConflitosEmailDetalhados(); if ($conflitosEmail !== []) { Storage::disk($diskName)->put( self::RELATORIO_DIR . "/conflitos-email-{$timestamp}.md", ImportacaoProdataContaAngraService::renderizarRelatorioConflitosEmailMarkdown($conflitosEmail) ); Storage::disk($diskName)->put( self::RELATORIO_DIR . "/conflitos-email-{$timestamp}.json", json_encode([ 'gerado_em' => now()->toIso8601String(), 'total' => count($conflitosEmail), 'conflitos' => $conflitosEmail, ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ); } } protected function renderizarRelatorioMarkdown(float $duracao): string { $linhas = []; $linhas[] = '# Relatorio de Importacao Prodata - Conta Angra'; $linhas[] = ''; $linhas[] = '- Gerado em: ' . now()->toDateTimeString(); $linhas[] = '- Duracao: ' . $duracao . 's'; $linhas[] = ''; $linhas[] = '## Contadores'; $linhas[] = ''; $linhas[] = '| Indicador | Total |'; $linhas[] = '| --- | ---: |'; foreach ($this->contadores as $nome => $total) { $linhas[] = '| ' . $nome . ' | ' . $total . ' |'; } if ($this->invalidos !== []) { $linhas[] = ''; $linhas[] = '## Registros invalidos (' . count($this->invalidos) . ')'; $linhas[] = ''; $linhas[] = '| Linha | Nome | CPF | Motivo |'; $linhas[] = '| ---: | --- | --- | --- |'; foreach ($this->invalidos as $item) { $linhas[] = sprintf( '| %d | %s | %s | %s |', $item['linha'], $this->escaparCelulaRelatorio($item['nome'] ?? null), $this->escaparCelulaRelatorio($item['cpf'] ?? null), $this->escaparCelulaRelatorio($item['motivo']) ); } } if ($this->erros !== []) { $linhas[] = ''; $linhas[] = '## Erros (' . count($this->erros) . ')'; $linhas[] = ''; $linhas[] = '| Linha | Nome | CPF | Erro |'; $linhas[] = '| ---: | --- | --- | --- |'; foreach ($this->erros as $item) { $linhas[] = sprintf( '| %d | %s | %s | %s |', $item['linha'], $this->escaparCelulaRelatorio($item['nome'] ?? null), $this->escaparCelulaRelatorio($item['cpf'] ?? null), $this->escaparCelulaRelatorio($item['erro']) ); } } return implode(PHP_EOL, $linhas) . PHP_EOL; } protected function exibirRelatorioConsole(float $duracao): void { $this->info(str_repeat('=', 60)); $this->info(__('Relatorio final da importacao')); $this->info(str_repeat('=', 60)); foreach ($this->contadores as $nome => $total) { $this->info(sprintf(' %-15s : %d', $nome, $total)); } $this->info(sprintf(' %-15s : %.2fs', 'duracao', $duracao)); $this->info(str_repeat('=', 60)); if ($this->invalidos !== []) { $this->command?->warn(__('Invalidos (:total) registrados no relatorio.', ['total' => count($this->invalidos)])); } if ($this->erros !== []) { $this->command?->error(__('Erros (:total) registrados no relatorio.', ['total' => count($this->erros)])); } $totalConflitosEmail = count($this->service->getConflitosEmailDetalhados()); if ($totalConflitosEmail > 0) { $this->command?->warn(__('Conflitos de e-mail (:total): relatorio conflitos-email-*.md em storage/app/:dir', [ 'total' => $totalConflitosEmail, 'dir' => self::RELATORIO_DIR, ])); } } protected function info(string $mensagem): void { if ($this->command !== null) { $this->command->info($mensagem); } } /** * @param array<string,string> $linha */ protected function nomeDaLinhaCsv(array $linha): ?string { $nome = trim((string) ($linha['nome'] ?? '')); return $nome !== '' ? $nome : null; } protected function escaparCelulaRelatorio(mixed $valor): string { if ($valor === null || $valor === '') { return '—'; } return str_replace('|', '\\|', (string) $valor); } /** * @return array<string,int> */ public function getContadores(): array { return $this->contadores; } /** * @return array<int,array{linha:int,cpf:?string,nome:?string,motivo:string}> */ public function getInvalidos(): array { return $this->invalidos; } /** * @return array<int,array{linha:int,cpf:?string,nome:?string,erro:string}> */ public function getErros(): array { return $this->erros; } }
| ver. 1.4 |
Github
|
.
| PHP 8.3.28 | Generation time: 0.02 |
proxy
|
phpinfo
|
Settings