Compare commits

...

2 Commits

Author SHA1 Message Date
dimon8
d8a8759504 crud 2026-01-07 19:29:49 +00:00
dimon8
31b79d70c5 123 2026-01-07 06:44:07 +00:00
18 changed files with 488 additions and 66 deletions

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Регистрация пользователя.
*/
public function register(Request $request)
{
$validated = $request->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' => 'Вы успешно вышли из системы.'
]);
}
}

View File

@@ -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();
return response()->json($component->toJson());
}
public function update(Request $request, int $id): JsonResponse{
$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([
'name' => $request->get('name'),
'type' => $request->get('type'),
'brand' => $request->get('brand'),
'model' => $request->get('model'),
'price' => $request->get('price'),
], Response::HTTP_ACCEPTED);
'message' => 'Компонент успешно создан.',
'component' => $component
], 201);
}
}
public function update(Request $request, $id)
{
$component = Component::findOrFail($id);
public function destroy(int $id): JsonResponse
{
// мы бы здесь написали вызов запроса delete из БД
// Проверяем, что компонент принадлежит пользователю и не официальный
if ($component->created_by_user_id !== auth()->id() || $component->is_official) {
return response()->json([
'success' => true,
], Response::HTTP_ACCEPTED);
'message' => 'Вы не можете редактировать этот компонент.'
], 403);
}
$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->update($validated);
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([
'message' => 'Вы не можете удалить этот компонент.'
], 403);
}
$component->delete();
return response()->json([
'message' => 'Компонент удалён.'
]);
}

View File

@@ -33,8 +33,5 @@ class UsersController extends Controller
return ['token' => $user->createToken('frontend')->plainTextToken];
}
}

42
app/Models/AiTask.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AiTask extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'prompt_template',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Связь с пользователем (если шаблон пользовательский)
public function user()
{
return $this->belongsTo(User::class);
}
// Общие (глобальные) шаблоны — где user_id IS NULL
public function scopeGlobal($query)
{
return $query->whereNull('user_id');
}
// Активные шаблоны
public function scopeActive($query)
{
return $query->where('is_active', true);
}
}

View File

@@ -2,9 +2,33 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Component extends Model
{
use HasFactory;
protected $fillable = [
'name',
'price',
'component_type_id',
'specifications',
'is_official',
'created_by_user_id',
];
protected $casts = [
'specifications' => 'array', // автоматически преобразует JSON в массив
];
public function user()
{
return $this->belongsTo(User::class, 'created_by_user_id');
}
public function componentType()
{
return $this->belongsTo(ComponentType::class, 'component_type_id');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ComponentType extends Model
{
protected $fillable = ['name', 'code'];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}

31
app/Models/PCBuild.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PcBuild extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'description',
'is_ai_generated',
'ai_prompt',
];
protected $casts = [
'is_ai_generated' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Связь с пользователем
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PcBuildComponent extends Model
{
use HasFactory;
protected $fillable = [
'pc_build_id',
'component_id',
'quantity',
];
protected $casts = [
'quantity' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
// Связь с сборкой
public function build()
{
return $this->belongsTo(PcBuild::class);
}
// Связь с компонентом
public function component()
{
return $this->belongsTo(Component::class);
}
}

View File

@@ -24,6 +24,7 @@ class User extends Authenticatable
'name',
'email',
'password',
'custom_field'
];
/**

View File

@@ -8,7 +8,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1"
},
"require-dev": {

19
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "d3c16cb86c42230c6c023d9a5d9bcf42",
"content-hash": "8f387a0734f3bf879214e4aa2fca6e2f",
"packages": [
{
"name": "brick/math",
@@ -1333,16 +1333,16 @@
},
{
"name": "laravel/sanctum",
"version": "v4.2.0",
"version": "v4.2.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677"
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
"reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
"shasum": ""
},
"require": {
@@ -1356,9 +1356,8 @@
},
"require-dev": {
"mockery/mockery": "^1.6",
"orchestra/testbench": "^9.0|^10.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^11.3"
"orchestra/testbench": "^9.15|^10.8",
"phpstan/phpstan": "^1.10"
},
"type": "library",
"extra": {
@@ -1393,7 +1392,7 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
"time": "2025-07-09T19:45:24+00:00"
"time": "2026-01-06T23:11:51+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -8463,5 +8462,5 @@
"php": "^8.2"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.9.0"
}

View File

@@ -13,15 +13,20 @@ return new class extends Migration
{
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->text('name');
$table->text('type');
$table->text('brand');
$table->text('model');
$table->integer('price');
});
$table->foreign('component_type_id')->references('id')->on('component_types');
$table->foreign('created_by_user_id')->references('id')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/

View File

@@ -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);
//
});
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('component_types', function (Blueprint $table) {
$table->id();
$table->string('name'); // Например: "Процессор", "Видеокарта"
$table->string('code')->unique(); // Уникальный код: cpu, gpu, ram и т.д.
$table->timestamps();
});
// Заполняем таблицу начальными данными (seed)
DB::table('component_types')->insert([
['name' => 'Процессор', 'code' => 'cpu'],
['name' => 'Видеокарта', 'code' => 'gpu'],
['name' => 'Материнская плата', 'code' => 'motherboard'],
['name' => 'ОЗУ', 'code' => 'ram'],
['name' => 'Блок питания', 'code' => 'psu'],
['name' => 'SSD', 'code' => 'ssd'],
['name' => 'Корпус', 'code' => 'case'],
]);
}
public function down()
{
Schema::dropIfExists('component_types');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePcBuildsTable extends Migration
{
public function up()
{
Schema::create('pc_builds', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('name'); // Например: "Игровой ПК 2025"
$table->text('description')->nullable(); // Описание
$table->boolean('is_ai_generated')->default(false); // true = сгенерировано ИИ
$table->text('ai_prompt')->nullable(); // Исходный запрос к ИИ
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('pc_builds');
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePcBuildComponentsTable extends Migration
{
public function up()
{
Schema::create('pc_build_components', function (Blueprint $table) {
$table->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');
}
}

View File

@@ -0,0 +1,61 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
{
Schema::create('ai_tasks', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null');
$table->string('name');
$table->text('prompt_template');
$table->boolean('is_active')->default(true);
$table->timestamps();
// Индекс для поиска активных шаблонов
$table->index(['is_active']);
});
// Заполняем базовыми шаблонами от админа (user_id = NULL)
DB::table('ai_tasks')->insert([
[
'name' => 'Игровой ПК до 50 000 ₽',
'prompt_template' => 'Собери бюджетный игровой ПК до 50000 рублей. Цель: игры на средних настройках в 1080p.',
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => 'Игровой ПК до 100 000 ₽',
'prompt_template' => 'Собери мощный игровой ПК до 100000 рублей. Цель: игры на ультра в 1440p, 60+ FPS.',
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => 'Офисный ПК',
'prompt_template' => 'Собери надёжный ПК для офиса и учёбы. Бюджет до 40000 рублей. Важна тишина и энергоэффективность.',
'is_active' => true,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ai_tasks');
}
};

View File

@@ -4,21 +4,39 @@ use App\Http\Controllers\UsersController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ComponentsController;
use App\Http\Controllers\AuthController;
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']);
});