messages implemented

This commit is contained in:
2026-02-26 21:12:32 +01:00
parent d0aefc5ddc
commit 15b7b77d20
168 changed files with 14728 additions and 6786 deletions

View File

@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Jobs\IndexUserJob;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/**
* UserStatsService single source of truth for user_statistics counters.
*
* All counter updates MUST go through this service.
* No direct increments in controllers or jobs.
*
* Design:
* - Atomic SQL increments (no read-modify-write races).
* - Negative counters are prevented at the SQL level (WHERE col > 0).
* - ensureRow() upserts the row before any counter touch.
* - recomputeUser() rebuilds all columns from authoritative tables.
*/
final class UserStatsService
{
// ─── Row management ──────────────────────────────────────────────────────
/**
* Guarantee a user_statistics row exists for the given user.
* Safe to call before every increment.
*/
public function ensureRow(int $userId): void
{
DB::table('user_statistics')->insertOrIgnore([
'user_id' => $userId,
'created_at' => now(),
'updated_at' => now(),
]);
}
// ─── Increment helpers ────────────────────────────────────────────────────
public function incrementUploads(int $userId, int $by = 1): void
{
$this->ensureRow($userId);
$this->inc($userId, 'uploads_count', $by);
$this->touchActive($userId);
$this->reindex($userId);
}
public function decrementUploads(int $userId, int $by = 1): void
{
$this->dec($userId, 'uploads_count', $by);
$this->reindex($userId);
}
public function incrementDownloadsReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'downloads_received_count', $by);
$this->reindex($creatorUserId);
}
public function incrementArtworkViewsReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'artwork_views_received_count', $by);
// Views are high-frequency do NOT reindex on every view.
}
public function incrementAwardsReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'awards_received_count', $by);
$this->reindex($creatorUserId);
}
public function decrementAwardsReceived(int $creatorUserId, int $by = 1): void
{
$this->dec($creatorUserId, 'awards_received_count', $by);
$this->reindex($creatorUserId);
}
public function incrementFavoritesReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'favorites_received_count', $by);
$this->reindex($creatorUserId);
}
public function decrementFavoritesReceived(int $creatorUserId, int $by = 1): void
{
$this->dec($creatorUserId, 'favorites_received_count', $by);
$this->reindex($creatorUserId);
}
public function incrementCommentsReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'comments_received_count', $by);
$this->reindex($creatorUserId);
}
public function decrementCommentsReceived(int $creatorUserId, int $by = 1): void
{
$this->dec($creatorUserId, 'comments_received_count', $by);
$this->reindex($creatorUserId);
}
public function incrementReactionsReceived(int $creatorUserId, int $by = 1): void
{
$this->ensureRow($creatorUserId);
$this->inc($creatorUserId, 'reactions_received_count', $by);
$this->reindex($creatorUserId);
}
public function decrementReactionsReceived(int $creatorUserId, int $by = 1): void
{
$this->dec($creatorUserId, 'reactions_received_count', $by);
$this->reindex($creatorUserId);
}
public function incrementProfileViews(int $userId, int $by = 1): void
{
$this->ensureRow($userId);
$this->inc($userId, 'profile_views_count', $by);
}
// ─── Timestamp helpers ────────────────────────────────────────────────────
public function setLastUploadAt(int $userId, ?Carbon $timestamp = null): void
{
$this->ensureRow($userId);
DB::table('user_statistics')
->where('user_id', $userId)
->update([
'last_upload_at' => ($timestamp ?? now())->toDateTimeString(),
'updated_at' => now(),
]);
}
public function setLastActiveAt(int $userId, ?Carbon $timestamp = null): void
{
$this->ensureRow($userId);
DB::table('user_statistics')
->where('user_id', $userId)
->update([
'last_active_at' => ($timestamp ?? now())->toDateTimeString(),
'updated_at' => now(),
]);
}
// ─── Recompute ────────────────────────────────────────────────────────────
/**
* Recompute all counters for a single user from authoritative tables.
* Returns the computed values (array) without writing when $dryRun=true.
*
* @return array<string, int|string|null>
*/
public function recomputeUser(int $userId, bool $dryRun = false): array
{
$computed = [
'uploads_count' => (int) DB::table('artworks')
->where('user_id', $userId)
->whereNull('deleted_at')
->count(),
'downloads_received_count' => (int) DB::table('artwork_downloads as d')
->join('artworks as a', 'a.id', '=', 'd.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->count(),
'artwork_views_received_count' => (int) DB::table('artwork_stats as s')
->join('artworks as a', 'a.id', '=', 's.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->sum('s.views'),
'awards_received_count' => (int) DB::table('artwork_awards as aw')
->join('artworks as a', 'a.id', '=', 'aw.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->count(),
'favorites_received_count' => (int) DB::table('artwork_favourites as f')
->join('artworks as a', 'a.id', '=', 'f.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->count(),
'comments_received_count' => (int) DB::table('artwork_comments as c')
->join('artworks as a', 'a.id', '=', 'c.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->whereNull('c.deleted_at')
->count(),
'reactions_received_count' => (int) DB::table('artwork_reactions as r')
->join('artworks as a', 'a.id', '=', 'r.artwork_id')
->where('a.user_id', $userId)
->whereNull('a.deleted_at')
->count(),
'followers_count' => (int) DB::table('user_followers')
->where('user_id', $userId)
->count(),
'following_count' => (int) DB::table('user_followers')
->where('follower_id', $userId)
->count(),
'last_upload_at' => DB::table('artworks')
->where('user_id', $userId)
->whereNull('deleted_at')
->max('created_at'),
];
if (! $dryRun) {
$this->ensureRow($userId);
DB::table('user_statistics')
->where('user_id', $userId)
->update(array_merge($computed, ['updated_at' => now()]));
$this->reindex($userId);
}
return $computed;
}
/**
* Recompute stats for all users in chunks.
*
* @param int $chunk Users per chunk.
*/
public function recomputeAll(int $chunk = 1000): void
{
DB::table('users')
->whereNull('deleted_at')
->orderBy('id')
->chunk($chunk, function ($users) {
foreach ($users as $user) {
$this->recomputeUser($user->id);
}
});
}
// ─── Private helpers ──────────────────────────────────────────────────────
private function inc(int $userId, string $column, int $by = 1): void
{
DB::table('user_statistics')
->where('user_id', $userId)
->update([
$column => DB::raw("MAX(0, COALESCE({$column}, 0) + {$by})"),
'updated_at' => now(),
]);
}
private function dec(int $userId, string $column, int $by = 1): void
{
DB::table('user_statistics')
->where('user_id', $userId)
->where($column, '>', 0)
->update([
$column => DB::raw("MAX(0, COALESCE({$column}, 0) - {$by})"),
'updated_at' => now(),
]);
}
private function touchActive(int $userId): void
{
DB::table('user_statistics')
->where('user_id', $userId)
->update([
'last_active_at' => now(),
'updated_at' => now(),
]);
}
/**
* Queue a Meilisearch reindex for the user.
* Uses IndexUserJob to avoid blocking the request.
*/
private function reindex(int $userId): void
{
IndexUserJob::dispatch($userId);
}
}