From 915b4fe95d172eda0e8192e9fa53cc1ff7cea9a3 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 10 Jan 2026 07:00:36 +0000 Subject: [PATCH] Restore local changes after Git corruption --- app/Http/Controllers/AiController.php | 37 ++ app/Http/Controllers/AuthController.php | 74 +++ app/Http/Controllers/ComponentsController.php | 102 ++-- app/Http/Controllers/PCBuildsController.php | 246 ++++++++++ app/Http/Controllers/UsersController.php | 3 - app/Models/AiTask.php | 26 ++ app/Models/Component.php | 21 +- app/Models/PCBuild.php | 14 +- app/Models/PCBuildComponent.php | 35 ++ app/Models/User.php | 1 + app/Services/AiSuggestorService.php | 436 ++++++++++++++++++ app/Services/BuildValidator.php | 83 ++++ database/factories/UserFactory.php | 1 + ...5_10_29_154421_create_components_table.php | 23 +- ...170250_add_custom_field_to_users_table.php | 2 +- ...64803_create_pc_build_components_table.php | 30 ++ ...026_01_07_071225_create_ai_tasks_table.php | 27 ++ database/seeders/ComponentSeeder.php | 326 +++++++++++++ database/seeders/DatabaseSeeder.php | 2 +- routes/api.php | 39 +- 20 files changed, 1453 insertions(+), 75 deletions(-) create mode 100644 app/Http/Controllers/AiController.php create mode 100644 app/Http/Controllers/AuthController.php create mode 100644 app/Http/Controllers/PCBuildsController.php create mode 100644 app/Models/AiTask.php create mode 100644 app/Models/PCBuildComponent.php create mode 100644 app/Services/AiSuggestorService.php create mode 100644 app/Services/BuildValidator.php create mode 100644 database/migrations/2026_01_07_064803_create_pc_build_components_table.php create mode 100644 database/migrations/2026_01_07_071225_create_ai_tasks_table.php create mode 100644 database/seeders/ComponentSeeder.php diff --git a/app/Http/Controllers/AiController.php b/app/Http/Controllers/AiController.php new file mode 100644 index 0000000..19f64c7 --- /dev/null +++ b/app/Http/Controllers/AiController.php @@ -0,0 +1,37 @@ +validate([ + 'task_id' => 'nullable|integer', + 'custom_prompt' => 'nullable|string', + 'budget' => 'nullable|numeric|min:0', + ]); + + try { + $service = new AiSuggestorService(); + $result = $service->suggest( + $request->input('task_id'), // ✅ Безопасно: вернёт null, если нет + $request->input('custom_prompt'), + $request->input('budget') + ); + + return response()->json([ + 'message' => 'Сборка успешно сгенерирована ИИ.', + 'build' => $result['build']->load('components'), + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'message' => 'Не удалось сгенерировать сборку.', + 'error' => $e->getMessage() + ], 500); + } +} +} \ No newline at end of file diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..0e56944 --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,74 @@ +validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|min:8|confirmed', + 'custom_field' => 'required|string|min:2' + ]); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => Hash::make($validated['password']), + 'custom_field' => $validated['custom_field'], + ]); + + return response()->json([ + 'message' => 'Пользователь зарегистрирован.', + 'user' => $user, + 'token' => $user->createToken('auth_token')->plainTextToken + ], 201); + } + + /** + * Вход пользователя. + */ + public function login(Request $request) + { + $request->validate([ + 'email' => 'required|email', + 'password' => 'required', + ]); + + $user = User::where('email', $request->email)->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['Неверные учётные данные.'], + ]); + } + + return response()->json([ + 'message' => 'Успешный вход.', + 'user' => $user, + 'token' => $user->createToken('auth_token')->plainTextToken + ]); + } + + /** + * Выход (инвалидация токена). + */ + public function logout(Request $request) + { + $request->user()->currentAccessToken()->delete(); + + return response()->json([ + 'message' => 'Вы успешно вышли из системы.' + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ComponentsController.php b/app/Http/Controllers/ComponentsController.php index 700515a..fa2b3f7 100644 --- a/app/Http/Controllers/ComponentsController.php +++ b/app/Http/Controllers/ComponentsController.php @@ -9,9 +9,15 @@ use Illuminate\Http\Response; class ComponentsController extends Controller { - public function index(){ - return response()->json(Component::all()->toJson()); - } +public function index() +{ + $components = Component::with('user', 'componentType') + ->where('is_official', true) + ->orWhere('created_by_user_id', auth()->id()) + ->get(); + + return response()->json($components); +} @@ -26,49 +32,73 @@ class ComponentsController extends Controller return response()->json($component); } - public function create(Request $request) - { - $name = $request->get(key:'name'); - $type = $request->get(key:'type'); - $brand = $request->get(key:'brand'); - $model = $request->get(key:'model'); - $price = $request->get(key:'price'); +public function store(Request $request) +{ + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric|min:0', + 'component_type_id' => 'required|exists:component_types,id', + 'specifications' => 'nullable|array', + ]); - $component = new Component(); - $component->name = $name; - $component->type = $type; - $component->brand = $brand; - $component->model = $model; - $component->price = $price; - - $component->save(); + $component = Component::create([ + 'name' => $validated['name'], + 'price' => $validated['price'], + 'component_type_id' => $validated['component_type_id'], + 'specifications' => $validated['specifications'] ?? null, + 'is_official' => false, // всегда false для пользователя + 'created_by_user_id' => auth()->id(), // автоматически привязываем к пользователю + ]); + return response()->json([ + 'message' => 'Компонент успешно создан.', + 'component' => $component + ], 201); +} +public function update(Request $request, $id) +{ + $component = Component::findOrFail($id); - - return response()->json($component->toJson()); + // Проверяем, что компонент принадлежит пользователю и не официальный + if ($component->created_by_user_id !== auth()->id() || $component->is_official) { + return response()->json([ + 'message' => 'Вы не можете редактировать этот компонент.' + ], 403); } - public function update(Request $request, int $id): JsonResponse{ + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'price' => 'required|numeric|min:0', + 'component_type_id' => 'required|exists:component_types,id', + 'specifications' => 'nullable|array', + ]); - return response()->json([ - 'name' => $request->get('name'), - 'type' => $request->get('type'), - 'brand' => $request->get('brand'), - 'model' => $request->get('model'), - 'price' => $request->get('price'), - ], Response::HTTP_ACCEPTED); - - } + $component->update($validated); - public function destroy(int $id): JsonResponse - { - // мы бы здесь написали вызов запроса delete из БД + return response()->json([ + 'message' => 'Компонент обновлён.', + 'component' => $component + ]); +} + +public function destroy($id) +{ + $component = Component::findOrFail($id); + + // Проверяем, что компонент принадлежит пользователю и не официальный + if ($component->created_by_user_id !== auth()->id() || $component->is_official) { return response()->json([ - 'success' => true, - ], Response::HTTP_ACCEPTED); + 'message' => 'Вы не можете удалить этот компонент.' + ], 403); } - + + $component->delete(); + + return response()->json([ + 'message' => 'Компонент удалён.' + ]); +} diff --git a/app/Http/Controllers/PCBuildsController.php b/app/Http/Controllers/PCBuildsController.php new file mode 100644 index 0000000..61d26ca --- /dev/null +++ b/app/Http/Controllers/PCBuildsController.php @@ -0,0 +1,246 @@ +id()) + ->with('components') + ->get(); + + return response()->json($builds); + } + + /** + * @OA\Post( + * path="/api/builds", + * summary="Создать новую сборку", + * tags={"PC Builds"}, + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"name", "component_ids"}, + * @OA\Property(property="name", type="string", example="Игровой ПК 2025"), + * @OA\Property(property="description", type="string", nullable=true, example="Для игр в 1440p"), + * @OA\Property(property="component_ids", type="array", @OA\Items(type="integer", example=1)), + * @OA\Property(property="is_ai_generated", type="boolean", default=false), + * @OA\Property(property="ai_prompt", type="string", nullable=true, example="Сборка до 1000$ для игр") + * ) + * ), + * @OA\Response(response=201, description="Сборка создана"), + * @OA\Response(response=400, description="Ошибка валидации"), + * @OA\Response(response=403, description="Запрещено: чужие компоненты") + * ) + */ + public function store(Request $request) +{ + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'component_ids' => 'required|array|min:1', + 'component_ids.*' => 'exists:components,id', + 'is_ai_generated' => 'nullable|boolean', + 'ai_prompt' => 'nullable|string', + ]); + + // Проверяем: все компоненты — либо официальные, либо ваши + $invalidComponents = Component::whereIn('id', $validated['component_ids']) + ->where(function ($query) { + $query->where('is_official', false) + ->where('created_by_user_id', '!=', auth()->id()); + }) + ->pluck('name', 'id') + ->toArray(); + + if (!empty($invalidComponents)) { + return response()->json([ + 'message' => 'Запрещено использовать неофициальные компоненты, созданные другими пользователями.', + 'invalid_components' => $invalidComponents + ], 403); + } + + // 👇 ВСТАВЛЯЕМ ПРОВЕРКУ СОВМЕСТИМОСТИ ЗДЕСЬ — ПЕРЕД СОЗДАНИЕМ СБОРКИ + $validator = new BuildValidator(); + $compatibility = $validator->validateCompatibility($validated['component_ids']); + + if (!$compatibility['valid']) { + return response()->json([ + 'message' => 'Сборка содержит несовместимые компоненты.', + 'errors' => $compatibility['errors'], + 'warnings' => $compatibility['warnings'] + ], 422); // 422 Unprocessable Entity + } + + // ✅ Только если совместимость OK — создаём сборку + $build = PCBuild::create([ + 'user_id' => auth()->id(), + 'name' => $validated['name'], + 'description' => $validated['description'] ?? null, + 'is_ai_generated' => $validated['is_ai_generated'] ?? false, + 'ai_prompt' => $validated['ai_prompt'] ?? null, + ]); + + $build->components()->attach($validated['component_ids']); + + return response()->json([ + 'message' => 'Сборка успешно создана.', + 'build' => $build->load('components'), + 'compatibility' => $compatibility // опционально — для отладки + ], 201); +} + + /** + * @OA\Get( + * path="/api/builds/{id}", + * summary="Получить одну сборку по ID", + * tags={"PC Builds"}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * @OA\Response(response=200, description="Сборка найдена"), + * @OA\Response(response=403, description="Запрещено: не ваша сборка"), + * @OA\Response(response=404, description="Не найдено") + * ) + */ + public function show($id) + { + $build = PCBuild::with('components')->findOrFail($id); + + if ($build->user_id !== auth()->id()) { + return response()->json([ + 'message' => 'Вы не можете просматривать эту сборку.' + ], 403); + } + + return response()->json($build); + } + + /** + * @OA\Put( + * path="/api/builds/{id}", + * summary="Обновить сборку", + * tags={"PC Builds"}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * required={"name", "component_ids"}, + * @OA\Property(property="name", type="string"), + * @OA\Property(property="description", type="string", nullable=true), + * @OA\Property(property="component_ids", type="array", @OA\Items(type="integer")), + * @OA\Property(property="is_ai_generated", type="boolean"), + * @OA\Property(property="ai_prompt", type="string", nullable=true) + * ) + * ), + * @OA\Response(response=200, description="Сборка обновлена"), + * @OA\Response(response=403, description="Запрещено") + * ) + */ + public function update(Request $request, $id) +{ + $build = PCBuild::findOrFail($id); + + if ($build->user_id !== auth()->id()) { + return response()->json([ + 'message' => 'Вы не можете редактировать эту сборку.' + ], 403); + } + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'component_ids' => 'required|array|min:1', + 'component_ids.*' => 'exists:components,id', + 'is_ai_generated' => 'nullable|boolean', + 'ai_prompt' => 'nullable|string', + ]); + + // Проверка: все компоненты — либо официальные, либо ваши + $invalidComponents = Component::whereIn('id', $validated['component_ids']) + ->where(function ($query) { + $query->where('is_official', false) + ->where('created_by_user_id', '!=', auth()->id()); + }) + ->pluck('name', 'id') + ->toArray(); + + if (!empty($invalidComponents)) { + return response()->json([ + 'message' => 'Запрещено использовать неофициальные компоненты, созданные другими пользователями.', + 'invalid_components' => $invalidComponents + ], 403); + } + + // 👇 ВСТАВЛЯЕМ ПРОВЕРКУ СОВМЕСТИМОСТИ ЗДЕСЬ — ПЕРЕД ОБНОВЛЕНИЕМ + $validator = new BuildValidator(); + $compatibility = $validator->validateCompatibility($validated['component_ids']); + + if (!$compatibility['valid']) { + return response()->json([ + 'message' => 'Сборка содержит несовместимые компоненты.', + 'errors' => $compatibility['errors'], + 'warnings' => $compatibility['warnings'] + ], 422); + } + + // ✅ Обновляем сборку + DB::transaction(function () use ($build, $validated) { + $build->update([ + 'name' => $validated['name'], + 'description' => $validated['description'] ?? null, + 'is_ai_generated' => $validated['is_ai_generated'] ?? $build->is_ai_generated, + 'ai_prompt' => $validated['ai_prompt'] ?? $build->ai_prompt, + ]); + + $build->components()->sync($validated['component_ids']); + }); + + return response()->json([ + 'message' => 'Сборка обновлена.', + 'build' => $build->load('components'), + 'compatibility' => $compatibility // опционально + ]); +} + + /** + * @OA\Delete( + * path="/api/builds/{id}", + * summary="Удалить сборку", + * tags={"PC Builds"}, + * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * @OA\Response(response=200, description="Сборка удалена"), + * @OA\Response(response=403, description="Запрещено") + * ) + */ + public function destroy($id) + { + $build = PCBuild::findOrFail($id); + + if ($build->user_id !== auth()->id()) { + return response()->json([ + 'message' => 'Вы не можете удалить эту сборку.' + ], 403); + } + + $build->delete(); + + return response()->json([ + 'message' => 'Сборка удалена.' + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/UsersController.php b/app/Http/Controllers/UsersController.php index e6e3e72..5644c0f 100644 --- a/app/Http/Controllers/UsersController.php +++ b/app/Http/Controllers/UsersController.php @@ -33,8 +33,5 @@ class UsersController extends Controller return ['token' => $user->createToken('frontend')->plainTextToken]; - - - } } diff --git a/app/Models/AiTask.php b/app/Models/AiTask.php new file mode 100644 index 0000000..a9f6bc5 --- /dev/null +++ b/app/Models/AiTask.php @@ -0,0 +1,26 @@ + 'decimal:2', + 'budget_max' => 'decimal:2', + 'is_active' => 'boolean' + ]; +} \ No newline at end of file diff --git a/app/Models/Component.php b/app/Models/Component.php index c08b33a..a0311d6 100644 --- a/app/Models/Component.php +++ b/app/Models/Component.php @@ -11,29 +11,24 @@ class Component extends Model protected $fillable = [ 'name', - 'component_type_id', 'price', + 'component_type_id', 'specifications', 'is_official', 'created_by_user_id', ]; protected $casts = [ - 'specifications' => 'array', // Автоматически преобразует JSON в массив - 'is_official' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'specifications' => 'array', // автоматически преобразует JSON в массив ]; - // Связь с типом компонента - public function type() - { - return $this->belongsTo(ComponentType::class, 'component_type_id'); - } - - // Связь с пользователем (если добавил пользователь) - public function createdBy() + public function user() { return $this->belongsTo(User::class, 'created_by_user_id'); } + + public function componentType() + { + return $this->belongsTo(ComponentType::class, 'component_type_id'); + } } \ No newline at end of file diff --git a/app/Models/PCBuild.php b/app/Models/PCBuild.php index abfce54..de41444 100644 --- a/app/Models/PCBuild.php +++ b/app/Models/PCBuild.php @@ -10,20 +10,20 @@ class PcBuild extends Model use HasFactory; protected $fillable = [ - 'user_id', 'name', 'description', + 'user_id', 'is_ai_generated', 'ai_prompt', ]; - protected $casts = [ - 'is_ai_generated' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; + // 👇 Связь "многие-ко-многим" с компонентами + public function components() + { + return $this->belongsToMany(Component::class, 'pc_build_components'); + } - // Связь с пользователем + // Если хотите, можно добавить обратную связь (опционально) public function user() { return $this->belongsTo(User::class); diff --git a/app/Models/PCBuildComponent.php b/app/Models/PCBuildComponent.php new file mode 100644 index 0000000..da43944 --- /dev/null +++ b/app/Models/PCBuildComponent.php @@ -0,0 +1,35 @@ + 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + // Связь с сборкой + public function build() + { + return $this->belongsTo(PcBuild::class); + } + + // Связь с компонентом + public function component() + { + return $this->belongsTo(Component::class); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index d0ad225..2c5b5a0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,6 +24,7 @@ class User extends Authenticatable 'name', 'email', 'password', + 'custom_field' ]; /** diff --git a/app/Services/AiSuggestorService.php b/app/Services/AiSuggestorService.php new file mode 100644 index 0000000..b9db5aa --- /dev/null +++ b/app/Services/AiSuggestorService.php @@ -0,0 +1,436 @@ +apiToken = env('GIGACHAT_API_TOKEN'); + if (!$this->apiToken) { + throw new Exception('GIGACHAT_API_TOKEN не установлен в .env'); + } + } + + public function suggest($task_id = null, $custom_prompt = null, $budget = null) + { + $prompt = $this->generatePrompt($task_id, $custom_prompt, $budget); + + $response = Http::withHeaders([ + 'Authorization' => 'Bearer ' . $this->apiToken, + 'Content-Type' => 'application/json', + ])->withoutVerifying() + ->post($this->baseUrl, [ + 'model' => 'GigaChat', + 'messages' => [ + [ + 'role' => 'user', + 'content' => $prompt + ] + ], + 'temperature' => 0.7, + 'max_tokens' => 2500, + ]); + + if ($response->failed()) { + Log::error('Ошибка при запросе к ГигаЧату', [ + 'status' => $response->status(), + 'body' => $response->body() + ]); + throw new Exception('Не удалось получить ответ от ИИ.'); + } + + $responseData = $response->json(); + $aiComponents = $this->parseResponse($responseData); + + // 🔹 ДЕБАГ: логируем полученные компоненты от ИИ + Log::info('Компоненты от ИИ:', [ + 'count' => count($aiComponents), + 'types' => array_column($aiComponents, 'component_type_id'), + 'components' => $aiComponents + ]); + + // 🔹 ШАГ 1: Удаляем дубликаты по name + component_type_id + $uniqueComponents = []; + $seen = []; + + foreach ($aiComponents as $component) { + $key = $component['name'] . '|' . $component['component_type_id']; + if (!isset($seen[$key])) { + $seen[$key] = true; + $uniqueComponents[] = $component; + } + } + + $aiComponents = $uniqueComponents; + + // 🔹 ШАГ 2: Гарантируем наличие всех 7 типов компонентов + $requiredTypes = [1, 2, 3, 4, 5, 6, 7]; + $existingTypes = array_column($aiComponents, 'component_type_id'); + + // ДЕБАГ: какие типы уже есть + Log::info('Проверка типов компонентов:', [ + 'required' => $requiredTypes, + 'existing' => $existingTypes, + 'missing' => array_diff($requiredTypes, $existingTypes) + ]); + + foreach ($requiredTypes as $typeId) { + if (!in_array($typeId, $existingTypes)) { + Log::warning('Добавляем недостающий тип компонента:', ['type_id' => $typeId]); + + switch ($typeId) { + case 1: + $aiComponents[] = [ + 'name' => 'AMD Ryzen 5 5600', + 'price' => 15000, + 'component_type_id' => 1, + 'specifications' => ['socket' => 'AM4', 'cores' => 6] + ]; + break; + case 2: + $aiComponents[] = [ + 'name' => 'NVIDIA RTX 4060', + 'price' => 30000, + 'component_type_id' => 2, + 'specifications' => [] + ]; + break; + case 3: + $aiComponents[] = [ + 'name' => 'ASUS TUF GAMING B660M-PLUS', + 'price' => 8000, + 'component_type_id' => 3, + 'specifications' => ['chipset' => 'B660'] + ]; + break; + case 4: + $aiComponents[] = [ + 'name' => 'Kingston FURY Beast DDR4 16GB', + 'price' => 5000, + 'component_type_id' => 4, + 'specifications' => ['type' => 'DDR4', 'capacity' => '16GB'] + ]; + break; + case 5: + $aiComponents[] = [ + 'name' => 'EVGA SuperNOVA 750 G2', + 'price' => 8000, + 'component_type_id' => 5, + 'specifications' => ['power' => 750] + ]; + break; + case 6: + $aiComponents[] = [ + 'name' => 'Crucial P3 500GB NVMe', + 'price' => 4000, + 'component_type_id' => 6, + 'specifications' => ['capacity' => 500, 'interface' => 'NVMe'] + ]; + break; + case 7: + $aiComponents[] = [ + 'name' => 'Cooler Master H500P ATX Mid Tower Case', + 'price' => 4000, + 'component_type_id' => 7, + 'specifications' => ['form_factor' => 'ATX'] + ]; + break; + } + } + } + + // ДЕБАГ: проверяем итоговый набор компонентов + Log::info('Итоговые компоненты перед созданием сборки:', [ + 'total_count' => count($aiComponents), + 'types_present' => array_column($aiComponents, 'component_type_id'), + 'unique_types_count' => count(array_unique(array_column($aiComponents, 'component_type_id'))) + ]); + + // Проверка бюджета + if ($budget) { + $totalPrice = array_sum(array_column($aiComponents, 'price')); + + if ($totalPrice > $budget) { + // Удаляем возможные дубликаты перед заменой + $uniqueComponents = []; + $seen = []; + + foreach ($aiComponents as $component) { + $key = $component['name'] . '|' . $component['component_type_id']; + if (!isset($seen[$key])) { + $seen[$key] = true; + $uniqueComponents[] = $component; + } + } + + $aiComponents = $uniqueComponents; + + // Заменяем дорогие компоненты + foreach ($aiComponents as &$component) { + if ($component['component_type_id'] == 4) { + $component = [ + 'name' => 'Kingston FURY Beast DDR4 16GB', + 'price' => 5000, + 'component_type_id' => 4, + 'specifications' => ['type' => 'DDR4', 'capacity' => '16GB'] + ]; + } + if ($component['component_type_id'] == 6) { + $component = [ + 'name' => 'Crucial P3 500GB NVMe', + 'price' => 4000, + 'component_type_id' => 6, + 'specifications' => ['capacity' => 500, 'interface' => 'NVMe'] + ]; + } + if ($component['component_type_id'] == 1 && $totalPrice > $budget) { + $component = [ + 'name' => 'AMD Ryzen 5 5600', + 'price' => 15000, + 'component_type_id' => 1, + 'specifications' => ['socket' => 'AM4', 'cores' => 6] + ]; + } + } + + $totalPrice = array_sum(array_column($aiComponents, 'price')); + if ($totalPrice > $budget) { + throw new Exception("ИИ не смог уложиться в бюджет {$budget}₽ (итого: {$totalPrice}₽)."); + } + } + } + + // Создаём сборку + $build = PCBuild::create([ + 'user_id' => auth()->id(), + 'name' => 'Сборка от ИИ', + 'description' => "Сгенерировано ИИ на основе: " . ($custom_prompt ?: "задача #{$task_id}"), + 'is_ai_generated' => true, + 'ai_prompt' => $prompt, + ]); + + // Привязываем компоненты с защитой от дубликатов + $createdComponents = []; + $seenComponentIds = []; + + foreach ($aiComponents as $componentData) { + $component = $this->findOrCreateComponent($componentData); + + // Проверяем, что компонент ещё не добавлен в эту сборку + if (!in_array($component->id, $seenComponentIds)) { + $createdComponents[] = $component; + $seenComponentIds[] = $component->id; + } + } + + // Используем sync без детачей, чтобы избежать дубликатов + $build->components()->sync($seenComponentIds, false); + + // 🔹 ПРОВЕРКА: убеждаемся, что есть все 7 типов компонентов + $finalComponents = $build->components()->get(); + $finalTypes = $finalComponents->pluck('component_type_id')->unique()->toArray(); + + Log::info('Проверка финальной сборки:', [ + 'build_id' => $build->id, + 'total_components' => $finalComponents->count(), + 'component_types' => $finalTypes, + 'missing_types' => array_diff($requiredTypes, $finalTypes) + ]); + + if (!in_array(7, $finalTypes)) { + // Находим или создаем корпус по умолчанию + $defaultCase = Component::firstOrCreate( + [ + 'name' => 'Cooler Master H500P ATX Mid Tower Case', + 'component_type_id' => 7, + ], + [ + 'price' => 4000, + 'specifications' => ['form_factor' => 'ATX'], + 'is_official' => true, + 'created_by_user_id' => null, + ] + ); + + $build->components()->attach($defaultCase->id); + Log::warning('Был добавлен корпус по умолчанию для сборки', [ + 'build_id' => $build->id, + 'case_id' => $defaultCase->id + ]); + + // Обновляем массив созданных компонентов + $createdComponents[] = $defaultCase; + } + + // 🔹 ФИНАЛЬНАЯ ПРОВЕРКА + $finalCount = $build->components()->count(); + if ($finalCount < 7) { + Log::error('Сборка все еще имеет недостающее количество компонентов:', [ + 'build_id' => $build->id, + 'expected' => 7, + 'actual' => $finalCount + ]); + } + + return [ + 'build' => $build, + 'components' => collect($createdComponents), + ]; + } + + protected function generatePrompt($task_id, $custom_prompt, $budget) + { + $basePrompt = "Ты — эксперт по сборке ПК. Твоя задача — предложить **полную и сбалансированную сборку из 7 компонентов**, подходящую под запрос пользователя."; + + if ($budget) { + $basePrompt .= " Бюджет: {$budget} рублей"; + } + + if ($custom_prompt) { + $basePrompt .= " Запрос пользователя: '{$custom_prompt}'."; + } elseif ($task_id) { + $basePrompt .= " Задача: #{$task_id}."; + } + + $basePrompt .= " Ты **обязан** включить **ровно по одному компоненту каждого типа**: Процессор, Видеокарта, Материнская плата, ОЗУ, Блок питания, SSD, Корпус."; + $basePrompt .= " Используй ТОЛЬКО эти ID типов: 1=Процессор, 2=Видеокарта, 3=Материнская плата, 4=ОЗУ, 5=Блок питания, 6=SSD, 7=Корпус."; + $basePrompt .= " Не пропускай ни один тип. Не дублируй компоненты."; + $basePrompt .= " Все компоненты должны быть реальными, совместимыми и актуальными на 2025 год."; + $basePrompt .= " Ты **обязан** подобрать компоненты так, чтобы **общая стоимость не превышала** {$budget} рублей."; + $basePrompt .= " Никогда не пиши значения вроде 5600MHz — всегда используй строки: \"5600MHz\" или числа: 5600."; + $basePrompt .= " Не ставь лишние кавычки перед скобками."; + $basePrompt .= " Верни **только чистый JSON-массив из 7 объектов**, без пояснений, комментариев, маркдауна."; + $basePrompt .= " Формат: [{\"name\":\"Название\",\"price\":999.99,\"component_type_id\":1,\"specifications\":{\"socket\":\"AM5\",\"tdp\":105}}, ...]"; + $basePrompt .= " В specifications используй только строки или числа. Например: \"max_power\": 800, а не 800_watt."; + + return $basePrompt; + } + + protected function parseResponse($responseData) + { + if (!isset($responseData['choices'][0]['message']['content'])) { + throw new Exception('Ответ ИИ не содержит содержимого.'); + } + + $content = $responseData['choices'][0]['message']['content']; + + // ДЕБАГ: сохраняем оригинальный ответ + Log::debug('Оригинальный ответ от ИИ:', ['content' => substr($content, 0, 500) . '...']); + + $content = preg_replace('/```json\s*|\s*```/', '', $content); + + if (!preg_match('/\[[\s\S]*\]/', $content, $matches)) { + // Попробуем найти JSON другим способом + if (preg_match('/\{.*\}/s', $content, $matches)) { + // Возможно, ИИ вернул объект вместо массива + $content = '[' . $matches[0] . ']'; + } else { + throw new Exception('Не найден JSON-массив в ответе ИИ.'); + } + } else { + $content = $matches[0]; + } + + $jsonStr = $content; + + // 🔧 Основные исправления JSON + $jsonStr = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $jsonStr); + $jsonStr = preg_replace('/"([^"]+)"(?=\s*{)/', '"$1":', $jsonStr); + $jsonStr = preg_replace('/(? $jsonStr]); + + try { + $parsed = json_decode($jsonStr, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + Log::error('Ошибка парсинга JSON:', [ + 'original' => $content, + 'fixed' => $jsonStr, + 'error' => $e->getMessage() + ]); + throw new Exception('ИИ вернул некорректный JSON: ' . $e->getMessage()); + } + + if (!is_array($parsed)) { + throw new Exception('Ожидался массив компонентов.'); + } + + foreach ($parsed as $index => $item) { + if (!is_array($item)) { + throw new Exception("Элемент #{$index} не является объектом."); + } + } + + // Проверяем количество компонентов после парсинга + Log::info('Компоненты после парсинга:', [ + 'count' => count($parsed), + 'component_types' => array_column($parsed, 'component_type_id') + ]); + + return $parsed; + } + + protected function findOrCreateComponent($data) + { + $data['price'] = (float) ($data['price'] ?? 0); + $data['component_type_id'] = (int) ($data['component_type_id'] ?? 1); + + // Убедимся, что specifications всегда массив + $specifications = is_array($data['specifications'] ?? null) + ? $data['specifications'] + : []; + + // Ищем существующий компонент по имени и типу + $component = Component::where('name', $data['name']) + ->where('component_type_id', $data['component_type_id']) + ->first(); + + if (!$component) { + // Создаем новый компонент + $component = Component::create([ + 'name' => $data['name'], + 'component_type_id' => $data['component_type_id'], + 'price' => $data['price'], + 'specifications' => $specifications, + 'is_official' => true, + 'created_by_user_id' => null, + ]); + } else { + // Обновляем цену и спецификации существующего компонента + $component->update([ + 'price' => $data['price'], + 'specifications' => array_merge($component->specifications ?? [], $specifications), + ]); + } + + return $component; + } +} \ No newline at end of file diff --git a/app/Services/BuildValidator.php b/app/Services/BuildValidator.php new file mode 100644 index 0000000..a3d54fd --- /dev/null +++ b/app/Services/BuildValidator.php @@ -0,0 +1,83 @@ + bool, 'errors' => array, 'warnings' => array] + */ + public function validateCompatibility(array $componentIds) + { + $components = Component::whereIn('id', $componentIds)->get(); + + if ($components->count() < 2) { + return [ + 'valid' => true, + 'errors' => [], + 'warnings' => [] + ]; + } + + $errors = []; + $warnings = []; + + // Определяем типы компонентов по component_type_id (адаптируйте под вашу логику!) + $cpu = $components->firstWhere('component_type_id', 1); // 1 = CPU + $motherboard = $components->firstWhere('component_type_id', 3); // 3 = Motherboard + $ram = $components->firstWhere('component_type_id', 4); // 4 = RAM + $psu = $components->firstWhere('component_type_id', 5); // 5 = PSU + + // 1. Проверка сокета (CPU ↔ Motherboard) + if ($cpu && $motherboard) { + $cpuSocket = $cpu->specifications['socket'] ?? null; + $mbSocket = $motherboard->specifications['socket'] ?? null; + + if (!$cpuSocket || !$mbSocket) { + $warnings[] = "Не указан сокет для процессора или материнской платы."; + } elseif ($cpuSocket !== $mbSocket) { + $errors[] = "Сокет процессора '{$cpuSocket}' не совместим со сокетом материнской платы '{$mbSocket}'."; + } + } + + // 2. Проверка типа ОЗУ (RAM ↔ Motherboard) + if ($ram && $motherboard) { + $ramType = $ram->specifications['type'] ?? null; + $mbRamType = $motherboard->specifications['memory_type'] ?? $motherboard->specifications['type'] ?? null; + + if (!$ramType || !$mbRamType) { + $warnings[] = "Не указан тип памяти для ОЗУ или материнской платы."; + } elseif ($ramType !== $mbRamType) { + $errors[] = "Тип ОЗУ '{$ramType}' не совместим с типом памяти материнской платы '{$mbRamType}'."; + } + } + + // 3. Проверка мощности БП (PSU ≥ сумма TDP) + if ($psu) { + $totalTdp = 0; + + foreach ($components as $component) { + // Берём TDP из specifications, или 0 если нет + $tdp = $component->specifications['tdp'] ?? 0; + $totalTdp += (int) $tdp; + } + + $psuWattage = $psu->specifications['wattage'] ?? 0; + + if ($psuWattage < $totalTdp) { + $errors[] = "Мощность БП ({$psuWattage} Вт) < суммарного TDP ({$totalTdp} Вт). Рекомендуется ≥ {$totalTdp} Вт."; + } + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors, + 'warnings' => $warnings + ]; + } +} \ No newline at end of file diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..5fb92ea 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,6 +29,7 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'custom_field' => 'user', ]; } diff --git a/database/migrations/2025_10_29_154421_create_components_table.php b/database/migrations/2025_10_29_154421_create_components_table.php index 8dc9a76..ac06065 100644 --- a/database/migrations/2025_10_29_154421_create_components_table.php +++ b/database/migrations/2025_10_29_154421_create_components_table.php @@ -11,16 +11,19 @@ return new class extends Migration */ public function up(): void { - Schema::create('components', function (Blueprint $table) { - $table->id(); - $table->string('name'); // Например: "Intel Core i5-12400F" - $table->foreignId('component_type_id')->constrained()->onDelete('cascade'); - $table->decimal('price', 10, 2); - $table->json('specifications')->nullable(); // Для хранения характеристик - $table->boolean('is_official')->default(true); // true = админ, false = пользователь - $table->foreignId('created_by_user_id')->nullable()->constrained('users')->onDelete('set null'); - $table->timestamps(); - }); + Schema::create('components', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->decimal('price', 10, 2); + $table->unsignedBigInteger('component_type_id'); // ссылка на тип компонента + $table->json('specifications')->nullable(); // JSON-поле для характеристик + $table->boolean('is_official')->default(false); // официальный или нет + $table->unsignedBigInteger('created_by_user_id')->nullable(); // кто создал + $table->timestamps(); + + $table->foreign('component_type_id')->references('id')->on('component_types'); + $table->foreign('created_by_user_id')->references('id')->on('users')->onDelete('set null'); +}); } diff --git a/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php b/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php index c5d2643..7035542 100644 --- a/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php +++ b/database/migrations/2025_10_29_170250_add_custom_field_to_users_table.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::table('users', function (Blueprint $table) { - $table->text('custom_field'); + $table->text('custom_field')->nullable()->default(null); // }); } diff --git a/database/migrations/2026_01_07_064803_create_pc_build_components_table.php b/database/migrations/2026_01_07_064803_create_pc_build_components_table.php new file mode 100644 index 0000000..5204988 --- /dev/null +++ b/database/migrations/2026_01_07_064803_create_pc_build_components_table.php @@ -0,0 +1,30 @@ +id(); + $table->unsignedBigInteger('pc_build_id'); + $table->unsignedBigInteger('component_id'); + $table->timestamps(); + + // Внешние ключи + $table->foreign('pc_build_id')->references('id')->on('pc_builds')->onDelete('cascade'); + $table->foreign('component_id')->references('id')->on('components')->onDelete('cascade'); + + // Уникальность пары (build + component), если нужно + $table->unique(['pc_build_id', 'component_id']); + }); + } + + public function down() + { + Schema::dropIfExists('pc_build_components'); + } +} \ No newline at end of file diff --git a/database/migrations/2026_01_07_071225_create_ai_tasks_table.php b/database/migrations/2026_01_07_071225_create_ai_tasks_table.php new file mode 100644 index 0000000..20aafb3 --- /dev/null +++ b/database/migrations/2026_01_07_071225_create_ai_tasks_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('title'); + $table->text('description'); + $table->text('ai_prompt_template'); + $table->decimal('budget_min', 10, 2)->nullable(); + $table->decimal('budget_max', 10, 2)->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('ai_tasks'); + } +}; \ No newline at end of file diff --git a/database/seeders/ComponentSeeder.php b/database/seeders/ComponentSeeder.php new file mode 100644 index 0000000..61be1e8 --- /dev/null +++ b/database/seeders/ComponentSeeder.php @@ -0,0 +1,326 @@ +delete(); + DB::table('ai_tasks')->delete(); + + // 2. Теперь можно удалить родительскую таблицу + DB::table('component_types')->delete(); + + // 3. Вставляем типы с фиксированными ID + $componentTypes = [ + ['id' => 1, 'name' => 'Процессор', 'code' => 'cpu'], + ['id' => 2, 'name' => 'Видеокарта', 'code' => 'gpu'], + ['id' => 3, 'name' => 'Материнская плата', 'code' => 'motherboard'], + ['id' => 4, 'name' => 'ОЗУ', 'code' => 'ram'], + ['id' => 5, 'name' => 'Блок питания', 'code' => 'psu'], + ['id' => 6, 'name' => 'SSD', 'code' => 'ssd'], + ['id' => 7, 'name' => 'Корпус', 'code' => 'case'], + ['id' => 8, 'name' => 'Охлаждение', 'code' => 'cooling'], + ['id' => 9, 'name' => 'Сеть', 'code' => 'network'], + ['id' => 10, 'name' => 'Звуковая карта', 'code' => 'sound'] + ]; + + DB::table('component_types')->insert($componentTypes); + // 2. Добавляем 25 официальных компонентов + $components = [ + // Процессоры (ID=1) + [ + 'name' => 'AMD Ryzen 5 5600', + 'price' => 15000, + 'component_type_id' => 1, + 'specifications' => json_encode(['socket' => 'AM4', 'cores' => 6, 'tdp' => 65]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'AMD Ryzen 7 7700X', + 'price' => 25000, + 'component_type_id' => 1, + 'specifications' => json_encode(['socket' => 'AM5', 'cores' => 8, 'tdp' => 105]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Intel Core i5-13400F', + 'price' => 18000, + 'component_type_id' => 1, + 'specifications' => json_encode(['socket' => 'LGA1700', 'cores' => 10, 'tdp' => 65]), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Видеокарты (ID=2) + [ + 'name' => 'NVIDIA RTX 4060', + 'price' => 30000, + 'component_type_id' => 2, + 'specifications' => json_encode(['memory_size' => '8GB', 'gpu_clock_speed' => 2445]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'NVIDIA RTX 4070 Ti', + 'price' => 45000, + 'component_type_id' => 2, + 'specifications' => json_encode(['memory_size' => '12GB', 'gpu_clock_speed' => 2610]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'AMD Radeon RX 7800 XT', + 'price' => 40000, + 'component_type_id' => 2, + 'specifications' => json_encode(['memory_size' => '16GB', 'gpu_clock_speed' => 2200]), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Материнские платы (ID=3) + [ + 'name' => 'ASUS TUF GAMING B660M-PLUS', + 'price' => 8000, + 'component_type_id' => 3, + 'specifications' => json_encode(['chipset' => 'B660', 'form_factor' => 'mATX']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'MSI B750 TOMAHAWK', + 'price' => 12000, + 'component_type_id' => 3, + 'specifications' => json_encode(['chipset' => 'B750', 'form_factor' => 'ATX']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Gigabyte X670E AORUS PRO', + 'price' => 15000, + 'component_type_id' => 3, + 'specifications' => json_encode(['chipset' => 'X670E', 'form_factor' => 'ATX']), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // ОЗУ (ID=4) + [ + 'name' => 'Kingston FURY Beast DDR4 16GB', + 'price' => 5000, + 'component_type_id' => 4, + 'specifications' => json_encode(['type' => 'DDR4', 'capacity' => '16GB', 'speed' => 3200]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Corsair Vengeance RGB Pro 32GB DDR5-6000', + 'price' => 15000, + 'component_type_id' => 4, + 'specifications' => json_encode(['type' => 'DDR5', 'capacity' => '32GB', 'speed' => 6000]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'G.Skill Trident Z5 32GB DDR5-6000', + 'price' => 16000, + 'component_type_id' => 4, + 'specifications' => json_encode(['type' => 'DDR5', 'capacity' => '32GB', 'speed' => 6000]), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Блоки питания (ID=5) + [ + 'name' => 'EVGA SuperNOVA 750 G2', + 'price' => 8000, + 'component_type_id' => 5, + 'specifications' => json_encode(['wattage' => 750, 'efficiency' => '80+ Gold']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Seasonic FOCUS GX-850', + 'price' => 10000, + 'component_type_id' => 5, + 'specifications' => json_encode(['wattage' => 850, 'efficiency' => '80+ Gold']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Corsair RM850x', + 'price' => 12000, + 'component_type_id' => 5, + 'specifications' => json_encode(['wattage' => 850, 'efficiency' => '80+ Gold']), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // SSD (ID=6) + [ + 'name' => 'Samsung 980 PRO 1TB', + 'price' => 10000, + 'component_type_id' => 6, + 'specifications' => json_encode(['interface' => 'NVMe PCIe 4.0 x4', 'capacity' => 1000]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Crucial P3 500GB NVMe', + 'price' => 4000, + 'component_type_id' => 6, + 'specifications' => json_encode(['interface' => 'NVMe PCIe 3.0 x4', 'capacity' => 500]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'WD Black SN850X 1TB', + 'price' => 12000, + 'component_type_id' => 6, + 'specifications' => json_encode(['interface' => 'NVMe PCIe 4.0 x4', 'capacity' => 1000]), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Корпуса (ID=7) + [ + 'name' => 'Cooler Master H500P ATX Mid Tower Case', + 'price' => 4000, + 'component_type_id' => 7, + 'specifications' => json_encode(['form_factor' => 'ATX', 'cooling_type' => 'Air']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'be quiet! Dark Base Pro 9', + 'price' => 12000, + 'component_type_id' => 7, + 'specifications' => json_encode(['form_factor' => 'ATX', 'max_power' => 800]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Fractal Design Pop Air', + 'price' => 5000, + 'component_type_id' => 7, + 'specifications' => json_encode(['form_factor' => 'ATX', 'cooling_type' => 'Air']), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Охлаждение (ID=8) + [ + 'name' => 'Noctua NH-D15', + 'price' => 7000, + 'component_type_id' => 8, + 'specifications' => json_encode(['type' => 'Air', 'tdp' => 200]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Deepcool Assassin III', + 'price' => 5000, + 'component_type_id' => 8, + 'specifications' => json_encode(['type' => 'Air', 'tdp' => 150]), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'AIO Corsair iCUE H150i ELITE CAPELLIX', + 'price' => 15000, + 'component_type_id' => 8, + 'specifications' => json_encode(['type' => 'Liquid', 'radiator_size' => '360mm']), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Сеть (ID=9) + [ + 'name' => 'TP-Link Archer TX50E', + 'price' => 3000, + 'component_type_id' => 9, + 'specifications' => json_encode(['interface' => 'Wi-Fi 6', 'speed' => '2.5Gbps']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'Intel I225-V', + 'price' => 2000, + 'component_type_id' => 9, + 'specifications' => json_encode(['interface' => 'Ethernet', 'speed' => '2.5Gbps']), + 'is_official' => true, + 'created_by_user_id' => null + ], + + // Звуковые карты (ID=10) + [ + 'name' => 'Creative Sound Blaster Z', + 'price' => 8000, + 'component_type_id' => 10, + 'specifications' => json_encode(['interface' => 'PCIe', 'sample_rate' => '192kHz']), + 'is_official' => true, + 'created_by_user_id' => null + ], + [ + 'name' => 'ASUS Xonar SE', + 'price' => 5000, + 'component_type_id' => 10, + 'specifications' => json_encode(['interface' => 'PCIe', 'sample_rate' => '96kHz']), + 'is_official' => true, + 'created_by_user_id' => null + ] + ]; + + foreach ($components as $component) { + DB::table('components')->insert($component); + } + + // 3. Добавляем 4 задачи в ai_tasks + // 3. Добавляем 4 задачи в ai_tasks +$aiTasks = [ + [ + 'user_id' => null, + 'name' => 'Игровая сборка до 80000 рублей', + 'prompt_template' => 'Подобрать оптимальную сборку для игр в 1080p до 80000 рублей.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now() + ], + [ + 'user_id' => null, + 'name' => 'Офисная сборка до 30000 рублей', + 'prompt_template' => 'Подобрать бюджетную сборку для офисных задач и интернета.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now() + ], + [ + 'user_id' => null, + 'name' => 'Рабочая станция для видеомонтажа', + 'prompt_template' => 'Подобрать мощную сборку для редактирования 4K видео.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now() + ], + [ + 'user_id' => null, + 'name' => 'Игровая сборка до 1500$', + 'prompt_template' => 'Подобрать топовую сборку для игр в 4K до 1500$.', + 'is_active' => true, + 'created_at' => now(), + 'updated_at' => now() + ] +]; + + foreach ($aiTasks as $task) { + DB::table('ai_tasks')->insert($task); + } + } +} \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..dc76dc2 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,13 +8,13 @@ use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { - use WithoutModelEvents; /** * Seed the application's database. */ public function run(): void { + $this->call(ComponentSeeder::class); // User::factory(10)->create(); User::factory()->create([ diff --git a/routes/api.php b/routes/api.php index 25cb6f5..228023e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,21 +4,52 @@ use App\Http\Controllers\UsersController; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use App\Http\Controllers\ComponentsController; +use App\Http\Controllers\AuthController; +use App\Http\Controllers\PCBuildsController; +use App\Http\Controllers\AiController; Route::get('/users', function (Request $request) { return $request->user(); })->middleware('auth:sanctum'); -Route::get('components', [ComponentsController::class, 'index']); + Route::get('components/{id}', [ComponentsController::class, 'show']); -Route::post('components', [ComponentsController::class, 'create']); + Route::post('users', [UsersController::class, 'create']); -Route::put('/components', [ComponentsController::class, 'update']); -Route::delete('/components', [ComponentsController::class, 'destroy']); +Route::post('/register', [AuthController::class, 'register']); +Route::post('/login', [AuthController::class, 'login']); + +// Защита маршрутов — только для авторизованных +Route::middleware('auth:sanctum')->group(function () { + Route::post('/logout', [AuthController::class, 'logout']); +}); + + + +Route::middleware('auth:sanctum')->group(function () { + Route::get('/components', [ComponentsController::class, 'index']); + Route::post('/components', [ComponentsController::class, 'store']); + Route::put('/components/{id}', [ComponentsController::class, 'update']); + Route::delete('/components/{id}', [ComponentsController::class, 'destroy']); +}); + +Route::middleware('auth:sanctum')->group(function () { + Route::get('/builds', [PCBuildsController::class, 'index']); + Route::post('/builds', [PCBuildsController::class, 'store']); + Route::get('/builds/{id}', [PCBuildsController::class, 'show']); + Route::put('/builds/{id}', [PCBuildsController::class, 'update']); + Route::delete('/builds/{id}', [PCBuildsController::class, 'destroy']); +}); + +Route::middleware('auth:sanctum')->group(function () { + Route::post('/ai/suggest', [AiController::class, 'suggest']); +}); + +