update
This commit is contained in:
163
app/Console/Commands/RecalculateUserXpCommand.php
Normal file
163
app/Console/Commands/RecalculateUserXpCommand.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\AchievementService;
|
||||
use App\Services\XPService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecalculateUserXpCommand extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:recalculate-user-xp
|
||||
{user_id? : The ID of a single user to recompute}
|
||||
{--all : Recompute XP and level for all non-deleted users}
|
||||
{--chunk=1000 : Chunk size when --all is used}
|
||||
{--dry-run : Show computed values without writing}
|
||||
{--sync-achievements : Re-run achievement checks after a live recalculation}';
|
||||
|
||||
protected $description = 'Rebuild stored user XP, level, and rank from user_xp_logs';
|
||||
|
||||
public function handle(XPService $xp, AchievementService $achievements): int
|
||||
{
|
||||
$userId = $this->argument('user_id');
|
||||
$all = (bool) $this->option('all');
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$syncAchievements = (bool) $this->option('sync-achievements');
|
||||
$chunk = max(1, (int) $this->option('chunk'));
|
||||
|
||||
if ($userId !== null && $all) {
|
||||
$this->error('Provide either a user_id or --all, not both.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($userId !== null) {
|
||||
return $this->recalculateSingle((int) $userId, $xp, $achievements, $dryRun, $syncAchievements);
|
||||
}
|
||||
|
||||
if ($all) {
|
||||
return $this->recalculateAll($xp, $achievements, $chunk, $dryRun, $syncAchievements);
|
||||
}
|
||||
|
||||
$this->error('Provide a user_id or use --all.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
private function recalculateSingle(
|
||||
int $userId,
|
||||
XPService $xp,
|
||||
AchievementService $achievements,
|
||||
bool $dryRun,
|
||||
bool $syncAchievements,
|
||||
): int {
|
||||
$exists = DB::table('users')->where('id', $userId)->exists();
|
||||
if (! $exists) {
|
||||
$this->error("User {$userId} not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
|
||||
$this->line("{$label} Recomputing XP for user #{$userId}...");
|
||||
|
||||
$result = $xp->recalculateStoredProgress($userId, ! $dryRun);
|
||||
$this->table(
|
||||
['Field', 'Stored', 'Computed'],
|
||||
[
|
||||
['xp', $result['previous']['xp'], $result['computed']['xp']],
|
||||
['level', $result['previous']['level'], $result['computed']['level']],
|
||||
['rank', $result['previous']['rank'], $result['computed']['rank']],
|
||||
]
|
||||
);
|
||||
|
||||
if ($dryRun) {
|
||||
if ($syncAchievements) {
|
||||
$pending = $achievements->previewUnlocks($userId);
|
||||
$this->line('Achievements preview: ' . (empty($pending) ? 'no pending unlocks' : implode(', ', $pending)));
|
||||
}
|
||||
|
||||
$this->warn('Dry-run: no changes written.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if ($syncAchievements) {
|
||||
$unlocked = $achievements->checkAchievements($userId);
|
||||
$this->line('Achievements checked: ' . (empty($unlocked) ? 'no new unlocks' : implode(', ', $unlocked)));
|
||||
}
|
||||
|
||||
$this->info($result['changed'] ? "XP updated for user #{$userId}." : "User #{$userId} was already in sync.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function recalculateAll(
|
||||
XPService $xp,
|
||||
AchievementService $achievements,
|
||||
int $chunk,
|
||||
bool $dryRun,
|
||||
bool $syncAchievements,
|
||||
): int {
|
||||
$total = DB::table('users')->whereNull('deleted_at')->count();
|
||||
$label = $dryRun ? '[DRY-RUN]' : '[LIVE]';
|
||||
|
||||
$this->info("{$label} Recomputing XP for {$total} users (chunk={$chunk})...");
|
||||
|
||||
$processed = 0;
|
||||
$changed = 0;
|
||||
$pendingAchievementUsers = 0;
|
||||
$pendingAchievementUnlocks = 0;
|
||||
$appliedAchievementUnlocks = 0;
|
||||
$bar = $this->output->createProgressBar($total);
|
||||
$bar->start();
|
||||
|
||||
DB::table('users')
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->chunkById($chunk, function ($users) use ($xp, $achievements, $dryRun, $syncAchievements, &$processed, &$changed, &$pendingAchievementUsers, &$pendingAchievementUnlocks, &$appliedAchievementUnlocks, $bar): void {
|
||||
foreach ($users as $user) {
|
||||
$result = $xp->recalculateStoredProgress((int) $user->id, ! $dryRun);
|
||||
|
||||
if ($result['changed']) {
|
||||
$changed++;
|
||||
}
|
||||
|
||||
if ($syncAchievements) {
|
||||
if ($dryRun) {
|
||||
$pending = $achievements->previewUnlocks((int) $user->id);
|
||||
if (! empty($pending)) {
|
||||
$pendingAchievementUsers++;
|
||||
$pendingAchievementUnlocks += count($pending);
|
||||
}
|
||||
} else {
|
||||
$unlocked = $achievements->checkAchievements((int) $user->id);
|
||||
$appliedAchievementUnlocks += count($unlocked);
|
||||
}
|
||||
}
|
||||
|
||||
$processed++;
|
||||
$bar->advance();
|
||||
}
|
||||
});
|
||||
|
||||
$bar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$summary = "Done - {$processed} users processed, {$changed} " . ($dryRun ? 'would change.' : 'updated.');
|
||||
if ($syncAchievements) {
|
||||
if ($dryRun) {
|
||||
$summary .= " Achievement preview: {$pendingAchievementUnlocks} pending unlock(s) across {$pendingAchievementUsers} user(s).";
|
||||
} else {
|
||||
$summary .= " Achievements re-checked: {$appliedAchievementUnlocks} unlock(s) applied.";
|
||||
}
|
||||
}
|
||||
|
||||
$this->info($summary);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user