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:
@@ -12,6 +12,7 @@ use App\Models\WalletAddress;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\DashboardStatsService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -163,7 +164,7 @@ class DailyReportTest extends TestCase
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('devices')->where('id', $device->id)->update([
|
||||
DB::table('devices')->where('id', $device->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
@@ -172,7 +173,7 @@ class DailyReportTest extends TestCase
|
||||
'address' => 'THistDailyAddr1111111111111111111',
|
||||
'chain_type' => 'TRON',
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('wallet_addresses')->where('id', $addr->id)->update([
|
||||
DB::table('wallet_addresses')->where('id', $addr->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
@@ -182,7 +183,7 @@ class DailyReportTest extends TestCase
|
||||
]);
|
||||
$mnemonic->mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$mnemonic->save();
|
||||
\Illuminate\Support\Facades\DB::table('wallet_mnemonics')->where('id', $mnemonic->id)->update([
|
||||
DB::table('wallet_mnemonics')->where('id', $mnemonic->id)->update([
|
||||
'created_at' => $yesterday->toDateTimeString(),
|
||||
'updated_at' => $yesterday->toDateTimeString(),
|
||||
]);
|
||||
@@ -198,6 +199,8 @@ class DailyReportTest extends TestCase
|
||||
$this->assertNotNull($row);
|
||||
$this->assertSame(1, $row->uv);
|
||||
$this->assertSame(1, $row->effective_uv);
|
||||
$this->assertSame(1, $row->pv);
|
||||
$this->assertSame(1, $row->effective_pv);
|
||||
$this->assertSame(1, $row->devices);
|
||||
$this->assertSame(1, $row->wallet_devices);
|
||||
$this->assertSame(1, $row->address_count);
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\DailyStat;
|
||||
use App\Models\Device;
|
||||
use App\Models\PageVisit;
|
||||
use App\Services\DashboardStatsService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DashboardStatsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const CHANNEL = 'ffffffffffffffffffffffffffffffff';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config([
|
||||
'coruna.panel.admin_hosts' => [],
|
||||
'coruna.panel.agent_hosts' => [],
|
||||
]);
|
||||
Cache::flush();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collect_uses_daily_stats_when_present(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'live-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'live-dev',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'has_wallet' => Device::WALLET_YES,
|
||||
]);
|
||||
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 99,
|
||||
'uv' => 88,
|
||||
'effective_pv' => 77,
|
||||
'effective_uv' => 66,
|
||||
'devices' => 5,
|
||||
'wallet_devices' => 3,
|
||||
'address_count' => 4,
|
||||
'mnemonic_count' => 2,
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
|
||||
$stats = app(DashboardStatsService::class)->collect(['range' => 'today']);
|
||||
$this->assertSame(99, $stats['pv']);
|
||||
$this->assertSame(88, $stats['uv']);
|
||||
$this->assertSame(77, $stats['effective_pv']);
|
||||
$this->assertSame(66, $stats['effective_uv']);
|
||||
$this->assertSame(5, $stats['total']);
|
||||
$this->assertSame(3, $stats['wallet_count']);
|
||||
$this->assertSame(4, $stats['address_count']);
|
||||
$this->assertSame(2, $stats['mnemonic_count']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collect_falls_back_to_live_visits_when_rollup_missing(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'live-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'live-uv',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$stats = app(DashboardStatsService::class)->collect(['range' => 'today']);
|
||||
$this->assertSame(2, $stats['pv']);
|
||||
$this->assertSame(1, $stats['uv']);
|
||||
$this->assertSame(2, $stats['effective_pv']);
|
||||
$this->assertSame(1, $stats['effective_uv']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collect_scans_live_when_channel_filter_set(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'mine',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'client_uid' => 'other',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 99,
|
||||
'uv' => 88,
|
||||
'effective_pv' => 77,
|
||||
'effective_uv' => 66,
|
||||
'devices' => 5,
|
||||
'wallet_devices' => 3,
|
||||
'address_count' => 0,
|
||||
'mnemonic_count' => 0,
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
|
||||
$stats = app(DashboardStatsService::class)->collect([
|
||||
'range' => 'today',
|
||||
'channel_id' => self::CHANNEL,
|
||||
'channel_exact' => true,
|
||||
]);
|
||||
$this->assertSame(1, $stats['pv']);
|
||||
$this->assertSame(1, $stats['uv']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collect_caches_result(): void
|
||||
{
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 10,
|
||||
'uv' => 9,
|
||||
'effective_pv' => 8,
|
||||
'effective_uv' => 7,
|
||||
'devices' => 1,
|
||||
'wallet_devices' => 0,
|
||||
'address_count' => 0,
|
||||
'mnemonic_count' => 0,
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
|
||||
$svc = app(DashboardStatsService::class);
|
||||
$first = $svc->collect(['range' => 'today']);
|
||||
DailyStat::query()->update(['pv' => 50, 'uv' => 40]);
|
||||
$second = $svc->collect(['range' => 'today']);
|
||||
$this->assertSame(10, $first['pv']);
|
||||
$this->assertSame(10, $second['pv']);
|
||||
$this->assertSame(9, $second['uv']);
|
||||
|
||||
Cache::flush();
|
||||
$third = $svc->collect(['range' => 'today']);
|
||||
$this->assertSame(50, $third['pv']);
|
||||
$this->assertSame(40, $third['uv']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collect_sums_daily_rows_across_range(): void
|
||||
{
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 10,
|
||||
'uv' => 4,
|
||||
'effective_pv' => 3,
|
||||
'effective_uv' => 2,
|
||||
'devices' => 1,
|
||||
'wallet_devices' => 1,
|
||||
'address_count' => 1,
|
||||
'mnemonic_count' => 1,
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->subDay()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 7,
|
||||
'uv' => 3,
|
||||
'effective_pv' => 2,
|
||||
'effective_uv' => 1,
|
||||
'devices' => 2,
|
||||
'wallet_devices' => 0,
|
||||
'address_count' => 3,
|
||||
'mnemonic_count' => 4,
|
||||
'computed_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$stats = app(DashboardStatsService::class)->collect([
|
||||
'date_from' => now()->subDay()->toDateString(),
|
||||
'date_to' => now()->toDateString(),
|
||||
]);
|
||||
$this->assertSame(17, $stats['pv']);
|
||||
$this->assertSame(7, $stats['uv']);
|
||||
$this->assertSame(5, $stats['effective_pv']);
|
||||
$this->assertSame(3, $stats['effective_uv']);
|
||||
$this->assertSame(3, $stats['total']);
|
||||
$this->assertSame(1, $stats['wallet_count']);
|
||||
$this->assertSame(4, $stats['address_count']);
|
||||
$this->assertSame(5, $stats['mnemonic_count']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_dashboard_data_uses_rollup(): void
|
||||
{
|
||||
DailyStat::query()->create([
|
||||
'stat_date' => now()->toDateString(),
|
||||
'scope_key' => DailyStat::SCOPE_ALL,
|
||||
'pv' => 12,
|
||||
'uv' => 6,
|
||||
'effective_pv' => 4,
|
||||
'effective_uv' => 3,
|
||||
'devices' => 2,
|
||||
'wallet_devices' => 1,
|
||||
'address_count' => 0,
|
||||
'mnemonic_count' => 0,
|
||||
'computed_at' => now(),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.dashboard.data', ['range' => 'today']))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.pv', 12)
|
||||
->assertJsonPath('data.uv', 6)
|
||||
->assertJsonPath('data.total', 2);
|
||||
}
|
||||
}
|
||||
@@ -698,4 +698,48 @@ class PageVisitTest extends TestCase
|
||||
->assertJsonPath('data.1.stage', 'pe')
|
||||
->assertJsonPath('data.1.stage_label', '权限提升');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_data_defaults_to_today(): void
|
||||
{
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'today-uid',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now(),
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => self::CHANNEL,
|
||||
'client_uid' => 'old-uid',
|
||||
'os' => 'iOS',
|
||||
'os_version' => '16.6',
|
||||
'browser' => 'Safari',
|
||||
'created_at' => now()->subDays(2),
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$payload = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.visits.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('count', 1)
|
||||
->json('data');
|
||||
|
||||
$this->assertCount(1, $payload);
|
||||
$this->assertSame('today-uid', $payload[0]['client_uid']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function visits_page_defaults_range_to_today(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.visits.index'))
|
||||
->assertOk()
|
||||
->assertSee('value="today" selected', false)
|
||||
->assertSee("range: 'today'", false)
|
||||
->assertDontSee("range: '30d'", false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user