Save workspace changes
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('content_types', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 64);
|
||||
$table->string('slug', 64)->unique();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('content_types');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('content_type_id')
|
||||
->constrained('content_types')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreignId('parent_id')
|
||||
->nullable()
|
||||
->constrained('categories')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('name', 128);
|
||||
$table->string('slug', 128);
|
||||
$table->text('description')->nullable();
|
||||
$table->string('image', 255)->nullable();
|
||||
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['content_type_id', 'slug']);
|
||||
$table->index(['parent_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_category', function (Blueprint $table) {
|
||||
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('category_id');
|
||||
|
||||
$table->primary(['artwork_id', 'category_id']);
|
||||
|
||||
$table->foreign('category_id')
|
||||
->references('id')
|
||||
->on('categories')
|
||||
->cascadeOnDelete();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_category');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('category_seo', function (Blueprint $table) {
|
||||
$table->foreignId('category_id')
|
||||
->primary()
|
||||
->constrained('categories')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('meta_title')->nullable();
|
||||
$table->string('meta_description')->nullable();
|
||||
$table->string('meta_keywords')->nullable();
|
||||
$table->string('canonical_url')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('category_seo');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('category_translations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('category_id')
|
||||
->constrained('categories')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->char('locale', 2);
|
||||
|
||||
$table->string('name', 128);
|
||||
$table->text('description')->nullable();
|
||||
|
||||
$table->unique(['category_id', 'locale']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('category_translations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artworks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->index();
|
||||
|
||||
$table->string('title', 150);
|
||||
$table->string('slug', 160)->index();
|
||||
$table->text('description')->nullable();
|
||||
|
||||
$table->string('file_name');
|
||||
$table->string('file_path');
|
||||
$table->unsignedBigInteger('file_size');
|
||||
$table->string('mime_type', 64);
|
||||
|
||||
$table->unsignedInteger('width');
|
||||
$table->unsignedInteger('height');
|
||||
|
||||
$table->boolean('is_public')->default(true)->index();
|
||||
$table->boolean('is_approved')->default(false)->index();
|
||||
$table->timestamp('published_at')->nullable()->index();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['is_public', 'is_approved', 'published_at'], 'idx_artworks_browse');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artworks');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_translations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained()->cascadeOnDelete();
|
||||
$table->char('locale', 2);
|
||||
|
||||
$table->string('title', 150);
|
||||
$table->text('description')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['artwork_id', 'locale']);
|
||||
$table->index(['locale', 'artwork_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_translations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_stats', function (Blueprint $table) {
|
||||
$table->foreignId('artwork_id')->primary()->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->unsignedBigInteger('views')->default(0);
|
||||
$table->unsignedBigInteger('downloads')->default(0);
|
||||
$table->unsignedBigInteger('favorites')->default(0);
|
||||
|
||||
$table->float('rating_avg', 4, 2)->default(0);
|
||||
$table->unsignedInteger('rating_count')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_stats');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_comments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->index();
|
||||
|
||||
$table->text('content');
|
||||
$table->boolean('is_approved')->default(true)->index();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['artwork_id', 'created_at'], 'idx_comments_artwork');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_comments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_downloads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
|
||||
// Legacy binary IP is kept for existing analytics/tests compatibility.
|
||||
$table->binary('ip', 16)->nullable();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->text('referer')->nullable();
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('created_at');
|
||||
$table->index(['artwork_id', 'created_at'], 'idx_downloads_artwork');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_downloads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('artworks') && Schema::hasTable('artwork_category')) {
|
||||
Schema::table('artwork_category', function (Blueprint $table) {
|
||||
$table->foreign('artwork_id')
|
||||
->references('id')
|
||||
->on('artworks')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('artwork_category')) {
|
||||
try {
|
||||
Schema::table('artwork_category', function (Blueprint $table) {
|
||||
$table->dropForeign(['artwork_id']);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
// Foreign key does not exist or cannot be dropped; ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('category_translations', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('category_translations', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->index('slug');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('categories', function (Blueprint $table) {
|
||||
$table->dropIndex(['slug']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
// Add username (nullable for legacy), unique
|
||||
if (!Schema::hasColumn('users', 'username')) {
|
||||
$table->string('username', 80)->nullable()->unique()->after('id');
|
||||
}
|
||||
|
||||
// Ensure name exists and is nullable
|
||||
if (!Schema::hasColumn('users', 'name')) {
|
||||
$table->string('name')->nullable();
|
||||
} else {
|
||||
$table->string('name')->nullable()->change();
|
||||
}
|
||||
|
||||
// Email nullable to allow legacy nulls/dupes handling in import
|
||||
if (Schema::hasColumn('users', 'email')) {
|
||||
$table->string('email')->nullable()->change();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'is_active')) {
|
||||
$table->boolean('is_active')->default(true)->after('remember_token');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'needs_password_reset')) {
|
||||
$table->boolean('needs_password_reset')->default(true)->after('is_active');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'role')) {
|
||||
$table->string('role', 32)->default('user')->after('needs_password_reset');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'legacy_password_algo')) {
|
||||
$table->string('legacy_password_algo', 32)->nullable()->after('role');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('users', 'last_visit_at')) {
|
||||
$table->timestamp('last_visit_at')->nullable()->after('legacy_password_algo');
|
||||
}
|
||||
|
||||
// Soft deletes for user accounts
|
||||
if (!Schema::hasColumn('users', 'deleted_at')) {
|
||||
$table->softDeletes()->after('last_visit_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'username')) $table->dropColumn('username');
|
||||
if (Schema::hasColumn('users', 'is_active')) $table->dropColumn('is_active');
|
||||
if (Schema::hasColumn('users', 'needs_password_reset')) $table->dropColumn('needs_password_reset');
|
||||
if (Schema::hasColumn('users', 'role')) $table->dropColumn('role');
|
||||
if (Schema::hasColumn('users', 'legacy_password_algo')) $table->dropColumn('legacy_password_algo');
|
||||
if (Schema::hasColumn('users', 'last_visit_at')) $table->dropColumn('last_visit_at');
|
||||
if (Schema::hasColumn('users', 'deleted_at')) $table->dropColumn('deleted_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
|
||||
$table->text('bio')->nullable();
|
||||
$table->string('avatar', 255)->nullable();
|
||||
$table->string('cover_image', 255)->nullable();
|
||||
|
||||
$table->string('country', 80)->nullable();
|
||||
$table->char('country_code', 2)->nullable();
|
||||
$table->string('language', 10)->nullable();
|
||||
|
||||
$table->date('birthdate')->nullable();
|
||||
$table->enum('gender', ['M','F','X'])->default('X');
|
||||
|
||||
$table->string('website', 255)->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_profiles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_social_links', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
|
||||
$table->string('platform', 32);
|
||||
$table->string('url', 255);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'platform']);
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_social_links');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_statistics', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
|
||||
$table->unsignedInteger('uploads')->default(0);
|
||||
$table->unsignedInteger('downloads')->default(0);
|
||||
$table->unsignedInteger('pageviews')->default(0);
|
||||
$table->unsignedInteger('awards')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_statistics');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('users', 'deleted_at')) {
|
||||
$table->softDeletes()->after('last_visit_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'deleted_at')) {
|
||||
$table->dropColumn('deleted_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateArtworkFeaturesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('artwork_features', function (Blueprint $table) {
|
||||
// Production-ready settings
|
||||
$table->engine = 'InnoDB';
|
||||
$table->charset = 'utf8mb4';
|
||||
$table->collation = 'utf8mb4_unicode_ci';
|
||||
|
||||
$table->bigIncrements('id');
|
||||
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
|
||||
$table->dateTime('featured_at')->nullable(false);
|
||||
$table->dateTime('expires_at')->nullable();
|
||||
|
||||
$table->unsignedSmallInteger('priority')->default(100);
|
||||
|
||||
$table->string('label', 100)->nullable();
|
||||
$table->text('note')->nullable();
|
||||
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Indexes
|
||||
$table->index(['is_active', 'featured_at', 'expires_at'], 'idx_features_active');
|
||||
$table->index(['is_active', 'priority', 'featured_at'], 'idx_features_priority');
|
||||
$table->index('artwork_id', 'idx_features_artwork');
|
||||
$table->unique(['artwork_id', 'is_active'], 'uq_features_artwork_active');
|
||||
|
||||
// Foreign keys
|
||||
$table->foreign('artwork_id', 'fk_features_artwork')
|
||||
->references('id')->on('artworks')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('created_by', 'fk_features_user')
|
||||
->references('id')->on('users')
|
||||
->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('artwork_features', function (Blueprint $table) {
|
||||
$table->dropForeign('fk_features_artwork');
|
||||
$table->dropForeign('fk_features_user');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('artwork_features');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
$table->string('hash', 128)->nullable()->unique()->after('mime_type');
|
||||
$table->string('file_ext', 16)->nullable()->after('hash');
|
||||
$table->string('thumb_ext', 16)->nullable()->after('file_ext');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
$table->dropUnique(['hash']);
|
||||
$table->dropColumn(['hash', 'file_ext', 'thumb_ext']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
// Remove unique constraint on hash and add a non-unique index
|
||||
try {
|
||||
$table->dropUnique(['hash']);
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore if it doesn't exist
|
||||
}
|
||||
|
||||
// Add regular index for faster lookups. Wrap in try/catch since
|
||||
// checking existing indexes requires DBAL which may not be installed.
|
||||
try {
|
||||
if (Schema::hasColumn('artworks', 'hash')) {
|
||||
$table->index('hash', 'artworks_hash_index');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore index creation errors (likely already exists)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
// Restore unique constraint (if desired)
|
||||
try {
|
||||
$table->dropIndex('artworks_hash_index');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$table->unique('hash', 'artworks_hash_unique');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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('user_favorites', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['user_id', 'artwork_id'], 'uniq_user_artwork');
|
||||
$table->index('artwork_id', 'idx_artwork');
|
||||
$table->index(['user_id', 'created_at'], 'idx_user_created');
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->foreign('artwork_id')->references('id')->on('artworks')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_favorites');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateUserProfilesAvatars extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* Adds avatar metadata fields and renames legacy column if present.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->char('avatar_hash', 40)->nullable();
|
||||
}
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->dateTime('avatar_updated_at')->nullable();
|
||||
}
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->string('avatar_mime', 50)->nullable();
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to rename legacy `avatar` column to `avatar_legacy` if it exists
|
||||
if (Schema::hasColumn('user_profiles', 'avatar') && !Schema::hasColumn('user_profiles', 'avatar_legacy')) {
|
||||
// Use DB statement to avoid requiring doctrine/dbal at runtime
|
||||
try {
|
||||
DB::statement('ALTER TABLE user_profiles CHANGE COLUMN `avatar` `avatar_legacy` VARCHAR(255) NULL');
|
||||
} catch (\Exception $e) {
|
||||
// If the rename fails, we'll leave the legacy column as-is; migrations shouldn't break.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->dropColumn('avatar_hash');
|
||||
}
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->dropColumn('avatar_updated_at');
|
||||
}
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->dropColumn('avatar_mime');
|
||||
}
|
||||
});
|
||||
|
||||
// Attempt to rename back avatar_legacy to avatar
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_legacy') && !Schema::hasColumn('user_profiles', 'avatar')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE user_profiles CHANGE COLUMN `avatar_legacy` `avatar` VARCHAR(255) NULL');
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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 {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new columns if missing
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('user_profiles', 'about')) {
|
||||
$table->text('about')->nullable()->after('bio');
|
||||
}
|
||||
if (! Schema::hasColumn('user_profiles', 'signature')) {
|
||||
$table->text('signature')->nullable()->after('about');
|
||||
}
|
||||
if (! Schema::hasColumn('user_profiles', 'description')) {
|
||||
$table->text('description')->nullable()->after('signature');
|
||||
}
|
||||
});
|
||||
|
||||
// Copy existing `bio` data into `about` (safe no-op if bio missing)
|
||||
try {
|
||||
if (Schema::hasColumn('user_profiles', 'bio') && Schema::hasColumn('user_profiles', 'about')) {
|
||||
DB::statement('UPDATE `user_profiles` SET `about` = `bio` WHERE `about` IS NULL OR `about` = ""');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore copy errors
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_profiles', 'description')) {
|
||||
$table->dropColumn('description');
|
||||
}
|
||||
if (Schema::hasColumn('user_profiles', 'signature')) {
|
||||
$table->dropColumn('signature');
|
||||
}
|
||||
// keep `about` on down migration to avoid data loss; do not drop it automatically
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
<?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 {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
// Add avatar metadata to support the avatar pipeline
|
||||
if (! Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->string('avatar_hash', 64)->nullable()->after('avatar');
|
||||
}
|
||||
if (! Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->string('avatar_mime', 80)->nullable()->after('avatar_hash');
|
||||
}
|
||||
if (! Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->timestamp('avatar_updated_at')->nullable()->after('avatar_mime');
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure `user_id` is indexed/unique and add a FK to `users(id)` when possible
|
||||
try {
|
||||
// Add unique index on user_id if it doesn't exist
|
||||
DB::statement('ALTER TABLE `user_profiles` ADD UNIQUE INDEX `user_profiles_user_id_unique` (`user_id`)');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore if index exists or operation unsupported
|
||||
}
|
||||
|
||||
try {
|
||||
// Add foreign key constraint to users.id if not present
|
||||
DB::statement('ALTER TABLE `user_profiles` ADD CONSTRAINT `user_profiles_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore if users table missing or constraint already present
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->dropColumn('avatar_updated_at');
|
||||
}
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->dropColumn('avatar_mime');
|
||||
}
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->dropColumn('avatar_hash');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` DROP FOREIGN KEY `user_profiles_user_id_foreign`');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` DROP INDEX `user_profiles_user_id_unique`');
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('uploads_sessions', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('temp_path');
|
||||
$table->string('status', 32)->index();
|
||||
$table->string('ip', 64);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['user_id', 'created_at'], 'idx_uploads_sessions_user_created');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('uploads_sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('audit_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('action', 64)->index();
|
||||
$table->string('ip', 64);
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['user_id', 'created_at'], 'idx_audit_logs_user_created');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('audit_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_files', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->string('variant', 16);
|
||||
$table->string('path');
|
||||
$table->string('mime', 64);
|
||||
$table->unsignedBigInteger('size');
|
||||
|
||||
$table->primary(['artwork_id', 'variant']);
|
||||
$table->index('variant');
|
||||
$table->foreign('artwork_id')->references('id')->on('artworks')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_files');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('uploads_sessions', function (Blueprint $table) {
|
||||
$table->unsignedTinyInteger('progress')->default(0)->after('ip');
|
||||
$table->string('failure_reason', 255)->nullable()->after('progress');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('uploads_sessions', function (Blueprint $table) {
|
||||
$table->dropColumn(['progress', 'failure_reason']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tags', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('name', 64);
|
||||
$table->string('slug', 64)->unique();
|
||||
$table->unsignedBigInteger('usage_count')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('name');
|
||||
$table->index('usage_count');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_tag', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('tag_id');
|
||||
$table->enum('source', ['user', 'ai', 'system']);
|
||||
$table->float('confidence')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->primary(['artwork_id', 'tag_id']);
|
||||
$table->index('tag_id');
|
||||
|
||||
$table->foreign('artwork_id')->references('id')->on('artworks')->cascadeOnDelete();
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_tag');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('tag_synonyms', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tag_id');
|
||||
$table->string('synonym', 64);
|
||||
|
||||
$table->unique(['tag_id', 'synonym']);
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('tag_synonyms');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('uploads', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type', 32)->index(); // image, archive, etc.
|
||||
$table->string('status', 32)->default('draft')->index();
|
||||
$table->string('title')->nullable();
|
||||
$table->string('slug')->nullable()->unique();
|
||||
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||
$table->text('description')->nullable();
|
||||
$table->json('tags')->nullable();
|
||||
$table->string('license', 64)->nullable();
|
||||
$table->boolean('nsfw')->default(false);
|
||||
$table->boolean('is_scanned')->default(false)->index();
|
||||
$table->boolean('has_tags')->default(false)->index();
|
||||
$table->string('preview_path')->nullable();
|
||||
$table->timestamp('published_at')->nullable()->index();
|
||||
$table->string('final_path')->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('uploads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('upload_files', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->uuid('upload_id');
|
||||
$table->string('path');
|
||||
$table->string('type', 32)->index(); // main/screenshot/preview
|
||||
$table->string('hash', 128)->nullable()->index();
|
||||
$table->unsignedBigInteger('size')->nullable();
|
||||
$table->string('mime')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->foreign('upload_id')->references('id')->on('uploads')->cascadeOnDelete();
|
||||
$table->index(['upload_id', 'type']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('upload_files');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('upload_tags', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->uuid('upload_id');
|
||||
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
|
||||
$table->decimal('confidence', 5, 4)->nullable();
|
||||
$table->string('source', 24)->default('manual')->index(); // ai|filename|manual
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('upload_id')->references('id')->on('uploads')->cascadeOnDelete();
|
||||
$table->unique(['upload_id', 'tag_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('upload_tags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('uploads', function (Blueprint $table) {
|
||||
$table->string('processing_state', 32)
|
||||
->default('pending_scan')
|
||||
->index()
|
||||
->after('status');
|
||||
});
|
||||
|
||||
DB::table('uploads')
|
||||
->whereNull('processing_state')
|
||||
->update(['processing_state' => 'pending_scan']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('uploads', function (Blueprint $table) {
|
||||
$table->dropIndex(['processing_state']);
|
||||
$table->dropColumn('processing_state');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('uploads', function (Blueprint $table): void {
|
||||
$table->string('moderation_status', 16)->default('pending')->index();
|
||||
$table->timestamp('moderated_at')->nullable()->index();
|
||||
$table->foreignId('moderated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->text('moderation_note')->nullable();
|
||||
});
|
||||
|
||||
DB::table('uploads')
|
||||
->where('status', 'published')
|
||||
->update([
|
||||
'moderation_status' => 'approved',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('uploads')
|
||||
->where('status', 'draft')
|
||||
->update([
|
||||
'moderation_status' => 'pending',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('uploads', function (Blueprint $table): void {
|
||||
$table->dropConstrainedForeignId('moderated_by');
|
||||
$table->dropColumn('moderation_note');
|
||||
$table->dropColumn('moderated_at');
|
||||
$table->dropColumn('moderation_status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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(): void
|
||||
{
|
||||
Schema::create('artwork_embeddings', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->string('model', 64);
|
||||
$table->string('model_version', 64);
|
||||
$table->string('algo_version', 64)->default('clip-cosine-v1')->index();
|
||||
$table->unsignedSmallInteger('dim');
|
||||
$table->longText('embedding_json');
|
||||
$table->string('source_hash', 128)->nullable()->index();
|
||||
$table->boolean('is_normalized')->default(true);
|
||||
$table->timestamp('generated_at')->nullable()->index();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['artwork_id', 'model', 'model_version'], 'artwork_embeddings_artwork_model_unique');
|
||||
$table->index(['model', 'model_version'], 'artwork_embeddings_model_lookup_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_embeddings');
|
||||
}
|
||||
};
|
||||
@@ -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(): void
|
||||
{
|
||||
Schema::create('artwork_similarities', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('similar_artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->string('model', 64);
|
||||
$table->string('model_version', 64);
|
||||
$table->string('algo_version', 64)->default('clip-cosine-v1');
|
||||
$table->unsignedSmallInteger('rank');
|
||||
$table->decimal('score', 10, 7);
|
||||
$table->timestamp('generated_at')->nullable()->index();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['artwork_id', 'similar_artwork_id', 'algo_version'], 'artwork_similarities_unique_pair_algo');
|
||||
$table->index(['artwork_id', 'algo_version', 'rank', 'score'], 'artwork_similarities_page_read_idx');
|
||||
$table->index(['similar_artwork_id', 'algo_version'], 'artwork_similarities_reverse_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_similarities');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('similar_artwork_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->date('event_date')->index();
|
||||
$table->string('event_type', 16)->index(); // impression|click
|
||||
$table->string('algo_version', 64)->index();
|
||||
$table->foreignId('source_artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('similar_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
|
||||
$table->unsignedSmallInteger('position')->nullable();
|
||||
$table->unsignedSmallInteger('items_count')->nullable();
|
||||
$table->timestamp('occurred_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['event_date', 'algo_version', 'event_type'], 'similar_artwork_events_daily_idx');
|
||||
});
|
||||
|
||||
Schema::create('similar_artwork_daily_metrics', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->date('metric_date');
|
||||
$table->string('algo_version', 64);
|
||||
$table->unsignedInteger('impressions')->default(0);
|
||||
$table->unsignedInteger('clicks')->default(0);
|
||||
$table->decimal('ctr', 8, 6)->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['metric_date', 'algo_version'], 'similar_artwork_daily_metrics_unique');
|
||||
$table->index(['metric_date', 'algo_version'], 'similar_artwork_daily_metrics_lookup_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('similar_artwork_daily_metrics');
|
||||
Schema::dropIfExists('similar_artwork_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_interest_profiles', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('profile_version', 32)->default('profile-v1');
|
||||
$table->string('algo_version', 64)->default('clip-cosine-v1');
|
||||
$table->json('raw_scores_json')->nullable();
|
||||
$table->json('normalized_scores_json')->nullable();
|
||||
$table->decimal('total_weight', 14, 6)->default(0);
|
||||
$table->unsignedInteger('event_count')->default(0);
|
||||
$table->timestamp('last_event_at')->nullable()->index();
|
||||
$table->double('half_life_hours')->default(72);
|
||||
$table->uuid('updated_from_event_id')->nullable()->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'profile_version', 'algo_version'], 'user_interest_profiles_unique_key');
|
||||
$table->index(['algo_version', 'updated_at'], 'user_interest_profiles_algo_updated_idx');
|
||||
});
|
||||
|
||||
Schema::create('user_discovery_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->uuid('event_id')->unique();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||
$table->string('event_type', 16)->index(); // view|click|favorite|download
|
||||
$table->string('event_version', 32)->default('event-v1')->index();
|
||||
$table->string('algo_version', 64)->default('clip-cosine-v1')->index();
|
||||
$table->decimal('weight', 10, 4)->default(1.0);
|
||||
$table->date('event_date')->index();
|
||||
$table->timestamp('occurred_at')->nullable()->index();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'event_date', 'event_type'], 'user_discovery_events_user_date_type_idx');
|
||||
$table->index(['user_id', 'algo_version', 'occurred_at'], 'user_discovery_events_user_algo_time_idx');
|
||||
});
|
||||
|
||||
Schema::create('user_recommendation_cache', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('algo_version', 64);
|
||||
$table->string('cache_version', 32)->default('cache-v1');
|
||||
$table->json('recommendations_json')->nullable();
|
||||
$table->timestamp('generated_at')->nullable()->index();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'algo_version'], 'user_recommendation_cache_user_algo_unique');
|
||||
$table->index(['algo_version', 'expires_at'], 'user_recommendation_cache_algo_expiry_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_recommendation_cache');
|
||||
Schema::dropIfExists('user_discovery_events');
|
||||
Schema::dropIfExists('user_interest_profiles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('feed_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->date('event_date')->index();
|
||||
$table->string('event_type', 24)->index(); // feed_impression|feed_click
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->unsignedSmallInteger('position')->nullable();
|
||||
$table->string('algo_version', 64)->index();
|
||||
$table->string('source', 24)->index(); // personalized|cold_start|fallback
|
||||
$table->unsignedInteger('dwell_seconds')->nullable();
|
||||
$table->timestamp('occurred_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['event_date', 'algo_version', 'source', 'event_type'], 'feed_events_daily_agg_idx');
|
||||
$table->index(['user_id', 'artwork_id', 'event_date'], 'feed_events_user_art_date_idx');
|
||||
});
|
||||
|
||||
Schema::create('feed_daily_metrics', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->date('metric_date');
|
||||
$table->string('algo_version', 64);
|
||||
$table->string('source', 24);
|
||||
|
||||
$table->unsignedInteger('impressions')->default(0);
|
||||
$table->unsignedInteger('clicks')->default(0);
|
||||
$table->unsignedInteger('saves')->default(0);
|
||||
|
||||
$table->decimal('ctr', 8, 6)->default(0);
|
||||
$table->decimal('save_rate', 8, 6)->default(0);
|
||||
|
||||
$table->unsignedInteger('dwell_0_5')->default(0);
|
||||
$table->unsignedInteger('dwell_5_30')->default(0);
|
||||
$table->unsignedInteger('dwell_30_120')->default(0);
|
||||
$table->unsignedInteger('dwell_120_plus')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['metric_date', 'algo_version', 'source'], 'feed_daily_metrics_unique_idx');
|
||||
$table->index(['metric_date', 'algo_version', 'source'], 'feed_daily_metrics_lookup_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('feed_daily_metrics');
|
||||
Schema::dropIfExists('feed_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar') && !Schema::hasColumn('user_profiles', 'avatar_legacy')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` CHANGE COLUMN `avatar` `avatar_legacy` VARCHAR(255) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_legacy')) {
|
||||
$table->string('avatar_legacy', 255)->nullable();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->char('avatar_hash', 64)->nullable()->index();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->string('avatar_mime', 50)->nullable();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->dateTime('avatar_updated_at')->nullable();
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` MODIFY COLUMN `avatar_hash` CHAR(64) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` MODIFY COLUMN `avatar_hash` CHAR(40) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar') && !Schema::hasColumn('user_profiles', 'avatar_legacy')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` CHANGE COLUMN `avatar` `avatar_legacy` VARCHAR(255) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_legacy')) {
|
||||
$table->string('avatar_legacy', 255)->nullable();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
$table->char('avatar_hash', 64)->nullable()->index();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_mime')) {
|
||||
$table->string('avatar_mime', 50)->nullable();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('user_profiles', 'avatar_updated_at')) {
|
||||
$table->dateTime('avatar_updated_at')->nullable();
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` MODIFY COLUMN `avatar_hash` CHAR(64) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (!Schema::hasTable('user_profiles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('user_profiles', 'avatar_hash')) {
|
||||
try {
|
||||
DB::statement('ALTER TABLE `user_profiles` MODIFY COLUMN `avatar_hash` CHAR(40) NULL');
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('name', 150);
|
||||
$table->string('slug', 150)->unique();
|
||||
|
||||
$table->foreignId('parent_id')
|
||||
->nullable()
|
||||
->constrained('forum_categories')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->integer('position')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('forum_posts')) {
|
||||
Schema::create('forum_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// Added in a follow-up migration because forum_threads is created
|
||||
// later in filename order even though it shares the same timestamp.
|
||||
$table->unsignedBigInteger('thread_id');
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->longText('content');
|
||||
|
||||
$table->boolean('is_edited')->default(false);
|
||||
$table->timestamp('edited_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['thread_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_posts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_threads', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
|
||||
$table->longText('content');
|
||||
|
||||
$table->unsignedInteger('views')->default(0);
|
||||
|
||||
$table->boolean('is_locked')->default(false);
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
|
||||
$table->enum('visibility', ['public','members','staff'])->default('public');
|
||||
|
||||
$table->timestamp('last_post_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['category_id','last_post_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_threads');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::getConnection()->getDriverName() !== 'mysql') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('forum_posts') || !Schema::hasTable('forum_threads')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->foreignKeyExists('forum_posts', 'forum_posts_thread_id_foreign')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->deleteOrphanedForumPosts();
|
||||
|
||||
Schema::table('forum_posts', function (Blueprint $table) {
|
||||
$table->foreign('thread_id', 'forum_posts_thread_id_foreign')
|
||||
->references('id')
|
||||
->on('forum_threads')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::getConnection()->getDriverName() !== 'mysql') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('forum_posts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->foreignKeyExists('forum_posts', 'forum_posts_thread_id_foreign')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('forum_posts', function (Blueprint $table) {
|
||||
$table->dropForeign('forum_posts_thread_id_foreign');
|
||||
});
|
||||
}
|
||||
|
||||
private function foreignKeyExists(string $tableName, string $constraintName): bool
|
||||
{
|
||||
$connection = Schema::getConnection();
|
||||
$driver = $connection->getDriverName();
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
$databaseName = $connection->getDatabaseName();
|
||||
|
||||
return DB::table('information_schema.table_constraints')
|
||||
->where('constraint_schema', $databaseName)
|
||||
->where('table_name', $tableName)
|
||||
->where('constraint_name', $constraintName)
|
||||
->where('constraint_type', 'FOREIGN KEY')
|
||||
->exists();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function deleteOrphanedForumPosts(): void
|
||||
{
|
||||
$orphanedPostIds = DB::table('forum_posts')
|
||||
->leftJoin('forum_threads', 'forum_threads.id', '=', 'forum_posts.thread_id')
|
||||
->whereNull('forum_threads.id')
|
||||
->pluck('forum_posts.id');
|
||||
|
||||
if ($orphanedPostIds->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Schema::hasTable('forum_attachments')) {
|
||||
DB::table('forum_attachments')
|
||||
->whereIn('post_id', $orphanedPostIds)
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (Schema::hasTable('forum_post_reports')) {
|
||||
DB::table('forum_post_reports')
|
||||
->whereIn('post_id', $orphanedPostIds)
|
||||
->orWhereIn('thread_id', function ($query) {
|
||||
$query->select('forum_posts.thread_id')
|
||||
->from('forum_posts')
|
||||
->leftJoin('forum_threads', 'forum_threads.id', '=', 'forum_posts.thread_id')
|
||||
->whereNull('forum_threads.id');
|
||||
})
|
||||
->delete();
|
||||
}
|
||||
|
||||
DB::table('forum_posts')
|
||||
->whereIn('id', $orphanedPostIds)
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('post_id')->constrained('forum_posts')->cascadeOnDelete();
|
||||
|
||||
$table->string('file_path');
|
||||
$table->unsignedBigInteger('file_size');
|
||||
|
||||
$table->string('mime_type', 100)->nullable();
|
||||
|
||||
$table->unsignedInteger('width')->nullable();
|
||||
$table->unsignedInteger('height')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('post_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_attachments');
|
||||
}
|
||||
};
|
||||
@@ -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(): void
|
||||
{
|
||||
Schema::table('forum_threads', function (Blueprint $table) {
|
||||
// drop existing FK if present
|
||||
try {
|
||||
$table->dropForeign(['category_id']);
|
||||
} catch (\Exception $e) {
|
||||
// ignore if not exists
|
||||
}
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('forum_categories')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('forum_threads', function (Blueprint $table) {
|
||||
try {
|
||||
$table->dropForeign(['category_id']);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('forum_post_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained('forum_posts')->cascadeOnDelete();
|
||||
$table->foreignId('thread_id')->constrained('forum_threads')->cascadeOnDelete();
|
||||
$table->foreignId('reporter_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('status', 20)->default('open');
|
||||
$table->string('reason', 500)->nullable();
|
||||
$table->string('source_url', 1024)->nullable();
|
||||
$table->timestamp('reported_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['post_id', 'reporter_user_id'], 'forum_post_reports_unique_reporter_per_post');
|
||||
$table->index(['thread_id', 'status']);
|
||||
$table->index('reported_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('forum_post_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
//
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('users', 'username')) {
|
||||
$this->normalizeExistingUsernames();
|
||||
}
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'username')) {
|
||||
$table->string('username', 20)->nullable()->change();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'username_changed_at')) {
|
||||
$table->timestamp('username_changed_at')->nullable()->after('username');
|
||||
}
|
||||
});
|
||||
|
||||
$sm = Schema::getConnection()->getSchemaBuilder();
|
||||
$indexes = $sm->getIndexes('users');
|
||||
$hasUsernameUnique = collect($indexes)->contains(function ($index): bool {
|
||||
$columns = array_map('strtolower', (array) ($index['columns'] ?? []));
|
||||
return (bool) ($index['unique'] ?? false) && $columns === ['username'];
|
||||
});
|
||||
|
||||
if (! $hasUsernameUnique && Schema::hasColumn('users', 'username')) {
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->unique('username', 'users_username_unique');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'username_changed_at')) {
|
||||
$table->dropColumn('username_changed_at');
|
||||
}
|
||||
if (Schema::hasColumn('users', 'username')) {
|
||||
$table->string('username', 80)->nullable()->change();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function normalizeExistingUsernames(): void
|
||||
{
|
||||
$rows = DB::table('users')
|
||||
->select('id', 'username')
|
||||
->whereNotNull('username')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resolved = [];
|
||||
$used = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$raw = strtolower(trim((string) $row->username));
|
||||
$base = preg_replace('/[^a-z0-9_-]+/', '_', $raw) ?? '';
|
||||
$base = trim($base, '_-');
|
||||
if ($base === '') {
|
||||
$base = 'user' . (int) $row->id;
|
||||
}
|
||||
|
||||
$base = substr($base, 0, 20);
|
||||
if ($base === '') {
|
||||
$base = 'user';
|
||||
}
|
||||
|
||||
$candidate = $base;
|
||||
$suffix = 1;
|
||||
|
||||
while (isset($used[$candidate])) {
|
||||
$suffixValue = (string) $suffix;
|
||||
$prefixLen = max(1, 20 - strlen($suffixValue));
|
||||
$candidate = substr($base, 0, $prefixLen) . $suffixValue;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$used[$candidate] = true;
|
||||
$resolved[(int) $row->id] = $candidate;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
DB::table('users')
|
||||
->where('id', (int) $row->id)
|
||||
->update(['username' => 'tmpu' . (int) $row->id]);
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$final = $resolved[(int) $row->id] ?? null;
|
||||
if ($final === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('users')
|
||||
->where('id', (int) $row->id)
|
||||
->update(['username' => $final]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('username_history', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('old_username', 20);
|
||||
$table->timestamp('changed_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'changed_at']);
|
||||
$table->index('old_username');
|
||||
});
|
||||
|
||||
Schema::create('username_redirects', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('old_username', 20)->unique();
|
||||
$table->string('new_username', 20);
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('new_username');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('username_redirects');
|
||||
Schema::dropIfExists('username_history');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('username_approval_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('requested_username', 20);
|
||||
$table->string('context', 32)->default('unknown');
|
||||
$table->string('similar_to', 20)->nullable();
|
||||
$table->string('status', 20)->default('pending');
|
||||
$table->json('payload')->nullable();
|
||||
$table->foreignId('reviewed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->text('review_note')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['status', 'created_at']);
|
||||
$table->index(['requested_username', 'status']);
|
||||
$table->index(['user_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('username_approval_requests');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('users', 'email_verified_at')) {
|
||||
$table->timestamp('email_verified_at')->nullable()->after('email');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'onboarding_step')) {
|
||||
$table->enum('onboarding_step', ['email', 'verified', 'password', 'username', 'complete'])
|
||||
->nullable()
|
||||
->after('email_verified_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'username_changed_at')) {
|
||||
$table->timestamp('username_changed_at')->nullable()->after('username');
|
||||
}
|
||||
});
|
||||
|
||||
if (! Schema::hasTable('user_verification_tokens')) {
|
||||
Schema::create('user_verification_tokens', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('token', 128)->unique();
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'expires_at']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('user_verification_tokens')) {
|
||||
Schema::drop('user_verification_tokens');
|
||||
}
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'onboarding_step')) {
|
||||
$table->dropColumn('onboarding_step');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasColumn('users', 'username')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('users')
|
||||
->whereNotNull('username')
|
||||
->update(['username' => DB::raw('LOWER(username)')]);
|
||||
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
try {
|
||||
if ($driver === 'mysql') {
|
||||
DB::statement(
|
||||
'ALTER TABLE users ADD CONSTRAINT users_username_lowercase_check CHECK (username IS NULL OR BINARY username = LOWER(username))'
|
||||
);
|
||||
} elseif ($driver === 'pgsql') {
|
||||
DB::statement(
|
||||
'ALTER TABLE users ADD CONSTRAINT users_username_lowercase_check CHECK (username IS NULL OR username = LOWER(username))'
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if (! str_contains(strtolower($e->getMessage()), 'already exists')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
try {
|
||||
if ($driver === 'mysql') {
|
||||
DB::statement('ALTER TABLE users DROP CHECK users_username_lowercase_check');
|
||||
} elseif ($driver === 'pgsql') {
|
||||
DB::statement('ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_lowercase_check');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if (! str_contains(strtolower($e->getMessage()), 'check') && ! str_contains(strtolower($e->getMessage()), 'constraint')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('users', 'last_verification_sent_at')) {
|
||||
$table->timestamp('last_verification_sent_at')->nullable()->after('email_verified_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'verification_send_count_24h')) {
|
||||
$table->unsignedInteger('verification_send_count_24h')->default(0)->after('last_verification_sent_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('users', 'verification_send_window_started_at')) {
|
||||
$table->timestamp('verification_send_window_started_at')->nullable()->after('verification_send_count_24h');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('users', 'verification_send_window_started_at')) {
|
||||
$table->dropColumn('verification_send_window_started_at');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('users', 'verification_send_count_24h')) {
|
||||
$table->dropColumn('verification_send_count_24h');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('users', 'last_verification_sent_at')) {
|
||||
$table->dropColumn('last_verification_sent_at');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('email_send_events')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('email_send_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('type', 64);
|
||||
$table->string('email');
|
||||
$table->string('ip', 45)->nullable();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('status', 32);
|
||||
$table->string('reason', 64)->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('email');
|
||||
$table->index('ip');
|
||||
$table->index(['type', 'status']);
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('email_send_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('system_email_quota')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('system_email_quota', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('period', 7)->unique();
|
||||
$table->unsignedInteger('sent_count')->default(0);
|
||||
$table->unsignedInteger('limit_count');
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('system_email_quota');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip rename on SQLite (in-memory tests) — SQLite doesn't support CHANGE syntax.
|
||||
try {
|
||||
$driver = DB::connection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
} catch (\Throwable $e) {
|
||||
$driver = null;
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use raw statement to avoid requiring doctrine/dbal for simple rename
|
||||
// Adjust column definition to match original migration (VARCHAR(128) NOT NULL)
|
||||
DB::statement("ALTER TABLE `user_verification_tokens` CHANGE `token` `token_hash` VARCHAR(128) NOT NULL");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$driver = DB::connection()->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
} catch (\Throwable $e) {
|
||||
$driver = null;
|
||||
}
|
||||
|
||||
if ($driver === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::statement("ALTER TABLE `user_verification_tokens` CHANGE `token_hash` `token` VARCHAR(128) NOT NULL");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('user_verification_tokens')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('user_verification_tokens', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('token_hash', 128)->unique();
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'expires_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Intentionally no-op to avoid dropping an existing tokens table
|
||||
// that may have been created by earlier migrations.
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_likes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'artwork_id'], 'artwork_likes_unique_user_artwork');
|
||||
$table->index('artwork_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_likes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_reports', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('reporter_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->text('reason')->nullable();
|
||||
$table->timestamp('reported_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['artwork_id', 'reporter_user_id'], 'artwork_reports_unique_reporter_per_artwork');
|
||||
$table->index('reported_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_awards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->enum('medal', ['gold', 'silver', 'bronze']);
|
||||
$table->tinyInteger('weight')->unsigned()->default(1);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['artwork_id', 'user_id']);
|
||||
$table->index('artwork_id');
|
||||
$table->index('user_id');
|
||||
|
||||
$table->foreign('artwork_id')
|
||||
->references('id')->on('artworks')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('id')->on('users')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_awards');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_followers', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
|
||||
// The user being followed
|
||||
$table->unsignedBigInteger('user_id');
|
||||
// The follower
|
||||
$table->unsignedBigInteger('follower_id');
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['user_id', 'follower_id'], 'uq_user_follower');
|
||||
$table->index('user_id', 'idx_uf_user');
|
||||
$table->index('follower_id', 'idx_uf_follower');
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
$table->foreign('follower_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_followers');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_award_stats', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('artwork_id')->primary();
|
||||
$table->unsignedInteger('gold_count')->default(0);
|
||||
$table->unsignedInteger('silver_count')->default(0);
|
||||
$table->unsignedInteger('bronze_count')->default(0);
|
||||
$table->unsignedInteger('score_total')->default(0);
|
||||
$table->timestamp('updated_at')->nullable();
|
||||
|
||||
$table->foreign('artwork_id')
|
||||
->references('id')->on('artworks')
|
||||
->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_award_stats');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('profile_comments', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
|
||||
// Profile owner (who received the comment)
|
||||
$table->unsignedBigInteger('profile_user_id');
|
||||
// Who wrote the comment
|
||||
$table->unsignedBigInteger('author_user_id');
|
||||
|
||||
$table->text('body');
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('profile_user_id', 'idx_pc_profile');
|
||||
$table->index('author_user_id', 'idx_pc_author');
|
||||
$table->index(['profile_user_id', 'is_active', 'created_at'], 'idx_pc_active_feed');
|
||||
|
||||
$table->foreign('profile_user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
$table->foreign('author_user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('profile_comments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
// Tracks the original legacy comment_id for idempotent imports.
|
||||
// NULL for comments created natively in the new system.
|
||||
$table->unsignedInteger('legacy_id')->nullable()->unique()->after('id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->dropUnique(['legacy_id']);
|
||||
$table->dropColumn('legacy_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('user_statistics', 'profile_views')) {
|
||||
$table->unsignedInteger('profile_views')->default(0)->after('awards');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_statistics', 'profile_views')) {
|
||||
$table->dropColumn('profile_views');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
// raw_content stores user-submitted markdown/plain text
|
||||
// rendered_content stores the sanitized HTML cache
|
||||
$table->mediumText('raw_content')->nullable()->after('content');
|
||||
$table->mediumText('rendered_content')->nullable()->after('raw_content');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->dropColumn(['raw_content', 'rendered_content']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Allowed reaction slugs → emoji mapping (authoritative list lives in ReactionType).
|
||||
* Stored as VARCHAR to avoid MySQL ENUM emoji encoding issues.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_reactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
// slug: thumbs_up | heart | fire | laugh | clap | wow
|
||||
$table->string('reaction', 20);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['artwork_id', 'user_id', 'reaction'], 'artwork_reactions_unique');
|
||||
$table->index('artwork_id');
|
||||
$table->index('user_id');
|
||||
|
||||
$table->foreign('artwork_id')->references('id')->on('artworks')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::create('comment_reactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('comment_id');
|
||||
$table->unsignedBigInteger('user_id');
|
||||
// slug: thumbs_up | heart | fire | laugh | clap | wow
|
||||
$table->string('reaction', 20);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['comment_id', 'user_id', 'reaction'], 'comment_reactions_unique');
|
||||
$table->index('comment_id');
|
||||
$table->index('user_id');
|
||||
|
||||
$table->foreign('comment_id')->references('id')->on('artwork_comments')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('comment_reactions');
|
||||
Schema::dropIfExists('artwork_reactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Upgrade legacy TEXT columns to MEDIUMTEXT so large imported comments
|
||||
* (incl. spam-heavy legacy rows) do not cause truncation errors.
|
||||
*
|
||||
* TEXT = 65,535 bytes (~64 KB)
|
||||
* MEDIUMTEXT = 16,777,215 bytes (~16 MB)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->mediumText('content')->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('forum_posts', function (Blueprint $table) {
|
||||
$table->mediumText('content')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->text('content')->nullable()->change();
|
||||
});
|
||||
|
||||
Schema::table('forum_posts', function (Blueprint $table) {
|
||||
$table->text('content')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Modernized replacement for the legacy `favourites` table.
|
||||
*
|
||||
* What changed from the legacy schema:
|
||||
* - Renamed table: favourites → artwork_favourites
|
||||
* - `favourite_id` → `id` (bigint unsigned, auto-increment)
|
||||
* - `datum` → `created_at` / `updated_at` via timestamps()
|
||||
* - `user_type` dropped — membership tier is not a property of the
|
||||
* favourite relationship; query via users.role if needed
|
||||
* - `author_id` dropped — always derivable via artworks.user_id
|
||||
* - Both FKs are constrained with cascadeOnDelete so orphaned rows are
|
||||
* automatically cleaned up when an artwork or user is hard-deleted
|
||||
* - `legacy_id` tracks the original favourite_id for idempotent re-imports
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_favourites', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreignId('artwork_id')
|
||||
->constrained('artworks')
|
||||
->cascadeOnDelete();
|
||||
|
||||
// Preserve original legacy PK for idempotent re-imports.
|
||||
// NULL for favourites created natively in the new system.
|
||||
$table->unsignedInteger('legacy_id')->nullable()->unique();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Prevent duplicate favourites
|
||||
$table->unique(['user_id', 'artwork_id'], 'artwork_favourites_unique_user_artwork');
|
||||
|
||||
// Fast lookup: "how many favourites does this artwork have?"
|
||||
$table->index('artwork_id');
|
||||
// Fast lookup: "which artworks has this user favourited?"
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_favourites');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Consolidate onto artwork_favourites.
|
||||
*
|
||||
* Any rows in the interim user_favorites table (created 2026-02-07) that are
|
||||
* not already present in artwork_favourites are copied over, then
|
||||
* user_favorites is dropped so only one favourites table remains.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user_favorites')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy any rows not yet in artwork_favourites, using insertOrIgnore so the
|
||||
// unique (user_id, artwork_id) constraint silently skips duplicates.
|
||||
// chunk() avoids memory spikes on large tables.
|
||||
DB::table('user_favorites')->orderBy('id')->chunk(500, function ($rows) {
|
||||
DB::table('artwork_favourites')->insertOrIgnore(
|
||||
$rows->map(fn ($r) => [
|
||||
'user_id' => $r->user_id,
|
||||
'artwork_id' => $r->artwork_id,
|
||||
'created_at' => $r->created_at,
|
||||
'updated_at' => $r->created_at,
|
||||
])->all()
|
||||
);
|
||||
});
|
||||
|
||||
Schema::drop('user_favorites');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('user_favorites')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('user_favorites', function ($table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
$table->unique(['user_id', 'artwork_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
$table->unsignedInteger('followers_count')->default(0)->after('profile_views');
|
||||
$table->unsignedInteger('following_count')->default(0)->after('followers_count');
|
||||
});
|
||||
|
||||
// Backfill follow counters using subquery syntax (compatible with MySQL + SQLite).
|
||||
DB::statement("
|
||||
UPDATE user_statistics
|
||||
SET followers_count = (
|
||||
SELECT COUNT(*) FROM user_followers
|
||||
WHERE user_followers.user_id = user_statistics.user_id
|
||||
)
|
||||
");
|
||||
|
||||
DB::statement("
|
||||
UPDATE user_statistics
|
||||
SET following_count = (
|
||||
SELECT COUNT(*) FROM user_followers
|
||||
WHERE user_followers.follower_id = user_statistics.user_id
|
||||
)
|
||||
");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
$table->dropColumn(['followers_count', 'following_count']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* User Statistics v2 – in-place schema upgrade.
|
||||
*
|
||||
* Renames legacy columns, widens all counters to unsignedBigInteger,
|
||||
* and adds creator-received metrics + activity timestamps.
|
||||
*
|
||||
* Mapping:
|
||||
* uploads → uploads_count
|
||||
* downloads → downloads_received_count
|
||||
* pageviews → artwork_views_received_count
|
||||
* awards → awards_received_count
|
||||
* profile_views → profile_views_count
|
||||
*
|
||||
* New columns added:
|
||||
* favorites_received_count, comments_received_count,
|
||||
* reactions_received_count, last_upload_at, last_active_at
|
||||
*
|
||||
* followers_count / following_count already exist with correct naming – only
|
||||
* widened to bigint.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// ── 1. Rename legacy columns ─────────────────────────────────────────
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_statistics', 'uploads')) {
|
||||
$table->renameColumn('uploads', 'uploads_count');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'downloads')) {
|
||||
$table->renameColumn('downloads', 'downloads_received_count');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'pageviews')) {
|
||||
$table->renameColumn('pageviews', 'artwork_views_received_count');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'awards')) {
|
||||
$table->renameColumn('awards', 'awards_received_count');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'profile_views')) {
|
||||
$table->renameColumn('profile_views', 'profile_views_count');
|
||||
}
|
||||
});
|
||||
|
||||
// ── 2. Widen to unsignedBigInteger + add new columns ─────────────────
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
// Widen existing counters
|
||||
$table->unsignedBigInteger('uploads_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('downloads_received_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('artwork_views_received_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('awards_received_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('profile_views_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('followers_count')->default(0)->change();
|
||||
$table->unsignedBigInteger('following_count')->default(0)->change();
|
||||
|
||||
// Add new creator-received counters
|
||||
if (! Schema::hasColumn('user_statistics', 'favorites_received_count')) {
|
||||
$table->unsignedBigInteger('favorites_received_count')->default(0)->after('awards_received_count');
|
||||
}
|
||||
if (! Schema::hasColumn('user_statistics', 'comments_received_count')) {
|
||||
$table->unsignedBigInteger('comments_received_count')->default(0)->after('favorites_received_count');
|
||||
}
|
||||
if (! Schema::hasColumn('user_statistics', 'reactions_received_count')) {
|
||||
$table->unsignedBigInteger('reactions_received_count')->default(0)->after('comments_received_count');
|
||||
}
|
||||
|
||||
// Activity timestamps
|
||||
if (! Schema::hasColumn('user_statistics', 'last_upload_at')) {
|
||||
$table->timestamp('last_upload_at')->nullable()->after('reactions_received_count');
|
||||
}
|
||||
if (! Schema::hasColumn('user_statistics', 'last_active_at')) {
|
||||
$table->timestamp('last_active_at')->nullable()->after('last_upload_at');
|
||||
}
|
||||
});
|
||||
|
||||
// ── 3. Optional: indexes for creator ranking ─────────────────────────
|
||||
try {
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
$table->index('awards_received_count', 'idx_us_awards');
|
||||
});
|
||||
} catch (\Throwable) {}
|
||||
|
||||
try {
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
$table->index('favorites_received_count', 'idx_us_favorites');
|
||||
});
|
||||
} catch (\Throwable) {}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Remove added columns
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
foreach (['favorites_received_count', 'comments_received_count', 'reactions_received_count', 'last_upload_at', 'last_active_at'] as $col) {
|
||||
if (Schema::hasColumn('user_statistics', $col)) {
|
||||
$table->dropColumn($col);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Drop indexes
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
try { $table->dropIndex('idx_us_awards'); } catch (\Throwable) {}
|
||||
try { $table->dropIndex('idx_us_favorites'); } catch (\Throwable) {}
|
||||
});
|
||||
|
||||
// Rename back
|
||||
Schema::table('user_statistics', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('user_statistics', 'uploads_count')) {
|
||||
$table->renameColumn('uploads_count', 'uploads');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'downloads_received_count')) {
|
||||
$table->renameColumn('downloads_received_count', 'downloads');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'artwork_views_received_count')) {
|
||||
$table->renameColumn('artwork_views_received_count', 'pageviews');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'awards_received_count')) {
|
||||
$table->renameColumn('awards_received_count', 'awards');
|
||||
}
|
||||
if (Schema::hasColumn('user_statistics', 'profile_views_count')) {
|
||||
$table->renameColumn('profile_views_count', 'profile_views');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('conversations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->enum('type', ['direct', 'group'])->default('direct');
|
||||
$table->string('title')->nullable();
|
||||
$table->unsignedBigInteger('created_by');
|
||||
$table->timestamp('last_message_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('created_by')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('conversations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('conversation_participants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->enum('role', ['member', 'admin'])->default('member');
|
||||
$table->timestamp('last_read_at')->nullable();
|
||||
$table->boolean('is_muted')->default(false);
|
||||
$table->boolean('is_archived')->default(false);
|
||||
$table->timestamp('joined_at')->useCurrent();
|
||||
$table->timestamp('left_at')->nullable();
|
||||
|
||||
$table->unique(['conversation_id', 'user_id']);
|
||||
$table->index('user_id');
|
||||
$table->index('conversation_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('conversation_participants');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('conversation_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('sender_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->mediumText('body');
|
||||
$table->timestamp('edited_at')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['conversation_id', 'created_at']);
|
||||
$table->index('sender_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('messages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('message_reactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('message_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->string('reaction', 32);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->unique(['message_id', 'user_id', 'reaction']);
|
||||
$table->index('message_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('message_reactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->enum('allow_messages_from', ['everyone', 'followers', 'mutual_followers', 'nobody'])
|
||||
->default('everyone')
|
||||
->after('role');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('allow_messages_from');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('notifications')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('notifications', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type', 32);
|
||||
$table->json('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index('read_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('conversation_participants', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('conversation_participants', 'is_pinned')) {
|
||||
$table->boolean('is_pinned')->default(false)->after('is_archived');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('conversation_participants', 'pinned_at')) {
|
||||
$table->timestamp('pinned_at')->nullable()->after('is_pinned');
|
||||
}
|
||||
|
||||
$table->index(['user_id', 'is_pinned']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('conversation_participants', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('conversation_participants', 'pinned_at')) {
|
||||
$table->dropColumn('pinned_at');
|
||||
}
|
||||
if (Schema::hasColumn('conversation_participants', 'is_pinned')) {
|
||||
$table->dropColumn('is_pinned');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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(): void
|
||||
{
|
||||
Schema::create('message_attachments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('message_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->enum('type', ['image', 'file']);
|
||||
$table->string('mime', 191);
|
||||
$table->unsignedBigInteger('size_bytes');
|
||||
$table->unsignedInteger('width')->nullable();
|
||||
$table->unsignedInteger('height')->nullable();
|
||||
$table->string('sha256', 64)->nullable();
|
||||
$table->string('original_name', 255);
|
||||
$table->string('storage_path', 500);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('message_id');
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('message_attachments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('reports')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('reports', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('reporter_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->enum('target_type', ['message', 'conversation', 'user']);
|
||||
$table->unsignedBigInteger('target_id');
|
||||
$table->string('reason', 120);
|
||||
$table->text('details')->nullable();
|
||||
$table->enum('status', ['open', 'reviewing', 'closed'])->default('open');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['target_type', 'target_id']);
|
||||
$table->index('status');
|
||||
$table->index('reporter_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('message_reactions', function (Blueprint $table): void {
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('message_reactions', function (Blueprint $table): void {
|
||||
$table->dropIndex(['user_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
$table->float('trending_score_24h', 10, 4)->default(0)->after('is_approved')->index();
|
||||
$table->float('trending_score_7d', 10, 4)->default(0)->after('trending_score_24h')->index();
|
||||
$table->timestamp('last_trending_calculated_at')->nullable()->after('trending_score_7d');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
$table->dropIndex(['trending_score_24h']);
|
||||
$table->dropIndex(['trending_score_7d']);
|
||||
$table->dropColumn(['trending_score_24h', 'trending_score_7d', 'last_trending_calculated_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Adds sliding-window view and download counters to artwork_stats.
|
||||
*
|
||||
* These columns accumulate between scheduled resets:
|
||||
* views_24h — incremented on every view event; zeroed nightly at 03:30
|
||||
* views_7d — incremented on every view event; zeroed weekly on Monday 03:30
|
||||
* downloads_24h — recomputed nightly from artwork_downloads log (accurate)
|
||||
* downloads_7d — recomputed weekly from artwork_downloads log (accurate)
|
||||
*
|
||||
* TrendingService uses these instead of the all-time views/downloads totals.
|
||||
*/
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_stats', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('views_24h')->default(0)->after('views');
|
||||
$table->unsignedBigInteger('views_7d')->default(0)->after('views_24h');
|
||||
$table->unsignedBigInteger('downloads_24h')->default(0)->after('downloads');
|
||||
$table->unsignedBigInteger('downloads_7d')->default(0)->after('downloads_24h');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_stats', function (Blueprint $table) {
|
||||
$table->dropColumn(['views_24h', 'views_7d', 'downloads_24h', 'downloads_7d']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Unified activity feed events table.
|
||||
*
|
||||
* Event types: upload | comment | favorite | award | follow
|
||||
* target_type: artwork | user
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('actor_id')->index();
|
||||
$table->string('type', 20)->index(); // upload|comment|favorite|award|follow
|
||||
$table->string('target_type', 20)->index(); // artwork|user
|
||||
$table->unsignedBigInteger('target_id')->index();
|
||||
$table->json('meta')->nullable(); // extra context (category, tag, etc.)
|
||||
$table->timestamp('created_at')->useCurrent()->index();
|
||||
|
||||
// Composite indexes for feed queries
|
||||
$table->index(['type', 'created_at'], 'activity_events_type_created_idx');
|
||||
$table->index(['actor_id', 'created_at'], 'activity_events_actor_created_idx');
|
||||
$table->index(['target_type', 'target_id'], 'activity_events_target_idx');
|
||||
|
||||
$table->foreign('actor_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activity_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Persistent view event log.
|
||||
*
|
||||
* Stores one row per view — authenticated users get a user_id, guests are
|
||||
* recorded with user_id = null. Enables:
|
||||
* - "Recently viewed" per user
|
||||
* - Exact windowed counts (replayable)
|
||||
* - Unique-viewer counts per artwork
|
||||
*
|
||||
* Rows older than 90 days are pruned by the weekly
|
||||
* skinbase:prune-view-events command.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_view_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('user_id')->nullable(); // null = guest
|
||||
|
||||
$table->timestamp('viewed_at')->useCurrent();
|
||||
|
||||
// Windowed aggregate queries: COUNT(*) WHERE artwork_id=? AND viewed_at>=?
|
||||
$table->index(['artwork_id', 'viewed_at']);
|
||||
// Per-user history: recent artworks viewed by a user
|
||||
$table->index(['user_id', 'viewed_at']);
|
||||
// Pruning: DELETE WHERE viewed_at < cutoff
|
||||
$table->index('viewed_at');
|
||||
|
||||
$table->foreign('artwork_id')
|
||||
->references('id')->on('artworks')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('id')->on('users')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_view_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Phase 1 recommendation engine: user preference cache table.
|
||||
*
|
||||
* Stores pre-computed tag/category/creator affinity profiles so that
|
||||
* the "For You" feed can be generated without heavy joins on every request.
|
||||
*
|
||||
* TTL enforcement: the application checks `updated_at` + config TTL to decide
|
||||
* whether to rebuild. A background job (or on-demand compute) refreshes stale rows.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_reco_profiles', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('user_id')->primary();
|
||||
$table->json('top_tags_json')->nullable()->comment('Top tag slugs ordered by weighted score (up to 20)');
|
||||
$table->json('top_categories_json')->nullable()->comment('Top category slugs ordered by weight (up to 5)');
|
||||
$table->json('followed_creator_ids_json')->nullable()->comment('Followed creator IDs (up to 50)');
|
||||
$table->json('tag_weights_json')->nullable()->comment('Normalised tag slug → weight map');
|
||||
$table->json('category_weights_json')->nullable()->comment('Normalised category slug → weight map');
|
||||
$table->json('disliked_tag_ids_json')->nullable()->comment('Hidden/blocked tag slugs (future use)');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->index('updated_at', 'urp_updated_at_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_reco_profiles');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Materialised ranking score table.
|
||||
*
|
||||
* Stores three pre-computed scores per artwork:
|
||||
* score_trending — time-decayed engagement (HL=72h)
|
||||
* score_new_hot — short novelty boost (HL=36h, first 48h emphasis)
|
||||
* score_best — slow-decay evergreen (HL=720h)
|
||||
*
|
||||
* Rebuilt hourly by RankComputeArtworkScoresJob.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rank_artwork_scores', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('artwork_id')->primary();
|
||||
$table->foreign('artwork_id')->references('id')->on('artworks')->cascadeOnDelete();
|
||||
|
||||
$table->double('score_trending', 10, 6)->default(0)->index();
|
||||
$table->double('score_new_hot', 10, 6)->default(0)->index();
|
||||
$table->double('score_best', 10, 6)->default(0)->index();
|
||||
|
||||
$table->string('model_version', 32)->default('rank_v1')->index();
|
||||
$table->timestamp('computed_at')->nullable()->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rank_artwork_scores');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Ranked list cache table.
|
||||
*
|
||||
* Stores ordered artwork_id JSON arrays for each feed surface:
|
||||
* scope_type : global | category | content_type
|
||||
* scope_id : 0 for global, foreign-key id for category / content_type
|
||||
* list_type : trending | new_hot | best
|
||||
*
|
||||
* Rebuilt hourly by RankBuildListsJob.
|
||||
* scope_id uses 0 as sentinel for "global" (avoids nullable unique-key pitfalls in MySQL).
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rank_lists', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
|
||||
$table->string('scope_type', 32)->index(); // global | category | content_type
|
||||
$table->unsignedBigInteger('scope_id')->default(0)->index(); // 0 = global
|
||||
$table->string('list_type', 32)->index(); // trending | new_hot | best
|
||||
$table->string('model_version', 32)->default('rank_v1');
|
||||
|
||||
$table->json('artwork_ids'); // ordered list of ids
|
||||
$table->timestamp('computed_at')->nullable();
|
||||
|
||||
$table->unique(
|
||||
['scope_type', 'scope_id', 'list_type', 'model_version'],
|
||||
'rank_lists_scope_unique'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rank_lists');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('parent_id')->nullable()->after('user_id');
|
||||
$table->foreign('parent_id')->references('id')->on('artwork_comments')->onDelete('cascade');
|
||||
$table->index('parent_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_comments', function (Blueprint $table) {
|
||||
$table->dropForeign(['parent_id']);
|
||||
$table->dropIndex(['parent_id']);
|
||||
$table->dropColumn('parent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artwork_shares', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('artwork_id');
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->string('platform', 32); // facebook, twitter, pinterest, copy, email, embed
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('artwork_id');
|
||||
$table->index('platform');
|
||||
$table->index(['artwork_id', 'platform']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_shares');
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user