update
This commit is contained in:
228
app/Services/AchievementService.php
Normal file
228
app/Services/AchievementService.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Achievement;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Story;
|
||||
use App\Models\User;
|
||||
use App\Models\UserAchievement;
|
||||
use App\Notifications\AchievementUnlockedNotification;
|
||||
use App\Services\Posts\PostAchievementService;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AchievementService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly XPService $xp,
|
||||
private readonly PostAchievementService $achievementPosts,
|
||||
) {}
|
||||
|
||||
public function checkAchievements(User|int $user): array
|
||||
{
|
||||
$currentUser = $this->resolveUser($user);
|
||||
$unlocked = [];
|
||||
|
||||
foreach ($this->unlockableDefinitions($currentUser) as $achievement) {
|
||||
if ($this->unlockAchievement($currentUser, $achievement)) {
|
||||
$unlocked[] = $achievement->slug;
|
||||
}
|
||||
}
|
||||
|
||||
$this->forgetSummaryCache((int) $currentUser->id);
|
||||
|
||||
return $unlocked;
|
||||
}
|
||||
|
||||
public function previewUnlocks(User|int $user): array
|
||||
{
|
||||
$currentUser = $this->resolveUser($user);
|
||||
|
||||
return $this->unlockableDefinitions($currentUser)
|
||||
->pluck('slug')
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function unlockAchievement(User|int $user, Achievement|int $achievement): bool
|
||||
{
|
||||
$currentUser = $user instanceof User ? $user : User::query()->findOrFail($user);
|
||||
$currentAchievement = $achievement instanceof Achievement
|
||||
? $achievement
|
||||
: Achievement::query()->findOrFail($achievement);
|
||||
|
||||
$inserted = false;
|
||||
|
||||
DB::transaction(function () use ($currentUser, $currentAchievement, &$inserted): void {
|
||||
$result = UserAchievement::query()->insertOrIgnore([
|
||||
'user_id' => (int) $currentUser->id,
|
||||
'achievement_id' => (int) $currentAchievement->id,
|
||||
'unlocked_at' => now(),
|
||||
]);
|
||||
|
||||
if ($result === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inserted = true;
|
||||
});
|
||||
|
||||
if (! $inserted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((int) $currentAchievement->xp_reward > 0) {
|
||||
$this->xp->addXP(
|
||||
(int) $currentUser->id,
|
||||
(int) $currentAchievement->xp_reward,
|
||||
'achievement_unlocked:' . $currentAchievement->slug,
|
||||
(int) $currentAchievement->id,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$currentUser->notify(new AchievementUnlockedNotification($currentAchievement));
|
||||
$this->achievementPosts->achievementUnlocked($currentUser, $currentAchievement);
|
||||
$this->forgetSummaryCache((int) $currentUser->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasAchievement(User|int $user, string $achievementSlug): bool
|
||||
{
|
||||
$userId = $user instanceof User ? (int) $user->id : $user;
|
||||
|
||||
return UserAchievement::query()
|
||||
->where('user_id', $userId)
|
||||
->whereHas('achievement', fn ($query) => $query->where('slug', $achievementSlug))
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function summary(User|int $user): array
|
||||
{
|
||||
$userId = $user instanceof User ? (int) $user->id : $user;
|
||||
|
||||
return Cache::remember($this->summaryCacheKey($userId), now()->addMinutes(10), function () use ($userId): array {
|
||||
$currentUser = User::query()->with('statistics')->findOrFail($userId);
|
||||
$progress = $this->progressSnapshot($currentUser);
|
||||
$unlockedMap = UserAchievement::query()
|
||||
->where('user_id', $userId)
|
||||
->get()
|
||||
->keyBy('achievement_id');
|
||||
|
||||
$items = $this->definitions()->map(function (Achievement $achievement) use ($progress, $unlockedMap): array {
|
||||
$progressValue = $this->progressValue($progress, $achievement);
|
||||
/** @var UserAchievement|null $unlocked */
|
||||
$unlocked = $unlockedMap->get($achievement->id);
|
||||
|
||||
return [
|
||||
'id' => (int) $achievement->id,
|
||||
'name' => $achievement->name,
|
||||
'slug' => $achievement->slug,
|
||||
'description' => $achievement->description,
|
||||
'icon' => $achievement->icon,
|
||||
'xp_reward' => (int) $achievement->xp_reward,
|
||||
'type' => $achievement->type,
|
||||
'condition_type' => $achievement->condition_type,
|
||||
'condition_value' => (int) $achievement->condition_value,
|
||||
'progress' => min((int) $achievement->condition_value, $progressValue),
|
||||
'progress_percent' => $achievement->condition_value > 0
|
||||
? (int) round((min((int) $achievement->condition_value, $progressValue) / (int) $achievement->condition_value) * 100)
|
||||
: 100,
|
||||
'unlocked' => $unlocked !== null,
|
||||
'unlocked_at' => $unlocked?->unlocked_at?->toIso8601String(),
|
||||
];
|
||||
});
|
||||
|
||||
return [
|
||||
'unlocked' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->values()->all(),
|
||||
'locked' => $items->where('unlocked', false)->values()->all(),
|
||||
'recent' => $items->where('unlocked', true)->sortByDesc('unlocked_at')->take(4)->values()->all(),
|
||||
'counts' => [
|
||||
'total' => $items->count(),
|
||||
'unlocked' => $items->where('unlocked', true)->count(),
|
||||
'locked' => $items->where('unlocked', false)->count(),
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function definitions()
|
||||
{
|
||||
return Cache::remember('achievements:definitions', now()->addHour(), function () {
|
||||
return Achievement::query()->orderBy('type')->orderBy('condition_value')->get();
|
||||
});
|
||||
}
|
||||
|
||||
public function forgetDefinitionsCache(): void
|
||||
{
|
||||
Cache::forget('achievements:definitions');
|
||||
}
|
||||
|
||||
private function progressValue(array $progress, Achievement $achievement): int
|
||||
{
|
||||
return (int) ($progress[$achievement->condition_type] ?? 0);
|
||||
}
|
||||
|
||||
private function resolveUser(User|int $user): User
|
||||
{
|
||||
return $user instanceof User
|
||||
? $user->loadMissing('statistics')
|
||||
: User::query()->with('statistics')->findOrFail($user);
|
||||
}
|
||||
|
||||
private function unlockableDefinitions(User $user): Collection
|
||||
{
|
||||
$progress = $this->progressSnapshot($user);
|
||||
$unlockedSlugs = $this->unlockedSlugs((int) $user->id);
|
||||
|
||||
return $this->definitions()->filter(function (Achievement $achievement) use ($progress, $unlockedSlugs): bool {
|
||||
if ($this->progressValue($progress, $achievement) < (int) $achievement->condition_value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! isset($unlockedSlugs[$achievement->slug]);
|
||||
})->values();
|
||||
}
|
||||
|
||||
private function progressSnapshot(User $user): array
|
||||
{
|
||||
return [
|
||||
'upload_count' => Artwork::query()
|
||||
->published()
|
||||
->where('user_id', $user->id)
|
||||
->count(),
|
||||
'likes_received' => (int) DB::table('artwork_likes as likes')
|
||||
->join('artworks as artworks', 'artworks.id', '=', 'likes.artwork_id')
|
||||
->where('artworks.user_id', $user->id)
|
||||
->count(),
|
||||
'followers_count' => (int) ($user->statistics?->followers_count ?? $user->followers()->count()),
|
||||
'stories_published' => Story::query()->published()->where('creator_id', $user->id)->count(),
|
||||
'level_reached' => (int) ($user->level ?? 1),
|
||||
];
|
||||
}
|
||||
|
||||
private function unlockedSlugs(int $userId): array
|
||||
{
|
||||
return UserAchievement::query()
|
||||
->where('user_id', $userId)
|
||||
->join('achievements', 'achievements.id', '=', 'user_achievements.achievement_id')
|
||||
->pluck('achievements.slug')
|
||||
->flip()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function forgetSummaryCache(int $userId): void
|
||||
{
|
||||
Cache::forget($this->summaryCacheKey($userId));
|
||||
}
|
||||
|
||||
private function summaryCacheKey(int $userId): string
|
||||
{
|
||||
return 'achievements:summary:' . $userId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user