Upload beautify
This commit is contained in:
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
29
database/migrations/2026_02_12_000001_create_tags_table.php
Normal file
29
database/migrations/2026_02_12_000001_create_tags_table.php
Normal file
@@ -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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user