207 lines
7.9 KiB
PHP
207 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Tag;
|
|
use App\Models\UserNegativeSignal;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
final class DiscoveryNegativeSignalController extends Controller
|
|
{
|
|
public function hideArtwork(Request $request): JsonResponse
|
|
{
|
|
$payload = $request->validate([
|
|
'artwork_id' => ['required', 'integer', 'exists:artworks,id'],
|
|
'algo_version' => ['nullable', 'string', 'max:64'],
|
|
'source' => ['nullable', 'string', 'max:64'],
|
|
'meta' => ['nullable', 'array'],
|
|
]);
|
|
|
|
$signal = UserNegativeSignal::query()->updateOrCreate(
|
|
[
|
|
'user_id' => (int) $request->user()->id,
|
|
'signal_type' => 'hide_artwork',
|
|
'artwork_id' => (int) $payload['artwork_id'],
|
|
],
|
|
[
|
|
'tag_id' => null,
|
|
'algo_version' => $payload['algo_version'] ?? null,
|
|
'source' => $payload['source'] ?? 'api',
|
|
'meta' => (array) ($payload['meta'] ?? []),
|
|
]
|
|
);
|
|
|
|
$this->recordFeedbackEvent(
|
|
userId: (int) $request->user()->id,
|
|
artworkId: (int) $payload['artwork_id'],
|
|
eventType: 'hide_artwork',
|
|
algoVersion: isset($payload['algo_version']) ? (string) $payload['algo_version'] : null,
|
|
meta: (array) ($payload['meta'] ?? [])
|
|
);
|
|
|
|
return response()->json([
|
|
'stored' => true,
|
|
'signal_id' => (int) $signal->id,
|
|
'signal_type' => 'hide_artwork',
|
|
], Response::HTTP_ACCEPTED);
|
|
}
|
|
|
|
public function dislikeTag(Request $request): JsonResponse
|
|
{
|
|
$payload = $request->validate([
|
|
'tag_id' => ['nullable', 'integer', 'exists:tags,id'],
|
|
'tag_slug' => ['nullable', 'string', 'max:191'],
|
|
'artwork_id' => ['nullable', 'integer', 'exists:artworks,id'],
|
|
'algo_version' => ['nullable', 'string', 'max:64'],
|
|
'source' => ['nullable', 'string', 'max:64'],
|
|
'meta' => ['nullable', 'array'],
|
|
]);
|
|
|
|
$tagId = isset($payload['tag_id']) ? (int) $payload['tag_id'] : null;
|
|
if ($tagId === null && ! empty($payload['tag_slug'])) {
|
|
$tagId = Tag::query()->where('slug', (string) $payload['tag_slug'])->value('id');
|
|
}
|
|
|
|
abort_if($tagId === null || $tagId <= 0, Response::HTTP_UNPROCESSABLE_ENTITY, 'A valid tag is required.');
|
|
|
|
$signal = UserNegativeSignal::query()->updateOrCreate(
|
|
[
|
|
'user_id' => (int) $request->user()->id,
|
|
'signal_type' => 'dislike_tag',
|
|
'tag_id' => $tagId,
|
|
],
|
|
[
|
|
'artwork_id' => null,
|
|
'algo_version' => $payload['algo_version'] ?? null,
|
|
'source' => $payload['source'] ?? 'api',
|
|
'meta' => (array) ($payload['meta'] ?? []),
|
|
]
|
|
);
|
|
|
|
$this->recordFeedbackEvent(
|
|
userId: (int) $request->user()->id,
|
|
artworkId: isset($payload['artwork_id']) ? (int) $payload['artwork_id'] : (int) (($payload['meta']['artwork_id'] ?? 0)),
|
|
eventType: 'dislike_tag',
|
|
algoVersion: isset($payload['algo_version']) ? (string) $payload['algo_version'] : null,
|
|
meta: array_merge((array) ($payload['meta'] ?? []), ['tag_id' => $tagId])
|
|
);
|
|
|
|
return response()->json([
|
|
'stored' => true,
|
|
'signal_id' => (int) $signal->id,
|
|
'signal_type' => 'dislike_tag',
|
|
'tag_id' => $tagId,
|
|
], Response::HTTP_ACCEPTED);
|
|
}
|
|
|
|
public function unhideArtwork(Request $request): JsonResponse
|
|
{
|
|
$payload = $request->validate([
|
|
'artwork_id' => ['required', 'integer', 'exists:artworks,id'],
|
|
'algo_version' => ['nullable', 'string', 'max:64'],
|
|
'meta' => ['nullable', 'array'],
|
|
]);
|
|
|
|
$deleted = UserNegativeSignal::query()
|
|
->where('user_id', (int) $request->user()->id)
|
|
->where('signal_type', 'hide_artwork')
|
|
->where('artwork_id', (int) $payload['artwork_id'])
|
|
->delete();
|
|
|
|
if ($deleted > 0) {
|
|
$this->recordFeedbackEvent(
|
|
userId: (int) $request->user()->id,
|
|
artworkId: (int) $payload['artwork_id'],
|
|
eventType: 'unhide_artwork',
|
|
algoVersion: isset($payload['algo_version']) ? (string) $payload['algo_version'] : null,
|
|
meta: (array) ($payload['meta'] ?? [])
|
|
);
|
|
}
|
|
|
|
return response()->json([
|
|
'revoked' => $deleted > 0,
|
|
'signal_type' => 'hide_artwork',
|
|
'artwork_id' => (int) $payload['artwork_id'],
|
|
], Response::HTTP_OK);
|
|
}
|
|
|
|
public function undislikeTag(Request $request): JsonResponse
|
|
{
|
|
$payload = $request->validate([
|
|
'tag_id' => ['nullable', 'integer', 'exists:tags,id'],
|
|
'tag_slug' => ['nullable', 'string', 'max:191'],
|
|
'artwork_id' => ['nullable', 'integer', 'exists:artworks,id'],
|
|
'algo_version' => ['nullable', 'string', 'max:64'],
|
|
'meta' => ['nullable', 'array'],
|
|
]);
|
|
|
|
$tagId = isset($payload['tag_id']) ? (int) $payload['tag_id'] : null;
|
|
if ($tagId === null && ! empty($payload['tag_slug'])) {
|
|
$tagId = Tag::query()->where('slug', (string) $payload['tag_slug'])->value('id');
|
|
}
|
|
|
|
abort_if($tagId === null || $tagId <= 0, Response::HTTP_UNPROCESSABLE_ENTITY, 'A valid tag is required.');
|
|
|
|
$deleted = UserNegativeSignal::query()
|
|
->where('user_id', (int) $request->user()->id)
|
|
->where('signal_type', 'dislike_tag')
|
|
->where('tag_id', $tagId)
|
|
->delete();
|
|
|
|
if ($deleted > 0) {
|
|
$this->recordFeedbackEvent(
|
|
userId: (int) $request->user()->id,
|
|
artworkId: isset($payload['artwork_id']) ? (int) $payload['artwork_id'] : (int) (($payload['meta']['artwork_id'] ?? 0)),
|
|
eventType: 'undo_dislike_tag',
|
|
algoVersion: isset($payload['algo_version']) ? (string) $payload['algo_version'] : null,
|
|
meta: array_merge((array) ($payload['meta'] ?? []), ['tag_id' => $tagId])
|
|
);
|
|
}
|
|
|
|
return response()->json([
|
|
'revoked' => $deleted > 0,
|
|
'signal_type' => 'dislike_tag',
|
|
'tag_id' => $tagId,
|
|
], Response::HTTP_OK);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $meta
|
|
*/
|
|
private function recordFeedbackEvent(int $userId, int $artworkId, string $eventType, ?string $algoVersion = null, array $meta = []): void
|
|
{
|
|
if ($artworkId <= 0 || ! Schema::hasTable('user_discovery_events')) {
|
|
return;
|
|
}
|
|
|
|
$categoryId = DB::table('artwork_category')
|
|
->where('artwork_id', $artworkId)
|
|
->orderBy('category_id')
|
|
->value('category_id');
|
|
|
|
DB::table('user_discovery_events')->insert([
|
|
'event_id' => (string) Str::uuid(),
|
|
'user_id' => $userId,
|
|
'artwork_id' => $artworkId,
|
|
'category_id' => $categoryId !== null ? (int) $categoryId : null,
|
|
'event_type' => $eventType,
|
|
'event_version' => (string) config('discovery.event_version', 'event-v1'),
|
|
'algo_version' => (string) ($algoVersion ?: config('discovery.v2.algo_version', config('discovery.algo_version', 'clip-cosine-v1'))),
|
|
'weight' => 0.0,
|
|
'event_date' => now()->toDateString(),
|
|
'occurred_at' => now()->toDateTimeString(),
|
|
'meta' => json_encode($meta, JSON_THROW_ON_ERROR),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
}
|