Changes
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions;
|
||||
|
||||
use App\Containers\Article\Models\Category;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\CallAiServiceForFileSelectionTask;
|
||||
use App\Containers\Dashboard\Tasks\CallAiServiceTask;
|
||||
use App\Containers\Dashboard\Tasks\CreatePostFromAiDataTask;
|
||||
use App\Containers\Dashboard\Tasks\ExtractTextFromDocumentTask;
|
||||
use App\Containers\Dashboard\Tasks\ExtractTextFragmentTask;
|
||||
use App\Containers\Dashboard\Tasks\FindMainNewsFileTask;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessMixedFilesAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FindMainNewsFileTask $findMainNewsFileTask,
|
||||
private readonly ExtractTextFromDocumentTask $extractTextFromDocumentTask,
|
||||
private readonly CallAiServiceTask $callAiServiceTask,
|
||||
private readonly CreatePostFromAiDataTask $createPostFromAiDataTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Обрабатывает смешанные файлы и создает пост
|
||||
*
|
||||
* @param Collection<UploadedFile> $files Все загруженные файлы
|
||||
* @return array Данные о созданном посте
|
||||
*/
|
||||
public function run(Collection $files): array
|
||||
{
|
||||
Log::info('[ProcessMixedFilesAction] Начало обработки файлов', [
|
||||
'files_count' => $files->count(),
|
||||
'files' => $files->map(fn($f) => $f->getClientOriginalName())->toArray(),
|
||||
]);
|
||||
|
||||
// Находим основной файл с текстом новости
|
||||
$mainFile = $this->findMainNewsFileTask->run($files);
|
||||
|
||||
if (!$mainFile) {
|
||||
Log::error('[ProcessMixedFilesAction] Не найден файл для извлечения текста');
|
||||
throw new \RuntimeException('Не найден DOC/DOCX файл для извлечения текста');
|
||||
}
|
||||
|
||||
Log::info('[ProcessMixedFilesAction] Основной файл найден', [
|
||||
'file' => $mainFile->getClientOriginalName(),
|
||||
'extension' => $mainFile->getClientOriginalExtension(),
|
||||
]);
|
||||
|
||||
// Извлекаем текст из основного файла (поддерживает и .doc, и .docx)
|
||||
$extractedText = $this->extractTextFromDocumentTask->run($mainFile);
|
||||
|
||||
Log::info('[ProcessMixedFilesAction] Результат извлечения текста', [
|
||||
'text_length' => strlen($extractedText ?? ''),
|
||||
'has_text' => !empty($extractedText),
|
||||
]);
|
||||
|
||||
if (empty($extractedText)) {
|
||||
Log::warning('[ProcessMixedFilesAction] Пустой текст после извлечения', [
|
||||
'file' => $mainFile->getClientOriginalName(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Сохраняем все файлы
|
||||
['documentPath' => $documentPath, 'mediaPaths' => $mediaPaths] = $this->saveFiles($files, $mainFile);
|
||||
|
||||
// Обрабатываем прикреплённые файлы из media
|
||||
$attachedFiles = $this->processAttachedFiles($files, $mainFile);
|
||||
|
||||
// Получаем категории
|
||||
$categories = Category::all();
|
||||
|
||||
// Отправляем текст в AI
|
||||
$newsData = $this->callAiServiceTask->run($extractedText, $categories);
|
||||
|
||||
if (!$newsData) {
|
||||
throw new \RuntimeException('Не удалось распознать данные через AI сервис');
|
||||
}
|
||||
|
||||
// Создаём пост
|
||||
$post = $this->createPostFromAiDataTask->run($newsData, $documentPath, $mediaPaths, $attachedFiles);
|
||||
|
||||
// Возвращаем данные для отображения
|
||||
return $this->prepareResponse($post, $newsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет все файлы
|
||||
*/
|
||||
private function saveFiles(Collection $files, UploadedFile $mainFile): array
|
||||
{
|
||||
// Сохраняем основной документ
|
||||
$documentPath = $mainFile->store('documents', 'local');
|
||||
|
||||
// Сохраняем остальные файлы как медиа
|
||||
$mediaPaths = $files
|
||||
->filter(fn($file) => $file !== $mainFile)
|
||||
->map(fn($file) => $file->store('media', 'public'))
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'documentPath' => $documentPath,
|
||||
'mediaPaths' => $mediaPaths,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает прикреплённые файлы (для добавления в контент)
|
||||
*/
|
||||
private function processAttachedFiles(Collection $files, UploadedFile $mainFile): array
|
||||
{
|
||||
$attachedFiles = [];
|
||||
$fileExtensions = ['doc', 'docx', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar'];
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Пропускаем основной файл
|
||||
if ($file === $mainFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
// Пропускаем изображения
|
||||
if (in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обрабатываем только файлы-вложения
|
||||
if (!in_array($extension, $fileExtensions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$savedPath = $file->store('media/attachments', 'public');
|
||||
|
||||
$attachedFiles[] = [
|
||||
'expansion' => $extension,
|
||||
'size' => $this->formatFileSize($file->getSize()),
|
||||
'time_added' => time(),
|
||||
'title' => $file->getClientOriginalName(),
|
||||
'path' => $savedPath,
|
||||
];
|
||||
}
|
||||
|
||||
return $attachedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает ответ для возврата
|
||||
*/
|
||||
private function prepareResponse(Post $post, array $newsData): array
|
||||
{
|
||||
$post->load('category');
|
||||
|
||||
return [
|
||||
'post' => $post,
|
||||
'newsData' => $newsData,
|
||||
'preview_url' => $post->preview ? asset('storage/' . $post->preview) : null,
|
||||
'images_urls' => $post->images
|
||||
? array_map(fn($img) => asset('storage/' . $img), $post->images)
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует размер файла
|
||||
*/
|
||||
private function formatFileSize(int $bytes): string
|
||||
{
|
||||
$units = ['б', 'КиБ', 'МиБ', 'ГиБ'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions;
|
||||
|
||||
use App\Containers\Article\Models\Category;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Containers\Dashboard\Tasks\CallAiServiceTask;
|
||||
use App\Containers\Dashboard\Tasks\CreatePostFromAiDataTask;
|
||||
use App\Containers\Dashboard\Tasks\ExtractTextFromDocumentTask;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ProcessUploadedFilesAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExtractTextFromDocumentTask $extractTextFromDocumentTask,
|
||||
private readonly CallAiServiceTask $callAiServiceTask,
|
||||
private readonly CreatePostFromAiDataTask $createPostFromAiDataTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Обрабатывает загруженные файлы и создаёт пост
|
||||
*
|
||||
* @param UploadedFile $document Основной документ
|
||||
* @param array $mediaFiles Массив медиафайлов
|
||||
* @return array Данные о созданном посте
|
||||
*/
|
||||
public function run(UploadedFile $document, array $mediaFiles = []): array
|
||||
{
|
||||
// Извлекаем текст из DOCX
|
||||
$extractedText = $this->extractText($document);
|
||||
|
||||
// Сохраняем документ
|
||||
$documentPath = $document->store('documents', 'local');
|
||||
|
||||
// Сохраняем медиафайлы
|
||||
$mediaPaths = $this->saveMediaFiles($mediaFiles);
|
||||
|
||||
// Обрабатываем прикреплённые файлы из media (DOCX, PDF и т.д.)
|
||||
$attachedFiles = $this->processAttachedFiles($mediaFiles);
|
||||
|
||||
// Получаем категории
|
||||
$categories = Category::all();
|
||||
|
||||
// Отправляем текст в AI
|
||||
$newsData = $this->callAiService($extractedText, $categories);
|
||||
|
||||
if (!$newsData) {
|
||||
throw new \RuntimeException('Не удалось распознать данные через AI сервис');
|
||||
}
|
||||
|
||||
// Создаём пост
|
||||
$post = $this->createPost($newsData, $documentPath, $mediaPaths, $attachedFiles);
|
||||
|
||||
// Возвращаем данные для отображения
|
||||
return $this->prepareResponse($post, $newsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает текст из документа
|
||||
*/
|
||||
private function extractText(UploadedFile $document): ?string
|
||||
{
|
||||
return $this->extractTextFromDocumentTask->run($document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет медиафайлы
|
||||
*/
|
||||
private function saveMediaFiles(array $mediaFiles): array
|
||||
{
|
||||
return collect($mediaFiles)
|
||||
->map(fn($file) => $file->store('media', 'public'))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает прикреплённые файлы (для добавления в контент)
|
||||
*/
|
||||
private function processAttachedFiles(array $mediaFiles): array
|
||||
{
|
||||
$attachedFiles = [];
|
||||
$fileExtensions = ['doc', 'docx', 'pdf', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar'];
|
||||
|
||||
foreach ($mediaFiles as $file) {
|
||||
$extension = strtolower($file->getClientOriginalExtension());
|
||||
|
||||
// Пропускаем изображения
|
||||
if (in_array($extension, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обрабатываем только файлы-вложения
|
||||
if (!in_array($extension, $fileExtensions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$savedPath = $file->store('media/attachments', 'public');
|
||||
|
||||
$attachedFiles[] = [
|
||||
'expansion' => $extension,
|
||||
'size' => $this->formatFileSize($file->getSize()),
|
||||
'time_added' => time(),
|
||||
'title' => $file->getClientOriginalName(),
|
||||
'path' => $savedPath,
|
||||
];
|
||||
}
|
||||
|
||||
return $attachedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вызывает AI сервис для распознавания данных
|
||||
*/
|
||||
private function callAiService(?string $text, $categories): ?array
|
||||
{
|
||||
if (!$text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->callAiServiceTask->run($text, $categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Создаёт пост из данных
|
||||
*/
|
||||
private function createPost(array $newsData, ?string $documentPath, array $mediaPaths, array $attachedFiles): Post
|
||||
{
|
||||
return $this->createPostFromAiDataTask->run($newsData, $documentPath, $mediaPaths, $attachedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Подготавливает ответ для возврата
|
||||
*/
|
||||
private function prepareResponse(Post $post, array $newsData): array
|
||||
{
|
||||
$post->load('category');
|
||||
|
||||
return [
|
||||
'post' => $post,
|
||||
'newsData' => $newsData,
|
||||
'preview_url' => $post->preview ? asset('storage/' . $post->preview) : null,
|
||||
'images_urls' => $post->images
|
||||
? array_map(fn($img) => asset('storage/' . $img), $post->images)
|
||||
: [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует размер файла
|
||||
*/
|
||||
private function formatFileSize(int $bytes): string
|
||||
{
|
||||
$units = ['б', 'КиБ', 'МиБ', 'ГиБ'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= (1 << (10 * $pow));
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions;
|
||||
|
||||
use App\Containers\Article\Enums\PostStatus;
|
||||
use App\Containers\Article\Models\Post;
|
||||
use App\Services\Filament\Domain\Posts\PostDataProcessor;
|
||||
use App\Services\Filament\Domain\Posts\PostNotificationService;
|
||||
use App\Services\Filament\Domain\Posts\PostSliderService;
|
||||
use App\Services\Filament\Domain\Posts\VkPostPublisher;
|
||||
use App\Dto\MainSliderDTO;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class PublishPostAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PostDataProcessor $postDataProcessor,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Публикует черновик поста
|
||||
*
|
||||
* @param Post $post Пост для публикации
|
||||
* @return Post Опубликованный пост
|
||||
*/
|
||||
public function run(Post $post): Post
|
||||
{
|
||||
// Обновляем данные поста
|
||||
$updatedData = $this->postDataProcessor->processUpdate([
|
||||
...$post->toArray(),
|
||||
'status' => PostStatus::PUBLISHED,
|
||||
'publish_setting' => [
|
||||
'publish_after' => false,
|
||||
'publish_at' => null,
|
||||
],
|
||||
]);
|
||||
|
||||
$post->update($updatedData);
|
||||
|
||||
// Отправляем уведомления
|
||||
$this->sendNotifications($post);
|
||||
|
||||
// Публикуем в VK
|
||||
$this->publishToVk($post);
|
||||
|
||||
// Обновляем слайд, если есть
|
||||
$this->updateSlide($post);
|
||||
|
||||
return $post->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет уведомления о публикации
|
||||
*/
|
||||
private function sendNotifications(Post $post): void
|
||||
{
|
||||
(new PostNotificationService())->send($post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Публикует пост в VK
|
||||
*/
|
||||
private function publishToVk(Post $post): void
|
||||
{
|
||||
// Публикуем в VK по умолчанию (аналогично Filament, где есть чекбокс)
|
||||
(new VkPostPublisher())->publish(['vk' => true], $post);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет слайд для поста
|
||||
*/
|
||||
private function updateSlide(Post $post): void
|
||||
{
|
||||
// Если у поста уже есть слайд, обновляем его статус
|
||||
if ($post->slide) {
|
||||
$post->slide->update([
|
||||
'is_active' => true,
|
||||
'start_time' => $post->publish_at ?? Carbon::now(),
|
||||
]);
|
||||
}
|
||||
// Если слайда нет, но есть превью - создаём новый
|
||||
elseif ($post->preview) {
|
||||
$slideData = [
|
||||
'is_active' => true,
|
||||
'start_time' => $post->publish_at ?? Carbon::now(),
|
||||
'title' => $post->title,
|
||||
'description' => $post->preview_text,
|
||||
'image' => $post->preview,
|
||||
];
|
||||
|
||||
$sliderDTO = MainSliderDTO::fromArray($slideData);
|
||||
(new PostSliderService($sliderDTO, $post))->create();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user