Implement creator studio and upload updates
This commit is contained in:
@@ -13,11 +13,12 @@ class ArtworkFactory extends Factory
|
||||
public function definition(): array
|
||||
{
|
||||
$title = $this->faker->unique()->sentence(3);
|
||||
$slug = Str::slug($title);
|
||||
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'title' => $title,
|
||||
'slug' => Str::slug($title) . '-' . $this->faker->unique()->randomNumber(5),
|
||||
'slug' => $slug !== '' ? $slug : 'artwork',
|
||||
'description' => $this->faker->paragraph(),
|
||||
'file_name' => 'image.jpg',
|
||||
'file_path' => 'uploads/artworks/image.jpg',
|
||||
|
||||
@@ -12,7 +12,7 @@ return new class extends Migration {
|
||||
$table->foreignId('user_id')->index();
|
||||
|
||||
$table->string('title', 150);
|
||||
$table->string('slug', 160)->unique();
|
||||
$table->string('slug', 160)->index();
|
||||
$table->text('description')->nullable();
|
||||
|
||||
$table->string('file_name');
|
||||
|
||||
@@ -13,20 +13,22 @@ return new class extends Migration
|
||||
{
|
||||
if (!Schema::hasTable('forum_posts')) {
|
||||
Schema::create('forum_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('thread_id')->constrained('forum_threads')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
// 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->longText('content');
|
||||
|
||||
$table->boolean('is_edited')->default(false);
|
||||
$table->timestamp('edited_at')->nullable();
|
||||
$table->boolean('is_edited')->default(false);
|
||||
$table->timestamp('edited_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['thread_id','created_at']);
|
||||
$table->index(['thread_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -8,24 +8,26 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('blog_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug', 191)->unique();
|
||||
$table->string('title', 255);
|
||||
$table->mediumText('body');
|
||||
$table->text('excerpt')->nullable();
|
||||
$table->foreignId('author_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->string('featured_image', 500)->nullable();
|
||||
$table->string('meta_title', 255)->nullable();
|
||||
$table->text('meta_description')->nullable();
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
if (! Schema::hasTable('blog_posts')) {
|
||||
Schema::create('blog_posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('slug', 191)->unique();
|
||||
$table->string('title', 255);
|
||||
$table->mediumText('body');
|
||||
$table->text('excerpt')->nullable();
|
||||
$table->foreignId('author_id')->nullable()
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->string('featured_image', 500)->nullable();
|
||||
$table->string('meta_title', 255)->nullable();
|
||||
$table->text('meta_description')->nullable();
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['is_published', 'published_at']);
|
||||
});
|
||||
$table->index(['is_published', 'published_at']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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::table('nova_cards', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('nova_cards', 'scheduled_for')) {
|
||||
$table->timestamp('scheduled_for')->nullable()->after('published_at');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('nova_cards', 'scheduling_timezone')) {
|
||||
$table->string('scheduling_timezone', 64)->nullable()->after('scheduled_for');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('nova_cards', function (Blueprint $table): void {
|
||||
$table->index(['status', 'scheduled_for'], 'nova_cards_status_scheduled_for_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('nova_cards', function (Blueprint $table): void {
|
||||
$table->dropIndex('nova_cards_status_scheduled_for_idx');
|
||||
|
||||
if (Schema::hasColumn('nova_cards', 'scheduling_timezone')) {
|
||||
$table->dropColumn('scheduling_timezone');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('nova_cards', 'scheduled_for')) {
|
||||
$table->dropColumn('scheduled_for');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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
|
||||
{
|
||||
if (! Schema::hasTable('dashboard_preferences')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('dashboard_preferences', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('dashboard_preferences', 'studio_preferences')) {
|
||||
$table->json('studio_preferences')->nullable()->after('pinned_spaces');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('dashboard_preferences')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('dashboard_preferences', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('dashboard_preferences', 'studio_preferences')) {
|
||||
$table->dropColumn('studio_preferences');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('content_moderation_findings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('content_type', 50)->index(); // artwork_comment, artwork_description
|
||||
$table->unsignedBigInteger('content_id')->index(); // id of the source record
|
||||
$table->unsignedBigInteger('artwork_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index();
|
||||
$table->string('status', 30)->default('pending')->index(); // pending, reviewed_safe, confirmed_spam, ignored, resolved
|
||||
$table->string('severity', 20)->default('low')->index(); // low, medium, high, critical
|
||||
$table->unsignedSmallInteger('score')->default(0);
|
||||
$table->string('content_hash', 64); // sha256 of normalised content
|
||||
$table->string('scanner_version', 20)->default('1.0');
|
||||
$table->json('reasons_json')->nullable();
|
||||
$table->json('matched_links_json')->nullable();
|
||||
$table->json('matched_domains_json')->nullable();
|
||||
$table->json('matched_keywords_json')->nullable();
|
||||
$table->text('content_snapshot')->nullable();
|
||||
$table->unsignedBigInteger('reviewed_by')->nullable();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->string('action_taken', 50)->nullable();
|
||||
$table->text('admin_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Prevent duplicate findings for unchanged content
|
||||
$table->unique(['content_type', 'content_id', 'content_hash'], 'cmf_content_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('content_moderation_findings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->string('content_hash_normalized', 64)->nullable()->after('content_hash');
|
||||
$table->string('group_key', 128)->nullable()->after('content_hash_normalized')->index();
|
||||
$table->json('rule_hits_json')->nullable()->after('matched_keywords_json');
|
||||
$table->json('domain_ids_json')->nullable()->after('rule_hits_json');
|
||||
$table->integer('user_risk_score')->nullable()->after('domain_ids_json');
|
||||
$table->boolean('is_auto_hidden')->default(false)->after('user_risk_score')->index();
|
||||
$table->string('auto_action_taken', 50)->nullable()->after('is_auto_hidden');
|
||||
$table->timestamp('auto_hidden_at')->nullable()->after('auto_action_taken');
|
||||
$table->unsignedBigInteger('resolved_by')->nullable()->after('reviewed_by');
|
||||
$table->timestamp('resolved_at')->nullable()->after('reviewed_at');
|
||||
$table->unsignedBigInteger('restored_by')->nullable()->after('resolved_at');
|
||||
$table->timestamp('restored_at')->nullable()->after('restored_by');
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_domains', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('domain', 255)->unique();
|
||||
$table->string('status', 20)->default('neutral')->index();
|
||||
$table->unsignedInteger('times_seen')->default(0);
|
||||
$table->unsignedInteger('times_flagged')->default(0);
|
||||
$table->unsignedInteger('times_confirmed_spam')->default(0);
|
||||
$table->timestamp('first_seen_at')->nullable();
|
||||
$table->timestamp('last_seen_at')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_rules', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('type', 40)->index();
|
||||
$table->text('value');
|
||||
$table->boolean('enabled')->default(true)->index();
|
||||
$table->integer('weight')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_action_logs', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('finding_id')->nullable()->index();
|
||||
$table->string('target_type', 50)->index();
|
||||
$table->unsignedBigInteger('target_id')->nullable()->index();
|
||||
$table->string('action_type', 50)->index();
|
||||
$table->string('actor_type', 20)->default('system')->index();
|
||||
$table->unsignedBigInteger('actor_id')->nullable()->index();
|
||||
$table->string('old_status', 30)->nullable();
|
||||
$table->string('new_status', 30)->nullable();
|
||||
$table->string('old_visibility', 30)->nullable();
|
||||
$table->string('new_visibility', 30)->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
|
||||
DB::table('content_moderation_findings')
|
||||
->update([
|
||||
'content_hash_normalized' => DB::raw('content_hash'),
|
||||
'group_key' => DB::raw('content_hash'),
|
||||
'resolved_at' => DB::raw("CASE WHEN status = 'resolved' THEN reviewed_at ELSE NULL END"),
|
||||
'resolved_by' => DB::raw("CASE WHEN status = 'resolved' THEN reviewed_by ELSE NULL END"),
|
||||
]);
|
||||
|
||||
Schema::table('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->dropUnique('cmf_content_unique');
|
||||
$table->unique(['content_type', 'content_id', 'content_hash', 'scanner_version'], 'cmf_content_scanner_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->dropUnique('cmf_content_scanner_unique');
|
||||
$table->unique(['content_type', 'content_id', 'content_hash'], 'cmf_content_unique');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('content_moderation_action_logs');
|
||||
Schema::dropIfExists('content_moderation_rules');
|
||||
Schema::dropIfExists('content_moderation_domains');
|
||||
|
||||
Schema::table('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'content_hash_normalized',
|
||||
'group_key',
|
||||
'rule_hits_json',
|
||||
'domain_ids_json',
|
||||
'user_risk_score',
|
||||
'is_auto_hidden',
|
||||
'auto_action_taken',
|
||||
'auto_hidden_at',
|
||||
'resolved_by',
|
||||
'resolved_at',
|
||||
'restored_by',
|
||||
'restored_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('artworks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DB::getDriverName() === 'sqlite') {
|
||||
DB::statement('DROP INDEX IF EXISTS artworks_slug_unique');
|
||||
DB::statement('DROP INDEX IF EXISTS artworks_slug');
|
||||
DB::statement('CREATE INDEX IF NOT EXISTS artworks_slug_index ON artworks (slug)');
|
||||
|
||||
$this->normalizeArtworkSlugs();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
if ($this->indexExists('artworks', 'artworks_slug_unique')) {
|
||||
$table->dropUnique('artworks_slug_unique');
|
||||
}
|
||||
|
||||
if (! $this->indexExists('artworks', 'artworks_slug_index') && ! $this->indexExists('artworks', 'artworks_slug')) {
|
||||
$table->index('slug');
|
||||
}
|
||||
});
|
||||
|
||||
$this->normalizeArtworkSlugs();
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('artworks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DB::getDriverName() === 'sqlite') {
|
||||
DB::statement('DROP INDEX IF EXISTS artworks_slug_index');
|
||||
DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS artworks_slug_unique ON artworks (slug)');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
if ($this->indexExists('artworks', 'artworks_slug_index')) {
|
||||
$table->dropIndex('artworks_slug_index');
|
||||
} elseif ($this->indexExists('artworks', 'artworks_slug')) {
|
||||
$table->dropIndex('artworks_slug');
|
||||
}
|
||||
|
||||
if (! $this->indexExists('artworks', 'artworks_slug_unique')) {
|
||||
$table->unique('slug');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function indexExists(string $tableName, string $indexName): bool
|
||||
{
|
||||
$driver = DB::getDriverName();
|
||||
|
||||
if ($driver === 'mysql') {
|
||||
return DB::table('information_schema.statistics')
|
||||
->where('table_schema', DB::getDatabaseName())
|
||||
->where('table_name', $tableName)
|
||||
->where('index_name', $indexName)
|
||||
->exists();
|
||||
}
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
return DB::table('pg_indexes')
|
||||
->where('schemaname', 'public')
|
||||
->where('tablename', $tableName)
|
||||
->where('indexname', $indexName)
|
||||
->exists();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function normalizeArtworkSlugs(): void
|
||||
{
|
||||
DB::table('artworks')
|
||||
->select(['id', 'title'])
|
||||
->orderBy('id')
|
||||
->chunkById(500, function (Collection $artworks): void {
|
||||
foreach ($artworks as $artwork) {
|
||||
$slug = Str::limit(Str::slug((string) ($artwork->title ?? '')) ?: 'artwork', 160, '');
|
||||
|
||||
DB::table('artworks')
|
||||
->where('id', $artwork->id)
|
||||
->update(['slug' => $slug]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->string('content_target_type', 60)->nullable()->after('content_id')->index();
|
||||
$table->unsignedBigInteger('content_target_id')->nullable()->after('content_target_type')->index();
|
||||
$table->string('campaign_key', 80)->nullable()->after('group_key')->index();
|
||||
$table->unsignedInteger('cluster_score')->nullable()->after('campaign_key');
|
||||
$table->string('cluster_reason', 80)->nullable()->after('cluster_score');
|
||||
$table->unsignedInteger('priority_score')->default(0)->after('cluster_reason')->index();
|
||||
$table->string('ai_provider', 60)->nullable()->after('priority_score');
|
||||
$table->string('ai_label', 60)->nullable()->after('ai_provider')->index();
|
||||
$table->string('ai_suggested_action', 60)->nullable()->after('ai_label');
|
||||
$table->unsignedTinyInteger('ai_confidence')->nullable()->after('ai_suggested_action');
|
||||
$table->text('ai_explanation')->nullable()->after('ai_confidence');
|
||||
$table->unsignedInteger('false_positive_count')->default(0)->after('ai_explanation');
|
||||
$table->boolean('is_false_positive')->default(false)->after('false_positive_count')->index();
|
||||
$table->string('policy_name', 80)->nullable()->after('is_false_positive')->index();
|
||||
$table->string('review_bucket', 40)->nullable()->after('policy_name')->index();
|
||||
$table->string('escalation_status', 40)->default('none')->after('review_bucket')->index();
|
||||
$table->json('score_breakdown_json')->nullable()->after('rule_hits_json');
|
||||
});
|
||||
|
||||
Schema::table('content_moderation_domains', function (Blueprint $table): void {
|
||||
$table->unsignedInteger('linked_users_count')->default(0)->after('times_confirmed_spam');
|
||||
$table->unsignedInteger('linked_findings_count')->default(0)->after('linked_users_count');
|
||||
$table->unsignedInteger('linked_clusters_count')->default(0)->after('linked_findings_count');
|
||||
$table->json('top_keywords_json')->nullable()->after('last_seen_at');
|
||||
$table->json('top_content_types_json')->nullable()->after('top_keywords_json');
|
||||
$table->unsignedInteger('false_positive_count')->default(0)->after('top_content_types_json');
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_clusters', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('campaign_key', 80)->unique();
|
||||
$table->string('cluster_reason', 80)->nullable();
|
||||
$table->string('review_bucket', 40)->nullable()->index();
|
||||
$table->string('escalation_status', 40)->default('none')->index();
|
||||
$table->unsignedInteger('cluster_score')->default(0)->index();
|
||||
$table->unsignedInteger('findings_count')->default(0);
|
||||
$table->unsignedInteger('unique_users_count')->default(0);
|
||||
$table->unsignedInteger('unique_domains_count')->default(0);
|
||||
$table->timestamp('latest_finding_at')->nullable()->index();
|
||||
$table->json('summary_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_ai_suggestions', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('finding_id')->index();
|
||||
$table->string('provider', 60)->index();
|
||||
$table->string('suggested_label', 60)->nullable()->index();
|
||||
$table->string('suggested_action', 60)->nullable();
|
||||
$table->unsignedTinyInteger('confidence')->nullable();
|
||||
$table->text('explanation')->nullable();
|
||||
$table->json('campaign_tags_json')->nullable();
|
||||
$table->json('raw_response_json')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
|
||||
Schema::create('content_moderation_feedback', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('finding_id')->index();
|
||||
$table->string('feedback_type', 60)->index();
|
||||
$table->unsignedBigInteger('actor_id')->nullable()->index();
|
||||
$table->text('notes')->nullable();
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
});
|
||||
|
||||
DB::table('content_moderation_findings')->update([
|
||||
'content_target_type' => DB::raw("CASE WHEN content_type = 'artwork_comment' THEN 'artwork_comment' ELSE 'artwork' END"),
|
||||
'content_target_id' => DB::raw("CASE WHEN content_type = 'artwork_comment' THEN content_id ELSE COALESCE(artwork_id, content_id) END"),
|
||||
'campaign_key' => DB::raw('group_key'),
|
||||
'cluster_score' => DB::raw('score'),
|
||||
'cluster_reason' => 'legacy_group_key',
|
||||
'priority_score' => DB::raw('score'),
|
||||
'policy_name' => 'default',
|
||||
'review_bucket' => DB::raw("CASE WHEN score >= 90 THEN 'urgent' WHEN score >= 60 THEN 'high' WHEN score >= 30 THEN 'priority' ELSE 'standard' END"),
|
||||
'escalation_status' => DB::raw("CASE WHEN score >= 90 THEN 'urgent' WHEN score >= 60 THEN 'escalated' WHEN score >= 30 THEN 'review_required' ELSE 'none' END"),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('content_moderation_feedback');
|
||||
Schema::dropIfExists('content_moderation_ai_suggestions');
|
||||
Schema::dropIfExists('content_moderation_clusters');
|
||||
|
||||
Schema::table('content_moderation_domains', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'linked_users_count',
|
||||
'linked_findings_count',
|
||||
'linked_clusters_count',
|
||||
'top_keywords_json',
|
||||
'top_content_types_json',
|
||||
'false_positive_count',
|
||||
]);
|
||||
});
|
||||
|
||||
Schema::table('content_moderation_findings', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'content_target_type',
|
||||
'content_target_id',
|
||||
'campaign_key',
|
||||
'cluster_score',
|
||||
'cluster_reason',
|
||||
'priority_score',
|
||||
'ai_provider',
|
||||
'ai_label',
|
||||
'ai_suggested_action',
|
||||
'ai_confidence',
|
||||
'ai_explanation',
|
||||
'false_positive_count',
|
||||
'is_false_positive',
|
||||
'policy_name',
|
||||
'review_bucket',
|
||||
'escalation_status',
|
||||
'score_breakdown_json',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user