fix: statisc
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', ''];
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user