This commit is contained in:
hashbro
2026-08-27 01:28:15 +08:00
parent 3a2be309dc
commit e660335808
9 changed files with 178 additions and 11 deletions
+2 -4
View File
@@ -43,10 +43,9 @@ class PageHitController extends Controller
$parsed = UserAgentParser::parse($ua);
$referer = $this->referer($request);
PageVisit::query()->create([
PageVisit::recordLanding([
'channel_id' => $channelId,
'client_uid' => $uid,
'chain' => PageVisit::CHAIN_CORUNA,
'user_agent' => $ua !== '' ? $ua : null,
'os' => $parsed['os'],
'os_version' => $parsed['os_version'] !== '' ? $parsed['os_version'] : null,
@@ -55,8 +54,7 @@ class PageHitController extends Controller
'ip' => $ip !== '' ? $ip : null,
'domain' => $domain,
'referer' => $referer,
'created_at' => now(),
]);
], PageVisit::chainFromIosVersion($parsed['os'], $parsed['os_version']));
return $this->pixel();
}
+77
View File
@@ -41,6 +41,25 @@ class PageVisit extends Model
return $chain === self::CHAIN_DARKSWORD ? 'DarkSword' : 'Coruna';
}
/**
* t.js landings: iOS > 17.2.1 and < 18.7 count as DarkSword.
*/
public static function chainFromIosVersion(?string $os, ?string $osVersion): int
{
if (! in_array($os, ['iOS', 'iPadOS'], true)) {
return self::CHAIN_CORUNA;
}
$version = trim((string) $osVersion);
if ($version === '' || ! preg_match('/^\d+(?:\.\d+){0,3}$/', $version)) {
return self::CHAIN_CORUNA;
}
if (version_compare($version, '17.2.1', '>') && version_compare($version, '18.7', '<')) {
return self::CHAIN_DARKSWORD;
}
return self::CHAIN_CORUNA;
}
public static function normalizeDomain(?string $value): ?string
{
$value = trim((string) $value);
@@ -59,6 +78,64 @@ class PageVisit extends Model
public const VISITOR_UID_LENGTH = 16;
public const LANDING_MERGE_SECONDS = 60;
/**
* One landing = one row. t.js and DS register share this so the same
* channel+IP within {@see LANDING_MERGE_SECONDS} is merged, not doubled.
* DarkSword wins on chain; register may replace client_uid with the device UUID.
*
* @param array<string, mixed> $attrs
*/
public static function recordLanding(array $attrs, int $chain): self
{
$channel = (string) ($attrs['channel_id'] ?? '');
$ip = self::normalizeIp(isset($attrs['ip']) ? (string) $attrs['ip'] : null);
$existing = null;
if ($channel !== '' && $ip !== '') {
$existing = static::query()
->where('channel_id', $channel)
->whereIn('ip', self::ipLookupKeys($ip))
->where('created_at', '>=', now()->subSeconds(self::LANDING_MERGE_SECONDS))
->orderByDesc('id')
->first();
}
if ($existing) {
$updates = [];
if ($chain === self::CHAIN_DARKSWORD && (int) $existing->chain !== self::CHAIN_DARKSWORD) {
$updates['chain'] = self::CHAIN_DARKSWORD;
}
if ($chain === self::CHAIN_DARKSWORD) {
$uid = trim((string) ($attrs['client_uid'] ?? ''));
if ($uid !== '' && $uid !== (string) $existing->client_uid) {
$updates['client_uid'] = substr($uid, 0, 64);
}
}
foreach (['user_agent', 'os', 'os_version', 'browser', 'browser_version', 'domain', 'referer'] as $field) {
$incoming = $attrs[$field] ?? null;
if (! is_string($incoming) || trim($incoming) === '') {
continue;
}
$incoming = trim($incoming);
$current = trim((string) ($existing->{$field} ?? ''));
if ($current === '' || ($field === 'os_version' && $chain === self::CHAIN_DARKSWORD)) {
$updates[$field] = $incoming;
}
}
if ($updates !== []) {
$existing->fill($updates)->save();
}
return $existing;
}
$attrs['chain'] = $chain;
$attrs['created_at'] = $attrs['created_at'] ?? now();
return static::query()->create($attrs);
}
/**
* Stable visitor id: same domain+ip always yields the same 16-hex value.
*/
+2 -4
View File
@@ -153,10 +153,9 @@ class DarkSwordIngestAdapter
$ip = $this->clientIp($request, $payload);
$referer = trim((string) $request->headers->get('referer', ''));
PageVisit::query()->create([
PageVisit::recordLanding([
'channel_id' => $channel,
'client_uid' => $uid,
'chain' => PageVisit::CHAIN_DARKSWORD,
'user_agent' => $ua !== '' ? $ua : null,
'os' => $ios !== null ? 'iOS' : $parsed['os'],
'os_version' => $ios ?? ($parsed['os_version'] !== '' ? $parsed['os_version'] : null),
@@ -165,8 +164,7 @@ class DarkSwordIngestAdapter
'ip' => $ip !== '' ? $ip : null,
'domain' => PageVisit::normalizeDomain($request->getHost()),
'referer' => $referer !== '' ? substr($referer, 0, 512) : null,
'created_at' => now(),
]);
], PageVisit::CHAIN_DARKSWORD);
}
/**