Serve dashboard stats from daily_stats and default visit list to today.
Stop live-scanning page_visits on every dashboard load (including today) so the admin home stays fast; keep the live path only for channel filters and cache the result for 45s. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\DailyStat;
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\TransferRecord;
|
||||
@@ -11,10 +12,13 @@ use App\Models\WalletMnemonic;
|
||||
use App\Support\AgentScope;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardStatsService
|
||||
{
|
||||
public const CACHE_SECONDS = 45;
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* range?: string,
|
||||
@@ -48,8 +52,6 @@ class DashboardStatsService
|
||||
public function collect(array $filters = []): array
|
||||
{
|
||||
[$from, $to] = $this->resolveBounds($filters);
|
||||
$rangeLabel = $from->toDateString().' ~ '.$to->toDateString();
|
||||
|
||||
$channelId = trim((string) ($filters['channel_id'] ?? ''));
|
||||
$channelExact = (bool) ($filters['channel_exact'] ?? false);
|
||||
$agent = $filters['agent'] ?? null;
|
||||
@@ -57,6 +59,74 @@ class DashboardStatsService
|
||||
? $filters['agent_user_id']
|
||||
: null;
|
||||
$agentUserId = $agentUserId === null ? null : (int) $agentUserId;
|
||||
$scopeAgentId = $agent !== null ? (int) $agent->id : $agentUserId;
|
||||
|
||||
$cacheKey = 'dashboard:stats:'.md5(json_encode([
|
||||
$from->toDateTimeString(),
|
||||
$to->toDateTimeString(),
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$scopeAgentId,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_SECONDS, function () use (
|
||||
$from,
|
||||
$to,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$scopeAgentId,
|
||||
) {
|
||||
return $this->compute(
|
||||
$from,
|
||||
$to,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$scopeAgentId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer daily_stats sums (including today, up to ~15 min stale) unless a
|
||||
* channel filter cannot be served from the rollup. Missing rollup rows fall
|
||||
* back to a live page_visits scan.
|
||||
*
|
||||
* @return array{
|
||||
* total: int,
|
||||
* wallet_count: int,
|
||||
* new_count: int,
|
||||
* active_count: int,
|
||||
* pv: int,
|
||||
* uv: int,
|
||||
* effective_pv: int,
|
||||
* effective_uv: int,
|
||||
* mnemonic_count: int,
|
||||
* address_count: int,
|
||||
* balances: array{usdt: string, trx: string, eth: string, btc: string, bnb: string},
|
||||
* transfer_count: int,
|
||||
* transfers: array{usdt: string, trx: string, eth: string, btc: string},
|
||||
* range: string,
|
||||
* from: string,
|
||||
* to: string,
|
||||
* date_from: string,
|
||||
* date_to: string
|
||||
* }
|
||||
*/
|
||||
private function compute(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
string $channelId,
|
||||
bool $channelExact,
|
||||
?User $agent,
|
||||
?int $agentUserId,
|
||||
?int $scopeAgentId,
|
||||
): array {
|
||||
$rangeLabel = $from->toDateString().' ~ '.$to->toDateString();
|
||||
$rollup = $channelId === '' ? $this->rollupFromDailyStats($from, $to, $scopeAgentId) : null;
|
||||
|
||||
$base = Device::query();
|
||||
AgentScope::applyDeviceChannelScope($base, $agent);
|
||||
@@ -71,37 +141,63 @@ class DashboardStatsService
|
||||
}
|
||||
}
|
||||
|
||||
// Period cohort: devices installed (created) in the selected range.
|
||||
$createdInRange = (clone $base)->whereBetween('created_at', [$from, $to]);
|
||||
$deviceAgg = (clone $createdInRange)
|
||||
->selectRaw(
|
||||
'COUNT(*) as total, COALESCE(SUM(CASE WHEN has_wallet = ? THEN 1 ELSE 0 END), 0) as wallet_count',
|
||||
[Device::WALLET_YES],
|
||||
)
|
||||
->first();
|
||||
$total = (int) ($deviceAgg?->total ?? 0);
|
||||
$walletCount = (int) ($deviceAgg?->wallet_count ?? 0);
|
||||
$newCount = $total;
|
||||
$activeCount = (clone $base)->whereBetween('updated_at', [$from, $to])->count();
|
||||
|
||||
$mnemonicCount = $this->countDeviceChildren(
|
||||
WalletMnemonic::query(),
|
||||
'wallet_mnemonics',
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$from,
|
||||
$to,
|
||||
);
|
||||
[$addressCount, $balances] = $this->addressStats(
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$from,
|
||||
$to,
|
||||
);
|
||||
if ($rollup === null) {
|
||||
$createdInRange = (clone $base)->whereBetween('created_at', [$from, $to]);
|
||||
$deviceAgg = (clone $createdInRange)
|
||||
->selectRaw(
|
||||
'COUNT(*) as total, COALESCE(SUM(CASE WHEN has_wallet = ? THEN 1 ELSE 0 END), 0) as wallet_count',
|
||||
[Device::WALLET_YES],
|
||||
)
|
||||
->first();
|
||||
$total = (int) ($deviceAgg?->total ?? 0);
|
||||
$walletCount = (int) ($deviceAgg?->wallet_count ?? 0);
|
||||
$mnemonicCount = $this->countDeviceChildren(
|
||||
WalletMnemonic::query(),
|
||||
'wallet_mnemonics',
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$from,
|
||||
$to,
|
||||
);
|
||||
[$addressCount, $balances] = $this->addressStats(
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$from,
|
||||
$to,
|
||||
);
|
||||
[$pv, $uv, $effectivePv, $effectiveUv] = $this->liveVisitTotals(
|
||||
$from,
|
||||
$to,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$agent,
|
||||
$agentUserId,
|
||||
);
|
||||
} else {
|
||||
$total = $rollup['devices'];
|
||||
$walletCount = $rollup['wallet_devices'];
|
||||
$mnemonicCount = $rollup['mnemonic_count'];
|
||||
$addressCount = $rollup['address_count'];
|
||||
$pv = $rollup['pv'];
|
||||
$uv = $rollup['uv'];
|
||||
$effectivePv = $rollup['effective_pv'];
|
||||
$effectiveUv = $rollup['effective_uv'];
|
||||
[, $balances] = $this->addressStats(
|
||||
$agent,
|
||||
$agentUserId,
|
||||
$channelId,
|
||||
$channelExact,
|
||||
$from,
|
||||
$to,
|
||||
);
|
||||
}
|
||||
|
||||
[$transferCount, $transfers] = $this->transferStats(
|
||||
$agent,
|
||||
$agentUserId,
|
||||
@@ -111,38 +207,10 @@ class DashboardStatsService
|
||||
$to,
|
||||
);
|
||||
|
||||
$visits = PageVisit::query();
|
||||
AgentScope::applyChannelIdScope($visits, $agent);
|
||||
if ($agent === null) {
|
||||
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
if ($channelExact) {
|
||||
$visits->where('channel_id', $channelId);
|
||||
} else {
|
||||
$visits->where('channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
}
|
||||
$visits->whereBetween('created_at', [$from, $to]);
|
||||
|
||||
$effectiveSql = $this->effectiveVisitSql();
|
||||
$visitAgg = (clone $visits)
|
||||
->selectRaw(
|
||||
"COUNT(*) as pv,
|
||||
COUNT(DISTINCT client_uid) as uv,
|
||||
COALESCE(SUM(CASE WHEN ({$effectiveSql}) THEN 1 ELSE 0 END), 0) as effective_pv,
|
||||
COUNT(DISTINCT CASE WHEN ({$effectiveSql}) THEN client_uid END) as effective_uv",
|
||||
)
|
||||
->first();
|
||||
$pv = (int) ($visitAgg?->pv ?? 0);
|
||||
$uv = (int) ($visitAgg?->uv ?? 0);
|
||||
$effectivePv = (int) ($visitAgg?->effective_pv ?? 0);
|
||||
$effectiveUv = (int) ($visitAgg?->effective_uv ?? 0);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'wallet_count' => $walletCount,
|
||||
'new_count' => $newCount,
|
||||
'new_count' => $total,
|
||||
'active_count' => $activeCount,
|
||||
'pv' => $pv,
|
||||
'uv' => $uv,
|
||||
@@ -161,6 +229,88 @@ class DashboardStatsService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Day-level UV summed across the range (same as the daily report). Null when
|
||||
* this scope has no rollup rows yet.
|
||||
*
|
||||
* @return array{pv: int, uv: int, effective_pv: int, effective_uv: int, devices: int, wallet_devices: int, address_count: int, mnemonic_count: int}|null
|
||||
*/
|
||||
private function rollupFromDailyStats(Carbon $from, Carbon $to, ?int $scopeAgentId): ?array
|
||||
{
|
||||
$scope = DailyReportService::scopeKey($scopeAgentId);
|
||||
$q = DailyStat::query()
|
||||
->where('scope_key', $scope)
|
||||
->whereBetween('stat_date', [$from->toDateString(), $to->toDateString()]);
|
||||
if (! (clone $q)->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $q->selectRaw(
|
||||
'COALESCE(SUM(pv), 0) as pv,
|
||||
COALESCE(SUM(uv), 0) as uv,
|
||||
COALESCE(SUM(effective_pv), 0) as effective_pv,
|
||||
COALESCE(SUM(effective_uv), 0) as effective_uv,
|
||||
COALESCE(SUM(devices), 0) as devices,
|
||||
COALESCE(SUM(wallet_devices), 0) as wallet_devices,
|
||||
COALESCE(SUM(address_count), 0) as address_count,
|
||||
COALESCE(SUM(mnemonic_count), 0) as mnemonic_count',
|
||||
)->first();
|
||||
|
||||
return [
|
||||
'pv' => (int) ($row?->pv ?? 0),
|
||||
'uv' => (int) ($row?->uv ?? 0),
|
||||
'effective_pv' => (int) ($row?->effective_pv ?? 0),
|
||||
'effective_uv' => (int) ($row?->effective_uv ?? 0),
|
||||
'devices' => (int) ($row?->devices ?? 0),
|
||||
'wallet_devices' => (int) ($row?->wallet_devices ?? 0),
|
||||
'address_count' => (int) ($row?->address_count ?? 0),
|
||||
'mnemonic_count' => (int) ($row?->mnemonic_count ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int, 2: int, 3: int}
|
||||
*/
|
||||
private function liveVisitTotals(
|
||||
Carbon $from,
|
||||
Carbon $to,
|
||||
string $channelId,
|
||||
bool $channelExact,
|
||||
?User $agent,
|
||||
?int $agentUserId,
|
||||
): array {
|
||||
$visits = PageVisit::query();
|
||||
AgentScope::applyChannelIdScope($visits, $agent);
|
||||
if ($agent === null) {
|
||||
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
|
||||
}
|
||||
if ($channelId !== '') {
|
||||
if ($channelExact) {
|
||||
$visits->where('channel_id', $channelId);
|
||||
} else {
|
||||
$visits->where('channel_id', 'like', '%'.$channelId.'%');
|
||||
}
|
||||
}
|
||||
$visits->whereBetween('created_at', [$from, $to]);
|
||||
|
||||
$effectiveSql = $this->effectiveVisitSql();
|
||||
$visitAgg = $visits
|
||||
->selectRaw(
|
||||
"COUNT(*) as pv,
|
||||
COUNT(DISTINCT client_uid) as uv,
|
||||
COALESCE(SUM(CASE WHEN ({$effectiveSql}) THEN 1 ELSE 0 END), 0) as effective_pv,
|
||||
COUNT(DISTINCT CASE WHEN ({$effectiveSql}) THEN client_uid END) as effective_uv",
|
||||
)
|
||||
->first();
|
||||
|
||||
return [
|
||||
(int) ($visitAgg?->pv ?? 0),
|
||||
(int) ($visitAgg?->uv ?? 0),
|
||||
(int) ($visitAgg?->effective_pv ?? 0),
|
||||
(int) ($visitAgg?->effective_uv ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{range?: string, date_from?: string, date_to?: string, date_range?: string} $filters
|
||||
* @return array{0: Carbon, 1: Carbon}
|
||||
@@ -423,5 +573,4 @@ class DashboardStatsService
|
||||
|
||||
return [$major, $minor, $patch];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user