This commit is contained in:
F4ilji
2026-04-07 17:54:26 +05:00
parent 04adba6682
commit 936b1fb5b0
468 changed files with 43498 additions and 4176 deletions
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
class BulkDeletePostsTask
{
public function run(array $ids): int
{
return Post::whereIn('id', $ids)->delete();
}
}
@@ -0,0 +1,21 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Post;
use Carbon\Carbon;
class BulkUpdatePostStatusTask
{
public function run(array $ids, PostStatus $status, ?Carbon $publishAt = null): int
{
$data = ['status' => $status];
if ($publishAt !== null) {
$data['publish_at'] = $publishAt;
}
return Post::whereIn('id', $ids)->update($data);
}
}
@@ -0,0 +1,148 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\PostDataProcessor;
use Illuminate\Support\Str;
class CreatePostFromAiDataTask
{
/**
* Создаёт пост из данных, полученных от AI
*
* @param array $newsData Данные от AI (title, body, authors, tags, category_id)
* @param string|null $documentPath Путь к основному документу
* @param array $mediaPaths Пути к медиафайлам
* @param array $attachedFiles Прикреплённые файлы (для блока files)
*/
public function run(array $newsData, ?string $documentPath, array $mediaPaths, array $attachedFiles): Post
{
// Формируем контент в формате редактора
$content = $this->buildContent($newsData, $attachedFiles);
// Обрабатываем изображения
['preview' => $previewPath, 'images' => $imagesPaths] = $this->processImages($mediaPaths);
// Подготавливаем данные для создания поста
$postData = $this->preparePostData($newsData, $content, $previewPath, $imagesPaths);
// Обрабатываем через PostDataProcessor (как в админке)
$processedData = (new PostDataProcessor())->processCreate($postData);
// Создаём пост
$post = Post::create($processedData);
// Сохраняем теги
$this->syncTags($post, $newsData);
return $post;
}
/**
* Формирует контент в формате редактора
*/
private function buildContent(array $newsData, array $attachedFiles): array
{
$content = [
[
'type' => 'paragraph',
'data' => [
'seo_active' => true,
'content' => $newsData['body'] ?? '',
],
],
];
if (!empty($attachedFiles)) {
$content[] = [
'type' => 'files',
'data' => [
'file' => $attachedFiles,
],
];
}
return $content;
}
/**
* Обрабатывает изображения: все изображения идут в gallery, первое — preview
*/
private function processImages(array $mediaPaths): array
{
$imagesPaths = [];
$imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp', 'svg', 'tiff'];
foreach ($mediaPaths as $path) {
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($extension, $imageExtensions)) {
$imagesPaths[] = $path;
}
}
return [
'preview' => !empty($imagesPaths) ? reset($imagesPaths) : null,
'images' => $imagesPaths,
];
}
/**
* Подготавливает данные для создания поста
*/
private function preparePostData(array $newsData, array $content, ?string $previewPath, array $imagesPaths): array
{
$baseSlug = Str::slug($newsData['title'] ?? '');
$slug = $this->generateUniqueSlug($baseSlug);
return [
'title' => $newsData['title'] ?? 'Без названия',
'slug' => $slug,
'content' => $content,
'authors' => $newsData['authors'] ?? [],
'category_id' => $newsData['category_id'] ?? null,
'status' => PostStatus::VERIFICATION,
'user_id' => auth()->id(),
'preview' => $previewPath,
'images' => !empty($imagesPaths) ? $imagesPaths : null,
'publish_setting' => [
'publish_after' => false,
'publish_at' => null,
],
'publication' => [],
];
}
/**
* Генерирует уникальный slug
*/
private function generateUniqueSlug(string $baseSlug): string
{
$slug = $baseSlug;
$count = 1;
while (Post::where('slug', $slug)->exists()) {
$slug = $baseSlug . '-' . $count;
$count++;
}
return $slug;
}
/**
* Сохраняет теги поста
*/
private function syncTags(Post $post, array $newsData): void
{
if (!empty($newsData['tags'])) {
$tagNames = array_map(
fn($tag) => str_replace('#', '', $tag),
$newsData['tags']
);
$post->syncTags($tagNames);
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\PostDataProcessor;
use Illuminate\Support\Str;
class CreatePostTask
{
public function __construct(
private readonly PostDataProcessor $postDataProcessor,
) {}
/**
* Создает новый пост в БД
*
* @param array $data Данные поста
* @return Post
*/
public function run(array $data): Post
{
$processedData = $this->postDataProcessor->processCreate($data);
return Post::create($processedData);
}
/**
* Генерирует уникальный slug
*/
public function generateUniqueSlug(string $baseSlug): string
{
$slug = $baseSlug;
$count = 1;
while (Post::where('slug', $slug)->exists()) {
$slug = $baseSlug . '-' . $count;
$count++;
}
return $slug;
}
}
@@ -0,0 +1,13 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
class DeletePostTask
{
public function run(Post $post): bool
{
return $post->delete();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Post;
use Illuminate\Database\Eloquent\Collection;
class GetAiPreparedPostsTask
{
public function run(): Collection
{
return Post::query()
->with(['category', 'author'])
->whereIn('status', [PostStatus::VERIFICATION->value, PostStatus::REJECTED->value])
->orderBy('created_at', 'desc')
->get([
'id',
'title',
'slug',
'preview_text',
'content',
'status',
'authors',
'preview',
'images',
'reading_time',
'category_id',
'created_at',
'updated_at',
]);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Category;
use App\Containers\Widget\Models\Slider;
use Illuminate\Support\Collection;
class GetPostFormDataTask
{
public function run(): array
{
return [
'categories' => Category::all(['id', 'title']),
'sliders' => Slider::where('is_active', true)->get(['id', 'title']),
'statuses' => PostStatus::cases(),
];
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
use App\Dto\MainSliderDTO;
use App\Services\Filament\Domain\Posts\PostSliderService;
use Illuminate\Support\Carbon;
class HandlePostSliderTask
{
/**
* Обрабатывает слайдер для поста (создание/обновление/удаление)
*
* @param Post $post
* @param array $slideData Данные слайдера
* @param bool $isCreate Создание или обновление
* @return void
*/
public function run(Post $post, array $slideData, bool $isCreate = false): void
{
// Если слайдер не передан или slider_id null - ничего не делаем
if (empty($slideData) || empty($slideData['slider_id'])) {
return;
}
// Если нужно удалить слайдер
if (isset($slideData['slider_id']) && $slideData['slider_id'] === null) {
$post->slide?->delete();
return;
}
$data = [
'is_active' => $slideData['is_active'] ?? false,
'start_time' => $post->publish_at ?? Carbon::now(),
'title' => $slideData['title'] ?? $post->title,
'description' => $slideData['content'] ?? $post->preview_text,
'image' => $slideData['image']['url'] ?? null,
'color_theme' => $slideData['color_theme'] ?? '#ffffff',
];
// Добавляем настройки слайда
if (isset($slideData['settings'])) {
$data = array_merge($data, $slideData['settings']);
}
$sliderDTO = MainSliderDTO::fromArray($data);
$sliderService = new PostSliderService($sliderDTO, $post);
// Обновляем или создаем слайдер
if ($post->slide && !$isCreate) {
$sliderService->update();
} else {
$sliderService->create();
}
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ListPostsTask
{
public function run(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$query = Post::with(['category', 'author'])
->orderBy('publish_at', 'desc');
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['search'])) {
$query->where('title', 'like', '%' . $filters['search'] . '%');
}
return $query->paginate($perPage);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\VkPostPublisher;
use Illuminate\Support\Facades\DB;
class PublishPostToVkTask
{
/**
* Публикует пост в VK
*
* @param Post $post
* @param bool $shouldPublish Флаг публикации
* @param bool $isUpdate Флаг обновления (true) или создания (false)
* @return void
*/
public function run(Post $post, bool $shouldPublish, bool $isUpdate = false): void
{
if (!$shouldPublish) {
return;
}
$publisher = new VkPostPublisher();
if ($isUpdate) {
// Проверяем, опубликован ли пост уже в VK
$postRelation = DB::table('posts_vk_posts')
->where('post_id', $post->id)
->first();
if ($postRelation) {
$publisher->update(['vk' => true], $post);
return;
}
}
$publisher->publish(['vk' => true], $post);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Enums\PostStatus;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\PostNotificationService;
class SendPostNotificationTask
{
/**
* Отправляет уведомления о статусе поста
*
* @param Post $post
* @param string|null $newStatus Новый статус поста
* @param bool $isCreation Флаг создания (true) или обновления (false)
* @return void
*/
public function run(Post $post, ?string $newStatus = null, bool $isCreation = false): void
{
$notificationService = new PostNotificationService();
// При создании всегда отправляем уведомление
if ($isCreation) {
$notificationService->send($post);
return;
}
// При обновлении отправляем только при смене статуса
$status = $newStatus ?? $post->status;
if ($status instanceof PostStatus) {
if ($status === PostStatus::PUBLISHED) {
$notificationService->sendSuccessNotification($post);
} elseif ($status === PostStatus::REJECTED) {
$notificationService->sendDeniedNotification($post);
}
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Containers\Dashboard\Tasks\Posts;
use App\Containers\Article\Models\Post;
use App\Services\Filament\Domain\Posts\PostDataProcessor;
class UpdatePostTask
{
public function __construct(
private readonly PostDataProcessor $postDataProcessor,
) {}
/**
* Обновляет существующий пост
*
* @param Post $post Пост для обновления
* @param array $data Новые данные
* @return Post
*/
public function run(Post $post, array $data): Post
{
$processedData = $this->postDataProcessor->processUpdate([
...$post->toArray(),
...$data,
]);
$post->update($processedData);
return $post->fresh();
}
}