*/ public function backfill(?string $dateFrom, ?string $dateTo, int $limit = 0, bool $dryRun = false): array { $query = MallDailyPush::order('create_time', 'desc')->order('id', 'desc'); if ($dateFrom !== null && $dateFrom !== '') { $query->where('date', '>=', $dateFrom); } if ($dateTo !== null && $dateTo !== '') { $query->where('date', '<=', $dateTo); } $rows = $query->select(); $seenUserIds = []; $stats = [ 'dry_run' => $dryRun, 'date_from' => $dateFrom, 'date_to' => $dateTo, 'limit' => $limit, 'scanned_rows' => 0, 'target_users' => 0, 'processed_users' => 0, 'created_users' => 0, 'updated_users' => 0, 'unchanged_users' => 0, 'failed_users' => 0, 'errors' => [], ]; foreach ($rows as $row) { $stats['scanned_rows']++; $playxUserId = trim(strval($row->user_id ?? '')); if ($playxUserId === '' || isset($seenUserIds[$playxUserId])) { continue; } $seenUserIds[$playxUserId] = true; $stats['target_users']++; if ($limit > 0 && $stats['target_users'] > $limit) { break; } $username = trim(strval($row->username ?? '')); try { $result = $this->ensureAssetForPlayx($playxUserId, $username, $dryRun); $stats['processed_users']++; if ($result['created']) { $stats['created_users']++; } elseif ($result['updated']) { $stats['updated_users']++; } else { $stats['unchanged_users']++; } } catch (Throwable $e) { $stats['failed_users']++; $stats['errors'][] = [ 'user_id' => $playxUserId, 'message' => $e->getMessage(), ]; } } return $stats; } /** * @return array{created:bool,updated:bool} */ private function ensureAssetForPlayx(string $playxUserId, string $username, bool $dryRun): array { $asset = MallUserAsset::where('playx_user_id', $playxUserId)->find(); if ($asset) { $updated = false; $uname = trim($username); if ($uname !== '' && strval($asset->username ?? '') !== $uname) { $asset->username = $uname; $updated = true; } if ($updated && !$dryRun) { $asset->save(); } return ['created' => false, 'updated' => $updated]; } $effectiveUsername = trim($username); if ($effectiveUsername === '') { $effectiveUsername = 'playx_' . $playxUserId; } $byName = MallUserAsset::where('username', $effectiveUsername)->find(); if ($byName) { $updated = strval($byName->playx_user_id ?? '') !== $playxUserId; if ($updated && !$dryRun) { $byName->playx_user_id = $playxUserId; $byName->save(); } return ['created' => false, 'updated' => $updated]; } if ($dryRun) { return ['created' => true, 'updated' => false]; } $phone = $this->buildTempPhone(); if ($phone === null) { throw new \RuntimeException('Failed to allocate phone for playx user'); } $pwd = hash_password(Random::build('alnum', 16)); $now = time(); $created = MallUserAsset::create([ 'playx_user_id' => $playxUserId, 'username' => $effectiveUsername, 'phone' => $phone, 'password' => $pwd, 'admin_id' => 0, 'locked_points' => 0, 'available_points' => 0, 'today_limit' => 0, 'today_claimed' => 0, 'today_limit_date' => null, 'create_time' => $now, 'update_time' => $now, ]); if (!$created) { throw new \RuntimeException('Failed to create mall_user_asset'); } return ['created' => true, 'updated' => false]; } private function buildTempPhone(): ?string { for ($i = 0; $i < 8; $i++) { $candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT); if (!MallUserAsset::where('phone', $candidate)->find()) { return $candidate; } } return null; } }