Release v2.0.0: SVG QR Code Designer, visit counter buffering, Geo-IP and browser language tracking, dashboard redesign

This commit is contained in:
Bartłomiej Janczak
2026-06-03 13:48:24 +02:00
parent f3623df340
commit 26247fd94a
42 changed files with 2615 additions and 430 deletions

View File

@@ -44,24 +44,85 @@ class SyncBufferedCountersCommand extends Command
$dirtyIds = array_unique(array_filter($dirtyIds));
$processed = 0;
$updatesToMake = [];
foreach ($dirtyIds as $id) {
$totalKey = "{$prefix}total:{$id}";
$uniqueKey = "{$prefix}unique:{$id}";
$qrKey = "{$prefix}qr:{$id}";
$totalDelta = (int) Cache::pull($totalKey, 0);
$uniqueDelta = (int) Cache::pull($uniqueKey, 0);
$qrDelta = (int) Cache::pull($qrKey, 0);
if ($totalDelta > 0 || $uniqueDelta > 0) {
// Perform a single atomic update query for this URL
ShortUrl::where('id', $id)->update([
'total_visits' => DB::raw("total_visits + {$totalDelta}"),
'unique_visits' => DB::raw("unique_visits + {$uniqueDelta}"),
]);
$processed++;
if ($totalDelta > 0 || $uniqueDelta > 0 || $qrDelta > 0) {
$updatesToMake[$id] = [
'total' => $totalDelta,
'unique' => $uniqueDelta,
'qr' => $qrDelta,
];
}
}
if (empty($updatesToMake)) {
$this->info('No buffered counters to synchronize.');
return 0;
}
try {
DB::transaction(function () use ($updatesToMake, &$processed) {
$shortUrls = ShortUrl::whereIn('id', array_keys($updatesToMake))->get(['id', 'url_key']);
foreach ($updatesToMake as $id => $deltas) {
ShortUrl::where('id', $id)->update([
'total_visits' => DB::raw("total_visits + {$deltas['total']}"),
'unique_visits' => DB::raw("unique_visits + {$deltas['unique']}"),
'qr_scans' => DB::raw("qr_scans + {$deltas['qr']}"),
]);
$processed++;
}
foreach ($shortUrls as $url) {
Cache::forget("filament-short-url:{$url->url_key}");
}
});
} catch (\Throwable $e) {
// Restore pulled values in cache so no clicks are lost
foreach ($updatesToMake as $id => $deltas) {
$totalKey = "{$prefix}total:{$id}";
$uniqueKey = "{$prefix}unique:{$id}";
$qrKey = "{$prefix}qr:{$id}";
if ($deltas['total'] > 0) {
Cache::increment($totalKey, $deltas['total']);
}
if ($deltas['unique'] > 0) {
Cache::increment($uniqueKey, $deltas['unique']);
}
if ($deltas['qr'] > 0) {
Cache::increment($qrKey, $deltas['qr']);
}
}
// Put the IDs back into the dirty list
if (Cache::getDefaultDriver() === 'redis' && class_exists(Redis::class)) {
Redis::sadd($dirtyKey, ...array_keys($updatesToMake));
} else {
$lock = Cache::lock("{$prefix}dirty_ids_lock", 2);
$lock->get(function () use ($prefix, $updatesToMake) {
$cachedDirty = Cache::get("{$prefix}dirty_ids", []);
if (! is_array($cachedDirty)) {
$cachedDirty = [];
}
$merged = array_unique(array_merge($cachedDirty, array_keys($updatesToMake)));
Cache::forever("{$prefix}dirty_ids", $merged);
});
}
throw $e;
}
$this->info("Successfully synchronized counters for {$processed} short URLs.");
return 0;