Save workspace changes
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Artwork;
|
||||
use App\Services\ArtworkStatsService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* POST /api/art/{id}/view
|
||||
*
|
||||
* Fire-and-forget view tracker.
|
||||
*
|
||||
* Deduplication strategy (layered):
|
||||
* 1. Session key (`art_viewed.{id}`) — prevents double-counts within the
|
||||
* same browser session (survives page reloads).
|
||||
* 2. Route throttle (5 per 10 minutes per IP+artwork) — catches bots that
|
||||
* don't send session cookies.
|
||||
*
|
||||
* The frontend should additionally guard with sessionStorage so it only
|
||||
* calls this endpoint once per page load.
|
||||
*/
|
||||
final class ArtworkViewController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArtworkStatsService $stats,
|
||||
private readonly XPService $xp,
|
||||
) {}
|
||||
|
||||
public function __invoke(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$artwork = Artwork::public()
|
||||
->published()
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (! $artwork) {
|
||||
return response()->json(['error' => 'Not found'], 404);
|
||||
}
|
||||
|
||||
$sessionKey = 'art_viewed.' . $id;
|
||||
|
||||
// Already counted this session — return early without touching the DB.
|
||||
if ($request->hasSession() && $request->session()->has($sessionKey)) {
|
||||
return response()->json(['ok' => true, 'counted' => false]);
|
||||
}
|
||||
|
||||
// Write persistent event log (auth user_id or null for guests).
|
||||
$this->stats->logViewEvent((int) $artwork->id, $request->user()?->id);
|
||||
|
||||
// Defer to Redis when available, fall back to direct DB increment.
|
||||
$this->stats->incrementViews((int) $artwork->id, 1, defer: true);
|
||||
|
||||
$viewerId = $request->user()?->id;
|
||||
if ($artwork->user_id !== null && (int) $artwork->user_id !== (int) ($viewerId ?? 0)) {
|
||||
$this->xp->awardArtworkViewReceived(
|
||||
(int) $artwork->user_id,
|
||||
(int) $artwork->id,
|
||||
$viewerId,
|
||||
(string) $request->ip(),
|
||||
);
|
||||
}
|
||||
|
||||
// Mark this session so the artwork is not counted again.
|
||||
if ($request->hasSession()) {
|
||||
$request->session()->put($sessionKey, true);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true, 'counted' => true]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user