From 533b2bb9e4423603316e6a66300add39fc7e06cb Mon Sep 17 00:00:00 2001 From: hashbro Date: Wed, 26 Aug 2026 06:33:21 +0800 Subject: [PATCH] feat: ds --- app/Services/DashboardStatsService.php | 60 ++++++++++--------- app/Telegram/Handlers/DataCommand.php | 2 +- .../views/admin/dashboard/index.blade.php | 2 +- tests/Feature/PageVisitTest.php | 48 +++++++++++++++ 4 files changed, 83 insertions(+), 29 deletions(-) diff --git a/app/Services/DashboardStatsService.php b/app/Services/DashboardStatsService.php index 190c7c8..1298b35 100644 --- a/app/Services/DashboardStatsService.php +++ b/app/Services/DashboardStatsService.php @@ -348,7 +348,7 @@ class DashboardStatsService } /** - * Effective traffic: Safari on iOS ≤ 17.2 (includes 17.2.x patch builds; excludes 17.3+). + * Safari on iOS 13.0.0–17.2.1, plus 18.5 / 18.6 / 18.6.1 / 18.6.2. */ private function applyEffectiveFilter(Builder $query): void { @@ -357,33 +357,39 @@ class DashboardStatsService ->whereNotNull('os_version') ->where('os_version', '!=', ''); - $driver = DB::connection()->getDriverName(); - if ($driver === 'sqlite') { - // major < 17 OR (major = 17 AND minor ≤ 2) - $query->whereRaw( - "(CAST(os_version AS INTEGER) < 17) - OR ( - CAST(os_version AS INTEGER) = 17 - AND CAST( - CASE - WHEN INSTR(os_version, '.') > 0 - THEN SUBSTR(os_version, INSTR(os_version, '.') + 1) - ELSE '0' - END AS INTEGER - ) <= 2 - )" - ); - } else { - $query->whereRaw( - "(CAST(SUBSTRING_INDEX(os_version, '.', 1) AS UNSIGNED) < 17) - OR ( - CAST(SUBSTRING_INDEX(os_version, '.', 1) AS UNSIGNED) = 17 - AND CAST( - SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(os_version, '.0'), '.', 2), '.', -1) AS UNSIGNED - ) <= 2 - )" - ); + [$major, $minor, $patch] = $this->osVersionPartsSql(); + $query->whereRaw( + "( + ({$major} >= 13 AND ( + {$major} < 17 + OR ({$major} = 17 AND {$minor} < 2) + OR ({$major} = 17 AND {$minor} = 2 AND {$patch} <= 1) + )) + OR ({$major} = 18 AND {$minor} = 5 AND {$patch} = 0) + OR ({$major} = 18 AND {$minor} = 6 AND {$patch} IN (0, 1, 2)) + )" + ); + } + + /** + * @return array{0: string, 1: string, 2: string} major / minor / patch SQL + */ + private function osVersionPartsSql(): array + { + if (DB::connection()->getDriverName() === 'sqlite') { + $major = 'CAST(os_version AS INTEGER)'; + $minor = "CAST(CASE WHEN INSTR(os_version, '.') = 0 THEN '0' ELSE SUBSTR(os_version, INSTR(os_version, '.') + 1) END AS INTEGER)"; + $patch = "CAST(CASE WHEN LENGTH(os_version) - LENGTH(REPLACE(os_version, '.', '')) < 2 THEN '0' ELSE SUBSTR(os_version, INSTR(os_version, '.') + INSTR(SUBSTR(os_version, INSTR(os_version, '.') + 1), '.') + 1) END AS INTEGER)"; + + return [$major, $minor, $patch]; } + + $padded = "CONCAT(os_version, '.0.0')"; + $major = "CAST(SUBSTRING_INDEX(os_version, '.', 1) AS UNSIGNED)"; + $minor = "CAST(SUBSTRING_INDEX(SUBSTRING_INDEX({$padded}, '.', 2), '.', -1) AS UNSIGNED)"; + $patch = "CAST(SUBSTRING_INDEX(SUBSTRING_INDEX({$padded}, '.', 3), '.', -1) AS UNSIGNED)"; + + return [$major, $minor, $patch]; } /** diff --git a/app/Telegram/Handlers/DataCommand.php b/app/Telegram/Handlers/DataCommand.php index 5b3e1f0..ca12070 100644 --- a/app/Telegram/Handlers/DataCommand.php +++ b/app/Telegram/Handlers/DataCommand.php @@ -102,7 +102,7 @@ class DataCommand '🌐 页面访问', '页面 PV: '.$data['effective_pv'].' / '.$data['pv'].' (有效/全部)', '页面 UV: '.$data['effective_uv'].' / '.$data['uv'].' (有效/全部)', - '有效口径: iOS≤17.2 + Safari', + '有效口径: iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 + Safari', '', '访问 · 按系统 (UV / 占比):', ]; diff --git a/resources/views/admin/dashboard/index.blade.php b/resources/views/admin/dashboard/index.blade.php index 7f53e25..3ab104f 100644 --- a/resources/views/admin/dashboard/index.blade.php +++ b/resources/views/admin/dashboard/index.blade.php @@ -178,7 +178,7 @@ -
页面访问有效口径:iOS ≤ 17.2 且 Safari(左侧高亮)
+
页面访问有效口径:iOS 13–17.2.1 / 18.5 / 18.6 / 18.6.1 / 18.6.2 且 Safari(左侧高亮)
页面 PV
diff --git a/tests/Feature/PageVisitTest.php b/tests/Feature/PageVisitTest.php index 914e366..2e50cac 100644 --- a/tests/Feature/PageVisitTest.php +++ b/tests/Feature/PageVisitTest.php @@ -259,6 +259,54 @@ class PageVisitTest extends TestCase ->assertJsonStructure(['data' => ['by_os', 'by_ios_version', 'by_device_ios_version', 'pv', 'uv', 'effective_pv', 'effective_uv', 'total', 'wallet_count', 'balances', 'mnemonic_count', 'address_count']]); } + #[Test] + public function dashboard_effective_pv_uses_new_ios_bands(): void + { + Cache::flush(); + $rows = [ + ['12.5.7', false], + ['13.0', true], + ['17.2.1', true], + ['17.2.2', false], + ['17.3', false], + ['18.2', false], + ['18.2.1', false], + ['18.4', false], + ['18.5', true], + ['18.5.1', false], + ['18.6', true], + ['18.6.1', true], + ['18.6.2', true], + ['18.6.3', false], + ['18.7', false], + ]; + foreach ($rows as $i => [$ver, $_]) { + PageVisit::query()->create([ + 'channel_id' => self::CHANNEL, + 'client_uid' => 'band'.$i, + 'os' => 'iOS', + 'os_version' => $ver, + 'browser' => 'Safari', + 'created_at' => now(), + ]); + } + PageVisit::query()->create([ + 'channel_id' => self::CHANNEL, + 'client_uid' => 'chrome185', + 'os' => 'iOS', + 'os_version' => '18.5', + 'browser' => 'Chrome', + 'created_at' => now(), + ]); + + $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); + $this->actingAs($admin, 'admin') + ->getJson(route('admin.dashboard.data', ['range' => 'today'])) + ->assertOk() + ->assertJsonPath('data.pv', count($rows) + 1) + ->assertJsonPath('data.effective_pv', count(array_filter($rows, static fn ($r) => $r[1]))); + } + #[Test] public function visits_data_labels_chain(): void {