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:
root
2026-10-11 09:46:38 +00:00
parent 11caef98fc
commit fcbc479c2e
9 changed files with 550 additions and 76 deletions
@@ -4,12 +4,12 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\PortalAware;
use App\Http\Controllers\Controller;
use App\Models\DsChainLog;
use App\Models\Device;
use App\Models\DsChainLog;
use App\Models\PageVisit;
use App\Models\User;
use App\Support\CfIpCountry;
use App\Support\AgentScope;
use App\Support\CfIpCountry;
use Carbon\Carbon;
use Illuminate\Http\Request;
@@ -154,7 +154,7 @@ class PageVisitController extends Controller
$q->where('referer', 'like', '%'.$referer.'%');
}
[$from, $to] = $this->rangeBounds((string) $request->query('range', '30d'));
[$from, $to] = $this->rangeBounds((string) $request->query('range', 'today'));
$q->whereBetween('created_at', [$from, $to]);
return $q;
+5 -1
View File
@@ -13,7 +13,9 @@ class DailyStat extends Model
protected $fillable = [
'stat_date',
'scope_key',
'pv',
'uv',
'effective_pv',
'effective_uv',
'devices',
'wallet_devices',
@@ -25,7 +27,9 @@ class DailyStat extends Model
protected function casts(): array
{
return [
'pv' => 'integer',
'uv' => 'integer',
'effective_pv' => 'integer',
'effective_uv' => 'integer',
'devices' => 'integer',
'wallet_devices' => 'integer',
@@ -34,4 +38,4 @@ class DailyStat extends Model
'computed_at' => 'datetime',
];
}
};
}
+12 -4
View File
@@ -12,6 +12,7 @@ use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
use App\Support\AgentScope;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class DailyReportService
@@ -122,12 +123,14 @@ class DailyReportService
$end = $to->copy()->startOfDay();
while ($cursor->lte($end)) {
if ($force || $cursor->gte($refreshFrom) || ! $this->dayIsFresh($cursor, $agentUserId)) {
[$uv, $effectiveUv, $devices, $walletDevices, $addressCount, $mnemonicCount] = $this->totalsForDay($cursor, $agentUserId);
[$uv, $effectiveUv, $devices, $walletDevices, $addressCount, $mnemonicCount, $pv, $effectivePv] = $this->totalsForDay($cursor, $agentUserId);
$row = $this->present($cursor->toDateString(), $uv, $effectiveUv, $devices, $walletDevices, $addressCount, $mnemonicCount);
DailyStat::query()->updateOrCreate(
['stat_date' => $row['date'], 'scope_key' => $scope],
[
'pv' => $pv,
'uv' => $row['uv'],
'effective_pv' => $effectivePv,
'effective_uv' => $row['effective_uv'],
'devices' => $row['devices'],
'wallet_devices' => $row['wallet_devices'],
@@ -233,6 +236,7 @@ class DailyReportService
->where('stat_date', $date)
->whereNotNull('computed_at')
->whereNotNull('address_count')
->whereNotNull('pv')
->exists();
if (! $statOk) {
return false;
@@ -272,7 +276,7 @@ class DailyReportService
}
/**
* @return array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int}
* @return array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int}
*/
private function totalsForDay(Carbon $day, ?int $agentUserId): array
{
@@ -285,7 +289,9 @@ class DailyReportService
$visit = $visits
->whereBetween('created_at', [$from, $to])
->selectRaw(
"COUNT(DISTINCT client_uid) as uv,
"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();
@@ -307,11 +313,13 @@ class DailyReportService
(int) ($device?->wallet_devices ?? 0),
$this->countDeviceChildrenForDay(WalletAddress::query(), 'wallet_addresses', $from, $to, $agentUserId),
$this->countDeviceChildrenForDay(WalletMnemonic::query(), 'wallet_mnemonics', $from, $to, $agentUserId),
(int) ($visit?->pv ?? 0),
(int) ($visit?->effective_pv ?? 0),
];
}
private function countDeviceChildrenForDay(
\Illuminate\Database\Eloquent\Builder $query,
Builder $query,
string $table,
Carbon $from,
Carbon $to,
+210 -61
View File
@@ -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];
}
}