Implementacao upload anexos de ordens

This commit is contained in:
HalbeBruno
2026-03-12 15:20:38 -03:00
parent 6a1312d55c
commit fa96ec4aea
11 changed files with 352 additions and 11 deletions

View File

@@ -3,6 +3,8 @@
namespace App\Controllers;
use App\Models\Order;
use App\Models\OrderAttachment;
use App\Services\AttachmentService;
use App\Services\OrderProcessor;
use App\Services\TelegramService;
use App\Utils\View;
@@ -88,26 +90,32 @@ class OrderController
$sql = "INSERT INTO orders (title, type, content, received_at) VALUES (:title, :type, :content, :received_at)";
$stmt = $conn->prepare($sql);
$stmt->execute([
'title' => $title,
'type' => $type,
'content' => $content,
'title' => $title,
'type' => $type,
'content' => $content,
'received_at' => $received_at
]);
$orderId = $conn->lastInsertId();
// Process CSV
// Process CSV (unchanged)
$processor = new OrderProcessor();
$count = $processor->process($orderId, $type, $_FILES['csv_file']['tmp_name']);
// Store attachments (if any were sent)
if (isset($_FILES['attachments']) && !empty($_FILES['attachments']['name'][0])) {
$attachmentService = new AttachmentService();
$attachmentService->storeFiles((int) $orderId, $_FILES['attachments']);
}
// Send Telegram Notification
$telegramService = new TelegramService();
$typeLabel = ($type === 'block') ? 'Bloqueio' : (($type === 'unblock') ? 'Desbloqueio' : $type);
$telegramService->sendOrderNotification([
'id' => $orderId,
'id' => $orderId,
'title' => $title,
'type' => $typeLabel
'type' => $typeLabel
], $count);
$_SESSION['flash_success'] = "Ordem criada com sucesso! $count domínios processados.";
@@ -134,11 +142,47 @@ class OrderController
$stmt->execute(['id' => $id]);
$items = $stmt->fetchAll();
// Get Attachments
$attachmentModel = new OrderAttachment();
$attachments = $attachmentModel->findByOrderId((int) $id);
View::render('layouts.admin', [
'title' => 'Detalhes da Ordem #' . $id,
'content' => __DIR__ . '/../../resources/views/admin/orders/view.php',
'order' => $order,
'items' => $items
'title' => 'Detalhes da Ordem #' . $id,
'content' => __DIR__ . '/../../resources/views/admin/orders/view.php',
'order' => $order,
'items' => $items,
'attachments' => $attachments,
]);
}
public function downloadAttachment($id)
{
$attachmentModel = new OrderAttachment();
$attachment = $attachmentModel->find($id);
if (!$attachment) {
$_SESSION['flash_error'] = "Anexo não encontrado.";
View::redirect('/admin/orders');
return;
}
$attachmentService = new AttachmentService();
$filePath = $attachmentService->getFilePath((int) $attachment['order_id'], $attachment['stored_name']);
if (!file_exists($filePath)) {
$_SESSION['flash_error'] = "Arquivo físico não encontrado no servidor.";
View::redirect('/admin/orders/view/' . $attachment['order_id']);
return;
}
// Serve o arquivo para download
header('Content-Type: ' . ($attachment['mime_type'] ?? 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . addslashes($attachment['original_name']) . '"');
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: private, no-cache');
header('Pragma: no-cache');
readfile($filePath);
exit;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Core\Model;
class OrderAttachment extends Model
{
protected $table = 'order_attachments';
/**
* Retorna todos os anexos de uma ordem específica.
*/
public function findByOrderId(int $orderId): array
{
$stmt = $this->conn->prepare(
"SELECT * FROM {$this->table} WHERE order_id = :order_id ORDER BY created_at ASC"
);
$stmt->execute(['order_id' => $orderId]);
return $stmt->fetchAll();
}
/**
* Insere um registro de anexo no banco.
*/
public function create(array $data): int
{
$stmt = $this->conn->prepare(
"INSERT INTO {$this->table} (order_id, original_name, stored_name, mime_type, size)
VALUES (:order_id, :original_name, :stored_name, :mime_type, :size)"
);
$stmt->execute([
'order_id' => $data['order_id'],
'original_name' => $data['original_name'],
'stored_name' => $data['stored_name'],
'mime_type' => $data['mime_type'] ?? null,
'size' => $data['size'] ?? null,
]);
return (int) $this->conn->lastInsertId();
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Services;
use App\Models\OrderAttachment;
class AttachmentService
{
/**
* Tipos de arquivo permitidos para anexo.
* Mapeamento mime_type => extensões aceitas.
*/
private const ALLOWED_TYPES = [
'application/pdf' => 'pdf',
'image/png' => 'png',
'image/jpeg' => 'jpg',
'image/gif' => 'gif',
'application/msword' => 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.ms-excel' => 'xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'text/plain' => 'txt',
];
/** Tamanho máximo por arquivo: 20 MB */
private const MAX_SIZE = 20 * 1024 * 1024;
/**
* Raiz do diretório de uploads (fora do public/).
*/
private function getBaseUploadDir(): string
{
return dirname(__DIR__, 2) . '/storage/uploads/orders';
}
/**
* Pega o caminho do diretório de upload de uma ordem específica.
*/
private function getOrderUploadDir(int $orderId): string
{
return $this->getBaseUploadDir() . '/' . $orderId;
}
/**
* Garante que o diretório de upload da ordem exista.
*/
private function ensureDirectory(string $path): void
{
if (!is_dir($path)) {
mkdir($path, 0775, true);
}
}
/**
* Processa e armazena os arquivos do campo de upload.
* Retorna a quantidade de arquivos salvos com sucesso.
*
* @param int $orderId
* @param array $filesInput Entry do $_FILES (já normalizado como array de arquivos)
* @return int
* @throws \Exception em caso de erro crítico
*/
public function storeFiles(int $orderId, array $filesInput): int
{
$uploadDir = $this->getOrderUploadDir($orderId);
$this->ensureDirectory($uploadDir);
$model = new OrderAttachment();
$saved = 0;
// Normaliza o $_FILES para iterar arquivo a arquivo
$files = $this->normalizeFilesArray($filesInput);
foreach ($files as $file) {
// Ignora entradas vazias (campo não preenchido)
if ($file['error'] === UPLOAD_ERR_NO_FILE) {
continue;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new \Exception("Erro no upload do arquivo '{$file['name']}': código {$file['error']}");
}
if ($file['size'] > self::MAX_SIZE) {
throw new \Exception("O arquivo '{$file['name']}' excede o tamanho máximo permitido de 20 MB.");
}
// Detecta o tipo MIME real do arquivo (não confiar só no header HTTP)
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (!array_key_exists($mimeType, self::ALLOWED_TYPES)) {
throw new \Exception("Tipo de arquivo não permitido: '{$file['name']}' ({$mimeType}).");
}
$extension = self::ALLOWED_TYPES[$mimeType];
$storedName = uniqid('attach_', true) . '.' . $extension;
$destination = $uploadDir . '/' . $storedName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
throw new \Exception("Falha ao mover o arquivo '{$file['name']}' para o servidor.");
}
$model->create([
'order_id' => $orderId,
'original_name' => $file['name'],
'stored_name' => $storedName,
'mime_type' => $mimeType,
'size' => $file['size'],
]);
$saved++;
}
return $saved;
}
/**
* Retorna o caminho absoluto do arquivo no disco.
*/
public function getFilePath(int $orderId, string $storedName): string
{
return $this->getOrderUploadDir($orderId) . '/' . $storedName;
}
/**
* Normaliza o array $_FILES para uma lista linear de arquivos,
* independente de ser upload único ou múltiplo (name[]).
*/
private function normalizeFilesArray(array $files): array
{
// Se é um único arquivo, encapsula em array
if (!is_array($files['name'])) {
return [$files];
}
$normalized = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$normalized[] = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i],
];
}
return $normalized;
}
}

View File

@@ -22,7 +22,7 @@ class OrderProcessor
$domains = [];
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
$raw = trim($data[0]);
$raw = trim($data[0] ?? '');
if (empty($raw))
continue;

View File

@@ -84,6 +84,9 @@ $router->addMiddleware(\App\Middleware\AdminMiddleware::class);
$router->get('/admin/orders/view/{id}', [\App\Controllers\OrderController::class, 'view']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class);
$router->get('/admin/orders/attachments/{id}/download', [\App\Controllers\OrderController::class, 'downloadAttachment']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class);
// Settings
$router->get('/admin/settings', [\App\Controllers\SettingsController::class, 'index']);
$router->addMiddleware(\App\Middleware\AdminMiddleware::class);