📋 Backend Implementation Plan: Likes & Comments System

📚 Table of Contents

🗄️ Database Schema

Models Table (enhanced)

CREATE TABLE models (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    uuid VARCHAR(36) UNIQUE NOT NULL, -- For public URLs
    title VARCHAR(255) NOT NULL,
    description TEXT,
    author_id BIGINT NOT NULL,
    category VARCHAR(100),
    model_type VARCHAR(50) NOT NULL, -- STL, OBJ, PLY, etc.
    file_path VARCHAR(500) NOT NULL,
    file_size BIGINT NOT NULL, -- in bytes
    thumbnail_path VARCHAR(500),
    preview_image_path VARCHAR(500),
    license VARCHAR(100) DEFAULT 'CC-BY',
    status ENUM('processing', 'ready', 'failed') DEFAULT 'processing',
    is_public BOOLEAN DEFAULT TRUE,
    is_featured BOOLEAN DEFAULT FALSE,
    processing_error TEXT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    INDEX idx_models_author (author_id),
    INDEX idx_models_category (category),
    INDEX idx_models_status (status),
    INDEX idx_models_public (is_public, created_at),
    INDEX idx_models_featured (is_featured, created_at),
    FULLTEXT idx_models_search (title, description),
    
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE
);

Model Tags Table

CREATE TABLE model_tags (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_id BIGINT NOT NULL,
    tag VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_model_tag (model_id, tag),
    INDEX idx_tag_lookup (tag),
    
    FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE
);

Model Categories Table

CREATE TABLE model_categories (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) UNIQUE NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    icon VARCHAR(100), -- FontAwesome icon class
    sort_order INT DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_categories_active (is_active, sort_order)
);

File Processing Jobs Table

CREATE TABLE model_processing_jobs (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_id BIGINT NOT NULL,
    job_type ENUM('thumbnail', 'virus_scan', 'format_validation', 'metadata_extraction') NOT NULL,
    status ENUM('pending', 'processing', 'completed', 'failed') DEFAULT 'pending',
    result JSON,
    error_message TEXT,
    started_at TIMESTAMP NULL,
    completed_at TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    INDEX idx_processing_jobs_model (model_id),
    INDEX idx_processing_jobs_status (status),
    
    FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE
);

Comments Table

CREATE TABLE model_comments (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    parent_comment_id BIGINT NULL, -- For replies (self-referencing)
    text TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP NULL, -- Soft delete
    
    INDEX idx_model_comments (model_id, created_at),
    INDEX idx_parent_comments (parent_comment_id),
    INDEX idx_user_comments (user_id),
    
    FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (parent_comment_id) REFERENCES model_comments(id) ON DELETE CASCADE
);

Likes Tables

-- Model Likes
CREATE TABLE model_likes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    model_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_model_user_like (model_id, user_id),
    INDEX idx_model_likes (model_id),
    INDEX idx_user_likes (user_id),
    
    FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- Comment Likes
CREATE TABLE comment_likes (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    comment_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY unique_comment_user_like (comment_id, user_id),
    INDEX idx_comment_likes (comment_id),
    INDEX idx_user_comment_likes (user_id),
    
    FOREIGN KEY (comment_id) REFERENCES model_comments(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

Cached Counts Table (for performance)

CREATE TABLE model_stats (
    model_id BIGINT PRIMARY KEY,
    likes_count INT DEFAULT 0,
    comments_count INT DEFAULT 0,
    downloads_count INT DEFAULT 0,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    FOREIGN KEY (model_id) REFERENCES models(id) ON DELETE CASCADE
);

🚀 API Endpoints

1. Models List (Browse/Search)

GET /api/v1/models?page=1&limit=12&search=gear&category=mechanical&sort=popular

Query Parameters:

Response:

{
  "data": [
    {
      "id": 1,
      "title": "Mechanical Gear Assembly",
      "description": "High-precision gear mechanism...",
      "author": {
        "id": 123,
        "name": "Alex Rodriguez",
        "avatar": "https://cdn.example.com/avatars/123.jpg"
      },
      "thumbnail": "https://cdn.example.com/thumbnails/model_1_thumb.jpg",
      "tags": ["mechanical", "gear", "engineering"],
      "likes": 142,
      "comments": 18,
      "downloads": 85,
      "isLiked": false,
      "isSaved": true,
      "modelType": "STL",
      "fileSize": "2.4 MB",
      "createdAt": "2025-01-20T10:00:00Z"
    }
  ],
  "meta": {
    "currentPage": 1,
    "totalPages": 8,
    "totalCount": 94,
    "hasMore": true
  }
}

2. Upload New Model

POST /api/v1/models/upload

Request (Multipart Form Data):

Content-Type: multipart/form-data

{
  "title": "My Latest 3D Creation",
  "description": "Detailed description of the model...",
  "tags": ["creative", "artistic", "sculpture"],
  "category": "art",
  "license": "CC-BY",
  "model_file": [Binary STL/OBJ file],
  "preview_image": [Binary JPG/PNG file] // Optional
}

Response:

{
  "data": {
    "id": 95,
    "title": "My Latest 3D Creation",
    "description": "Detailed description of the model...",
    "author": {
      "id": 789,
      "name": "Current User",
      "avatar": "https://cdn.example.com/avatars/789.jpg"
    },
    "thumbnail": "https://cdn.example.com/thumbnails/model_95_thumb.jpg",
    "tags": ["creative", "artistic", "sculpture"],
    "likes": 0,
    "comments": 0,
    "downloads": 0,
    "isLiked": false,
    "isSaved": false,
    "modelType": "STL",
    "fileSize": "3.7 MB",
    "status": "processing", // processing|ready|failed
    "createdAt": "2025-01-24T09:00:00Z"
  }
}

3. Model with Comments & Likes

GET /api/v1/models/{modelId}

Response:

{
  "data": {
    "id": 1,
    "title": "Mechanical Gear Assembly",
    "description": "...",
    "author": {
      "id": 123,
      "name": "Alex Rodriguez",
      "avatar": "https://cdn.example.com/avatars/123.jpg"
    },
    "tags": ["mechanical", "gear"],
    "likes": 142,
    "comments": 18,
    "downloads": 85,
    "isLiked": false,
    "isSaved": true,
    "modelType": "STL",
    "createdAt": "2025-01-20T10:00:00Z"
  }
}

2. Get Comments

GET /api/v1/models/{modelId}/comments?page=1&limit=10&sort=newest

Query Parameters:

Response:

{
  "data": [
    {
      "id": 1,
      "text": "Amazing work!",
      "author": {
        "id": 456,
        "name": "Sarah Chen",
        "avatar": "https://cdn.example.com/avatars/456.jpg"
      },
      "createdAt": "2025-01-23T18:00:00Z",
      "likes": 3,
      "isLiked": false,
      "replies": [
        {
          "id": 101,
          "text": "Thanks!",
          "author": {
            "id": 123,
            "name": "Alex Rodriguez",
            "avatar": "https://cdn.example.com/avatars/123.jpg"
          },
          "createdAt": "2025-01-23T18:30:00Z",
          "likes": 1,
          "isLiked": true
        }
      ]
    }
  ],
  "meta": {
    "currentPage": 1,
    "totalPages": 2,
    "totalCount": 18,
    "hasMore": true
  }
}

3. Create Comment

POST /api/v1/models/{modelId}/comments

Request Body:

{
  "text": "This is an amazing model!",
  "parentCommentId": null // Optional: for replies
}

Response:

{
  "data": {
    "id": 19,
    "text": "This is an amazing model!",
    "author": {
      "id": 789,
      "name": "Current User",
      "avatar": "https://cdn.example.com/avatars/789.jpg"
    },
    "createdAt": "2025-01-24T09:00:00Z",
    "likes": 0,
    "isLiked": false,
    "replies": []
  }
}

4. Like/Unlike Model

POST /api/v1/models/{modelId}/like
DELETE /api/v1/models/{modelId}/like

Response:

{
  "data": {
    "liked": true,
    "likesCount": 143
  }
}

5. Like/Unlike Comment

POST /api/v1/comments/{commentId}/like
DELETE /api/v1/comments/{commentId}/like

Response:

{
  "data": {
    "liked": true,
    "likesCount": 4
  }
}

🏗️ Laravel Implementation Structure

Models

// app/Models/Model.php
class Model extends Eloquent {
    protected $fillable = ['title', 'description', 'author_id', 'model_type'];
    
    public function author() {
        return $this->belongsTo(User::class, 'author_id');
    }
    
    public function comments() {
        return $this->hasMany(ModelComment::class)->whereNull('parent_comment_id');
    }
    
    public function likes() {
        return $this->hasMany(ModelLike::class);
    }
    
    public function stats() {
        return $this->hasOne(ModelStats::class);
    }
    
    public function isLikedBy(User $user) {
        return $this->likes()->where('user_id', $user->id)->exists();
    }
}

// app/Models/ModelComment.php
class ModelComment extends Eloquent {
    protected $fillable = ['model_id', 'user_id', 'parent_comment_id', 'text'];
    
    public function model() {
        return $this->belongsTo(Model::class);
    }
    
    public function author() {
        return $this->belongsTo(User::class, 'user_id');
    }
    
    public function replies() {
        return $this->hasMany(ModelComment::class, 'parent_comment_id');
    }
    
    public function parent() {
        return $this->belongsTo(ModelComment::class, 'parent_comment_id');
    }
    
    public function likes() {
        return $this->hasMany(CommentLike::class, 'comment_id');
    }
    
    public function isLikedBy(User $user) {
        return $this->likes()->where('user_id', $user->id)->exists();
    }
}

Controllers

Model Controller

// app/Http/Controllers/Api/ModelController.php
class ModelController extends Controller {
    public function index(Request $request) {
        $query = Model::query()
            ->with(['author', 'stats'])
            ->where('status', 'ready')
            ->where('is_public', true);
            
        // Search functionality
        if ($search = $request->get('search')) {
            $query->whereFullText(['title', 'description'], $search);
        }
        
        // Category filter
        if ($category = $request->get('category')) {
            $query->where('category', $category);
        }
        
        // Tags filter
        if ($tags = $request->get('tags')) {
            $tagArray = explode(',', $tags);
            $query->whereHas('tags', function($q) use ($tagArray) {
                $q->whereIn('tag', $tagArray);
            });
        }
        
        // Author filter
        if ($authorId = $request->get('author_id')) {
            $query->where('author_id', $authorId);
        }
        
        // Sorting
        $sort = $request->get('sort', 'newest');
        switch ($sort) {
            case 'popular':
                $query->join('model_stats', 'models.id', '=', 'model_stats.model_id')
                      ->orderByDesc('model_stats.likes_count');
                break;
            case 'downloads':
                $query->join('model_stats', 'models.id', '=', 'model_stats.model_id')
                      ->orderByDesc('model_stats.downloads_count');
                break;
            case 'alphabetical':
                $query->orderBy('title');
                break;
            default: // newest
                $query->orderByDesc('created_at');
        }
        
        $models = $query->paginate($request->get('limit', 12));
        
        return ModelResource::collection($models);
    }
    
    public function show(Model $model) {
        $model->load(['author', 'stats', 'tags']);
        
        // Check if current user has liked/saved this model
        if ($user = auth()->user()) {
            $model->isLiked = $model->isLikedBy($user);
            $model->isSaved = $user->savedModels()->where('model_id', $model->id)->exists();
        }
        
        return new ModelResource($model);
    }
    
    public function store(StoreModelRequest $request) {
        $user = auth()->user();
        
        // Create model record
        $model = Model::create([
            'uuid' => Str::uuid(),
            'title' => $request->title,
            'description' => $request->description,
            'author_id' => $user->id,
            'category' => $request->category,
            'model_type' => $request->model_file->getClientOriginalExtension(),
            'license' => $request->license ?? 'CC-BY',
            'status' => 'processing'
        ]);
        
        // Handle file upload
        $this->handleFileUpload($model, $request);
        
        // Process tags
        if ($request->tags) {
            foreach ($request->tags as $tag) {
                $model->tags()->create(['tag' => strtolower(trim($tag))]);
            }
        }
        
        // Queue processing jobs
        ProcessModelFileJob::dispatch($model);
        GenerateThumbnailJob::dispatch($model);
        
        return new ModelResource($model->load(['author', 'tags']));
    }
    
    public function like(Model $model) {
        $user = auth()->user();
        
        $like = $model->likes()->firstOrCreate(['user_id' => $user->id]);
        
        // Update stats asynchronously
        UpdateModelStatsJob::dispatch($model);
        
        return response()->json([
            'data' => [
                'liked' => true,
                'likesCount' => $model->likes()->count()
            ]
        ]);
    }
    
    public function unlike(Model $model) {
        $user = auth()->user();
        
        $model->likes()->where('user_id', $user->id)->delete();
        
        // Update stats asynchronously
        UpdateModelStatsJob::dispatch($model);
        
        return response()->json([
            'data' => [
                'liked' => false,
                'likesCount' => $model->likes()->count()
            ]
        ]);
    }
    
    public function download(Model $model) {
        if ($model->status !== 'ready') {
            return response()->json(['error' => 'Model not ready for download'], 422);
        }
        
        // Increment download count
        IncrementDownloadCountJob::dispatch($model);
        
        // Generate signed URL for secure download
        $downloadUrl = Storage::temporaryUrl($model->file_path, now()->addMinutes(10));
        
        return response()->json([
            'data' => [
                'downloadUrl' => $downloadUrl,
                'fileName' => $model->title . '.' . $model->model_type,
                'fileSize' => $model->file_size
            ]
        ]);
    }
    
    private function handleFileUpload(Model $model, StoreModelRequest $request) {
        // Upload to S3 with organized structure
        $fileName = $model->uuid . '.' . $request->model_file->getClientOriginalExtension();
        $filePath = "models/{$model->author_id}/{$fileName}";
        
        $path = $request->model_file->storeAs($filePath, $fileName, 's3');
        
        $model->update([
            'file_path' => $path,
            'file_size' => $request->model_file->getSize()
        ]);
        
        // Handle preview image if provided
        if ($request->hasFile('preview_image')) {
            $previewFileName = $model->uuid . '_preview.' . $request->preview_image->getClientOriginalExtension();
            $previewPath = "models/{$model->author_id}/previews/{$previewFileName}";
            
            $previewStoredPath = $request->preview_image->storeAs($previewPath, $previewFileName, 's3');
            $model->update(['preview_image_path' => $previewStoredPath]);
        }
    }
}

Comment Controller

// app/Http/Controllers/Api/CommentController.php
class CommentController extends Controller {
    public function index(Model $model, Request $request) {
        $comments = $model->comments()
            ->with(['author', 'replies.author', 'likes'])
            ->withCount('likes')
            ->orderBy($this->getSortColumn($request->get('sort', 'newest')))
            ->paginate($request->get('limit', 10));
            
        return CommentResource::collection($comments);
    }
    
    public function store(Model $model, StoreCommentRequest $request) {
        $comment = $model->comments()->create([
            'user_id' => auth()->id(),
            'text' => $request->text,
            'parent_comment_id' => $request->parentCommentId,
        ]);
        
        // Update model stats
        $this->updateModelStats($model);
        
        return new CommentResource($comment->load('author'));
    }
    
    public function like(ModelComment $comment) {
        $user = auth()->user();
        
        $like = $comment->likes()->firstOrCreate(['user_id' => $user->id]);
        
        return response()->json([
            'data' => [
                'liked' => true,
                'likesCount' => $comment->likes()->count()
            ]
        ]);
    }
    
    public function unlike(ModelComment $comment) {
        $user = auth()->user();
        
        $comment->likes()->where('user_id', $user->id)->delete();
        
        return response()->json([
            'data' => [
                'liked' => false,
                'likesCount' => $comment->likes()->count()
            ]
        ]);
    }
}

⚡ Performance Optimizations

1. Database Indexing

-- Composite indexes for common queries
CREATE INDEX idx_model_comments_created (model_id, created_at DESC);
CREATE INDEX idx_comment_likes_count (comment_id, user_id);
CREATE INDEX idx_model_stats_lookup (model_id, likes_count, comments_count);

2. Caching Strategy

// Cache model stats for 5 minutes
public function getModelStats(Model $model) {
    return Cache::remember("model_stats_{$model->id}", 300, function() use ($model) {
        return [
            'likes' => $model->likes()->count(),
            'comments' => $model->comments()->count(),
        ];
    });
}

3. Background Jobs

// app/Jobs/UpdateModelStats.php
class UpdateModelStats implements ShouldQueue {
    public function handle(Model $model) {
        $model->stats()->updateOrCreate([], [
            'likes_count' => $model->likes()->count(),
            'comments_count' => $model->comments()->count(),
        ]);
        
        // Clear related caches
        Cache::forget("model_stats_{$model->id}");
    }
}

// app/Jobs/ProcessModelFileJob.php
class ProcessModelFileJob implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected $model;
    
    public function __construct(Model $model) {
        $this->model = $model;
    }
    
    public function handle() {
        try {
            // Virus scan
            $this->performVirusScan();
            
            // Validate file format
            $this->validateFileFormat();
            
            // Extract metadata
            $this->extractMetadata();
            
            // Mark as ready
            $this->model->update(['status' => 'ready']);
            
            // Generate initial stats
            $this->model->stats()->create([
                'likes_count' => 0,
                'comments_count' => 0,
                'downloads_count' => 0
            ]);
            
        } catch (Exception $e) {
            $this->model->update([
                'status' => 'failed',
                'processing_error' => $e->getMessage()
            ]);
            
            Log::error('Model processing failed', [
                'model_id' => $this->model->id,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    private function performVirusScan() {
        // Integration with ClamAV or similar
        $filePath = Storage::path($this->model->file_path);
        
        if (!$this->isFileClean($filePath)) {
            throw new Exception('File failed virus scan');
        }
    }
    
    private function validateFileFormat() {
        $allowedTypes = ['stl', 'obj', 'ply', '3mf'];
        
        if (!in_array(strtolower($this->model->model_type), $allowedTypes)) {
            throw new Exception('Unsupported file format');
        }
        
        // Additional format-specific validation
        $this->validateFileStructure();
    }
    
    private function extractMetadata() {
        // Extract polygon count, dimensions, etc.
        $metadata = $this->parseModelFile();
        
        $this->model->processing_jobs()->create([
            'job_type' => 'metadata_extraction',
            'status' => 'completed',
            'result' => $metadata,
            'completed_at' => now()
        ]);
    }
}

// app/Jobs/GenerateThumbnailJob.php
class GenerateThumbnailJob implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected $model;
    
    public function __construct(Model $model) {
        $this->model = $model;
    }
    
    public function handle() {
        try {
            // Use Three.js headless renderer or similar to generate thumbnail
            $thumbnailPath = $this->generateThumbnail();
            
            $this->model->update(['thumbnail_path' => $thumbnailPath]);
            
            $this->model->processing_jobs()->create([
                'job_type' => 'thumbnail',
                'status' => 'completed',
                'completed_at' => now()
            ]);
            
        } catch (Exception $e) {
            $this->model->processing_jobs()->create([
                'job_type' => 'thumbnail',
                'status' => 'failed',
                'error_message' => $e->getMessage(),
                'completed_at' => now()
            ]);
        }
    }
    
    private function generateThumbnail() {
        // Use puppeteer + three.js to generate thumbnail
        $thumbnailFileName = $this->model->uuid . '_thumb.jpg';
        $thumbnailPath = "models/{$this->model->author_id}/thumbnails/{$thumbnailFileName}";
        
        // Implementation would use headless browser to render 3D model
        $thumbnailData = $this->renderModelThumbnail();
        
        Storage::put($thumbnailPath, $thumbnailData);
        
        return $thumbnailPath;
    }
}

🔒 Security & Validation

Request Validation

// app/Http/Requests/StoreModelRequest.php
class StoreModelRequest extends FormRequest {
    public function rules() {
        return [
            'title' => 'required|string|min:3|max:255',
            'description' => 'required|string|min:10|max:5000',
            'category' => 'required|string|exists:model_categories,slug',
            'tags' => 'required|array|min:1|max:10',
            'tags.*' => 'string|min:2|max:30|alpha_dash',
            'license' => 'nullable|string|in:CC-BY,CC-BY-SA,CC-BY-NC,CC-BY-ND,MIT,GPL-3.0',
            'model_file' => 'required|file|max:102400|mimes:stl,obj,ply,3mf', // 100MB max
            'preview_image' => 'nullable|image|max:10240|mimes:jpg,jpeg,png,webp' // 10MB max
        ];
    }
    
    public function messages() {
        return [
            'model_file.max' => 'Model file cannot exceed 100MB',
            'model_file.mimes' => 'Only STL, OBJ, PLY, and 3MF files are supported',
            'tags.min' => 'At least one tag is required',
            'tags.max' => 'Maximum 10 tags allowed'
        ];
    }
}

// app/Http/Requests/StoreCommentRequest.php
class StoreCommentRequest extends FormRequest {
    public function rules() {
        return [
            'text' => 'required|string|min:1|max:2000',
            'parentCommentId' => 'nullable|exists:model_comments,id',
        ];
    }
}

Rate Limiting & Routes

// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
    // Model management
    Route::get('/models', [ModelController::class, 'index']);
    Route::get('/models/{model:uuid}', [ModelController::class, 'show']);
    Route::post('/models/upload', [ModelController::class, 'store'])->middleware('throttle:uploads');
    Route::get('/models/{model:uuid}/download', [ModelController::class, 'download']);
    
    // Model interactions
    Route::post('/models/{model:uuid}/like', [ModelController::class, 'like'])->middleware('throttle:likes');
    Route::delete('/models/{model:uuid}/like', [ModelController::class, 'unlike'])->middleware('throttle:likes');
    
    // Comments
    Route::get('/models/{model:uuid}/comments', [CommentController::class, 'index']);
    Route::post('/models/{model:uuid}/comments', [CommentController::class, 'store'])->middleware('throttle:comments');
    Route::post('/comments/{comment}/like', [CommentController::class, 'like'])->middleware('throttle:likes');
    Route::delete('/comments/{comment}/like', [CommentController::class, 'unlike'])->middleware('throttle:likes');
});

// Rate limiters in app/Providers/RouteServiceProvider.php
RateLimiter::for('uploads', function (Request $request) {
    return Limit::perHour(10)->by($request->user()->id); // 10 uploads per hour
});

RateLimiter::for('comments', function (Request $request) {
    return Limit::perMinute(5)->by($request->user()->id); // 5 comments per minute
});

RateLimiter::for('likes', function (Request $request) {
    return Limit::perMinute(30)->by($request->user()->id); // 30 likes per minute
});

📊 Testing Strategy

Feature Tests

// tests/Feature/CommentTest.php
class CommentTest extends TestCase {
    public function test_user_can_create_comment() {
        $user = User::factory()->create();
        $model = Model::factory()->create();
        
        $response = $this->actingAs($user)
            ->postJson("/api/v1/models/{$model->id}/comments", [
                'text' => 'Great model!'
            ]);
            
        $response->assertStatus(201)
            ->assertJsonStructure(['data' => ['id', 'text', 'author']]);
    }
    
    public function test_user_can_like_comment() {
        $user = User::factory()->create();
        $comment = ModelComment::factory()->create();
        
        $response = $this->actingAs($user)
            ->postJson("/api/v1/comments/{$comment->id}/like");
            
        $response->assertStatus(200)
            ->assertJson(['data' => ['liked' => true]]);
    }
}

🚀 Deployment Checklist

Essential Steps

  1. Database Migration Run all table creation scripts (models, comments, likes, stats, tags, categories, processing_jobs)
  2. Seed Data Create initial model categories and model_stats entries for existing models
  3. File Storage Configure S3 buckets with proper permissions and CDN setup
  4. Queue Workers Ensure background job processing is running (file processing, thumbnail generation)
  5. Cache Configuration Set up Redis/Memcached for caching
  6. Rate Limiting Configure appropriate limits for production (uploads, comments, likes)
  7. Security Setup Implement virus scanning (ClamAV) for uploaded files
  8. Monitoring Add logging for uploads, processing failures, and user actions
  9. CDN Ensure model files, thumbnails, and avatars are properly served from CDN
  10. Search Index Set up full-text search indexes for model discovery

🎯 Key Performance Metrics

🗂️ File Organization Structure

s3://your-bucket/
├── models/
│   ├── {user_id}/
│   │   ├── {uuid}.stl           # Original model files
│   │   ├── thumbnails/
│   │   │   └── {uuid}_thumb.jpg # Generated thumbnails
│   │   └── previews/
│   │       └── {uuid}_preview.jpg # User-uploaded previews
├── avatars/
│   └── {user_id}.jpg           # User profile pictures
└── temp-uploads/               # Temporary upload storage

This comprehensive implementation provides a complete 3D model sharing platform with: