|
|
@@ -0,0 +1,919 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Http\Controllers\Admin;
|
|
|
+
|
|
|
+use App\Http\Controllers\Controller;
|
|
|
+use App\Models\AccountsInfo;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+
|
|
|
+class AccountCookieController extends Controller
|
|
|
+{
|
|
|
+ public function index(Request $request)
|
|
|
+ {
|
|
|
+ $filters = $this->buildFilters($request);
|
|
|
+
|
|
|
+ $cookieSubQuery = DB::connection('read')->raw("
|
|
|
+ (
|
|
|
+ SELECT *,
|
|
|
+ ROW_NUMBER() OVER (
|
|
|
+ PARTITION BY
|
|
|
+ ISNULL(CAST(UserID AS VARCHAR(50)), '')
|
|
|
+ ORDER BY CreateTime DESC, ID DESC
|
|
|
+ ) AS rn
|
|
|
+ FROM QPAccountsDB.dbo.AccountCookie
|
|
|
+ ) ac
|
|
|
+ ");
|
|
|
+
|
|
|
+ $query = DB::connection('read')
|
|
|
+ ->table($cookieSubQuery)
|
|
|
+ ->leftJoin(AccountsInfo::TABLE . ' as ai', 'ai.UserID', '=', 'ac.UserID')
|
|
|
+ ->select([
|
|
|
+ 'ac.ID',
|
|
|
+ 'ac.UserID',
|
|
|
+ 'ai.GameID',
|
|
|
+ 'ai.Channel as AccountChannel',
|
|
|
+ 'ai.RegisterDate',
|
|
|
+ 'ac.UrlSign',
|
|
|
+ 'ac.Platform',
|
|
|
+ 'ac.CreateTime',
|
|
|
+ 'ac.IP',
|
|
|
+ 'ac.Locale',
|
|
|
+ 'ac.Origin',
|
|
|
+ 'ac.FPID',
|
|
|
+ 'ac.FF',
|
|
|
+ 'ac.ClickUA',
|
|
|
+ 'ac.GameUA',
|
|
|
+ 'ac.Params',
|
|
|
+ 'ac.Cookie',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->applyFilters($query, $filters);
|
|
|
+ $query->where('ac.rn', 1);
|
|
|
+
|
|
|
+ $stats = $this->buildStats(clone $query);
|
|
|
+ $fbclidGroups = $this->buildFbclidGroups(clone $query);
|
|
|
+ $ffGroups = $this->buildDuplicateValueGroups(clone $query, 'FF', 'ff');
|
|
|
+
|
|
|
+ $list = $query
|
|
|
+ ->orderBy('ac.CreateTime', 'desc')
|
|
|
+ ->paginate($filters['page_size'])
|
|
|
+ ->appends($request->query());
|
|
|
+
|
|
|
+ $userIds = $list->getCollection()
|
|
|
+ ->pluck('UserID')
|
|
|
+ ->filter(function ($userId) {
|
|
|
+ return !empty($userId);
|
|
|
+ })
|
|
|
+ ->map(function ($userId) {
|
|
|
+ return (int)$userId;
|
|
|
+ })
|
|
|
+ ->unique()
|
|
|
+ ->values()
|
|
|
+ ->all();
|
|
|
+
|
|
|
+ $payMap = $this->loadPayStats($userIds);
|
|
|
+ $adjustMap = $this->loadAdjustEventStats($userIds, $filters);
|
|
|
+
|
|
|
+ $items = $list->getCollection()->map(function ($row) {
|
|
|
+ $params = $this->decodeJson($row->Params);
|
|
|
+ $cookieMap = $this->parseCookieString($row->Cookie);
|
|
|
+ $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
|
|
|
+ $paramAnalysis = $this->analyzeMarketingParams($params);
|
|
|
+
|
|
|
+ $row->HasFbclid = stripos($row->Params ?? '', 'fbclid') !== false;
|
|
|
+ $row->Fbp = $cookieMap['_fbp'] ?? '';
|
|
|
+ $row->Fbc = $cookieMap['_fbc'] ?? ($params['fbclid'] ?? '');
|
|
|
+ $row->Pixel = $paramAnalysis['primary']['pixel'];
|
|
|
+ $row->UtmSource = $paramAnalysis['primary']['utm_source'];
|
|
|
+ $row->UtmMedium = $paramAnalysis['primary']['utm_medium'];
|
|
|
+ $row->UtmCampaign = $paramAnalysis['primary']['utm_campaign'];
|
|
|
+ $row->ParamChannel = $paramAnalysis['primary']['channel'];
|
|
|
+ $row->ParamCampaign = $paramAnalysis['primary']['campaign'];
|
|
|
+ $row->ParamAdgroup = $paramAnalysis['primary']['adgroup'];
|
|
|
+ $row->ParamCreative = $paramAnalysis['primary']['creative'];
|
|
|
+ $row->UaApp = $uaInfo['app'];
|
|
|
+ $row->UaOs = $uaInfo['os'];
|
|
|
+ $row->UaDevice = $uaInfo['device'];
|
|
|
+ $row->CookieKey = $this->makeCookieKey($row);
|
|
|
+ $row->ParamsDecoded = $params;
|
|
|
+ $row->ParamAnalysis = $paramAnalysis;
|
|
|
+ $row->FbclidValue = $paramAnalysis['primary']['fbclid'];
|
|
|
+ $row->FbclidGroup = null;
|
|
|
+ $row->FFGroup = null;
|
|
|
+ $row->FbclidCookieCheck = $this->validateFbclidCookieConsistency(
|
|
|
+ $paramAnalysis['primary']['fbclid'],
|
|
|
+ $cookieMap['_fbc'] ?? ''
|
|
|
+ );
|
|
|
+
|
|
|
+ return $row;
|
|
|
+ });
|
|
|
+
|
|
|
+ $items = $items->map(function ($row) use ($payMap, $adjustMap, $fbclidGroups, $ffGroups) {
|
|
|
+ $pay = $payMap[(int)($row->UserID ?? 0)] ?? null;
|
|
|
+ $adjust = $adjustMap[(int)($row->UserID ?? 0)] ?? null;
|
|
|
+ $fbclidGroup = $fbclidGroups['rows'][$row->ID] ?? null;
|
|
|
+ $ffGroup = $ffGroups['rows'][$row->ID] ?? null;
|
|
|
+
|
|
|
+ $row->PayOrderCount = $pay->pay_order_count ?? 0;
|
|
|
+ $row->PayAmountSum = $pay->pay_amount_sum ?? 0;
|
|
|
+ $row->PayAmountDisplay = (string)round(((float)($row->PayAmountSum ?? 0)) / 100);
|
|
|
+ $row->LastPayAt = $pay->last_pay_at ?? '';
|
|
|
+ $row->AdjustStatus = $adjust['status'] ?? 'none';
|
|
|
+ $row->AdjustLogs = $adjust['logs'] ?? [];
|
|
|
+ $row->FbclidGroup = $fbclidGroup;
|
|
|
+ $row->FFGroup = $ffGroup;
|
|
|
+
|
|
|
+ return $row;
|
|
|
+ });
|
|
|
+
|
|
|
+ $list->setCollection($items);
|
|
|
+
|
|
|
+ return view('admin.account_cookie.index', [
|
|
|
+ 'list' => $list,
|
|
|
+ 'filters' => $filters,
|
|
|
+ 'stats' => $stats,
|
|
|
+ 'fbclidGroups' => $fbclidGroups['groups'],
|
|
|
+ 'ffGroups' => $ffGroups['groups'],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function buildFilters(Request $request)
|
|
|
+ {
|
|
|
+ $gameId = trim((string)$request->input('GameID', ''));
|
|
|
+ $resolvedUserId = '';
|
|
|
+ if ($gameId !== '') {
|
|
|
+ $resolvedUserId = AccountsInfo::query()->where('GameID', $gameId)->value('UserID') ?: '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $dateStartRaw = trim((string)$request->input('date_start', date('Y-m-d')));
|
|
|
+ $dateEndRaw = trim((string)$request->input('date_end', date('Y-m-d')));
|
|
|
+ $registerStartRaw = trim((string)$request->input('register_start', date('Y-m-d')));
|
|
|
+ $registerEndRaw = trim((string)$request->input('register_end', date('Y-m-d')));
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'user_id' => trim((string)$request->input('UserID', '')),
|
|
|
+ 'game_id' => $gameId,
|
|
|
+ 'resolved_user_id' => $resolvedUserId,
|
|
|
+ 'url_signs' => $this->splitCsv($request->input('UrlSign', '')),
|
|
|
+ 'account_channels' => $this->splitCsv($request->input('AccountChannel', '')),
|
|
|
+ 'platform' => trim((string)$request->input('Platform', '')),
|
|
|
+ 'date_start_raw' => $dateStartRaw,
|
|
|
+ 'date_end_raw' => $dateEndRaw,
|
|
|
+ 'register_start_raw' => $registerStartRaw,
|
|
|
+ 'register_end_raw' => $registerEndRaw,
|
|
|
+ 'date_start' => $this->normalizeDateBoundary($dateStartRaw, false),
|
|
|
+ 'date_end' => $this->normalizeDateBoundary($dateEndRaw, true),
|
|
|
+ 'register_start' => $this->normalizeDateBoundary($registerStartRaw, false),
|
|
|
+ 'register_end' => $this->normalizeDateBoundary($registerEndRaw, true),
|
|
|
+ 'has_fbclid' => (int)$request->input('has_fbclid', 1),
|
|
|
+ 'origin' => trim((string)$request->input('Origin', '')),
|
|
|
+ 'ip' => trim((string)$request->input('IP', '')),
|
|
|
+ 'ua' => trim((string)$request->input('UA', '')),
|
|
|
+ 'param_category' => trim((string)$request->input('param_category', '')),
|
|
|
+ 'param_key' => trim((string)$request->input('param_key', '')),
|
|
|
+ 'param_value' => trim((string)$request->input('param_value', '')),
|
|
|
+ 'page_size' => max(20, min((int)$request->input('page_size', 100), 500)),
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function applyFilters($query, array $filters)
|
|
|
+ {
|
|
|
+ if ($filters['user_id'] !== '') {
|
|
|
+ $query->where('ac.UserID', $filters['user_id']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['game_id'] !== '') {
|
|
|
+ if ($filters['resolved_user_id'] !== '') {
|
|
|
+ $query->where('ac.UserID', $filters['resolved_user_id']);
|
|
|
+ } else {
|
|
|
+ $query->whereRaw('1 = 0');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($filters['url_signs'])) {
|
|
|
+ $query->whereIn('ac.UrlSign', $filters['url_signs']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($filters['account_channels'])) {
|
|
|
+ $query->whereIn('ai.Channel', $filters['account_channels']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['platform'] !== '') {
|
|
|
+ $query->where('ac.Platform', $filters['platform']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['date_start'] !== '') {
|
|
|
+ $query->where('ac.CreateTime', '>=', $filters['date_start']);
|
|
|
+ }
|
|
|
+ if ($filters['date_end'] !== '') {
|
|
|
+ $query->where('ac.CreateTime', '<=', $filters['date_end']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['register_start'] !== '') {
|
|
|
+ $query->where('ai.RegisterDate', '>=', $filters['register_start']);
|
|
|
+ }
|
|
|
+ if ($filters['register_end'] !== '') {
|
|
|
+ $query->where('ai.RegisterDate', '<=', $filters['register_end']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['has_fbclid'] === 1) {
|
|
|
+ $query->where('ac.Params', 'like', '%fbclid%');
|
|
|
+ } elseif ($filters['has_fbclid'] === 2) {
|
|
|
+ $query->where('ac.Params', 'not like', '%fbclid%');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['origin'] !== '') {
|
|
|
+ $query->where('ac.Origin', 'like', '%' . $filters['origin'] . '%');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['ip'] !== '') {
|
|
|
+ $query->where('ac.IP', 'like', '%' . $filters['ip'] . '%');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['ua'] !== '') {
|
|
|
+ $query->where(function ($subQuery) use ($filters) {
|
|
|
+ $subQuery->where('ac.ClickUA', 'like', '%' . $filters['ua'] . '%')
|
|
|
+ ->orWhere('ac.GameUA', 'like', '%' . $filters['ua'] . '%');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['param_key'] !== '') {
|
|
|
+ $query->where('ac.Params', 'like', '%"' . $filters['param_key'] . '"%');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['param_value'] !== '') {
|
|
|
+ $query->where('ac.Params', 'like', '%' . $filters['param_value'] . '%');
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($filters['param_category'] !== '') {
|
|
|
+ $keys = $this->getCategoryKeys($filters['param_category']);
|
|
|
+ if (empty($keys)) {
|
|
|
+ $query->whereRaw('1 = 0');
|
|
|
+ } else {
|
|
|
+ $query->where(function ($subQuery) use ($keys) {
|
|
|
+ foreach ($keys as $key) {
|
|
|
+ $subQuery->orWhere('ac.Params', 'like', '%"' . $key . '"%');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function buildStats($query)
|
|
|
+ {
|
|
|
+ $rows = $query->get([
|
|
|
+ 'ac.ID',
|
|
|
+ 'ac.UserID',
|
|
|
+ 'ac.FPID',
|
|
|
+ 'ac.FF',
|
|
|
+ 'ac.UrlSign',
|
|
|
+ 'ac.Platform',
|
|
|
+ 'ac.IP',
|
|
|
+ 'ac.Origin',
|
|
|
+ 'ac.ClickUA',
|
|
|
+ 'ac.GameUA',
|
|
|
+ 'ac.Params',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $stats = [
|
|
|
+ 'total' => $rows->count(),
|
|
|
+ 'unique_users' => $rows->pluck('UserID')->filter()->unique()->count(),
|
|
|
+ 'unique_ips' => $rows->pluck('IP')->filter()->unique()->count(),
|
|
|
+ 'unique_cookies' => $rows->map(function ($row) {
|
|
|
+ return $this->makeCookieKey($row);
|
|
|
+ })->filter()->unique()->count(),
|
|
|
+ 'registered_users' => $rows->filter(function ($row) {
|
|
|
+ return !empty($row->UserID);
|
|
|
+ })->pluck('UserID')->unique()->count(),
|
|
|
+ 'paid_users' => 0,
|
|
|
+ 'fbclid_count' => 0,
|
|
|
+ 'fb_inapp_count' => 0,
|
|
|
+ 'ig_inapp_count' => 0,
|
|
|
+ 'platforms' => [],
|
|
|
+ 'url_signs' => [],
|
|
|
+ 'origins' => [],
|
|
|
+ 'utm_sources' => [],
|
|
|
+ 'param_categories' => [],
|
|
|
+ 'param_keys' => [],
|
|
|
+ 'param_category_stats' => [],
|
|
|
+ 'duplicate_fbclid_groups' => 0,
|
|
|
+ 'duplicate_fbclid_rows' => 0,
|
|
|
+ 'duplicate_ff_groups' => 0,
|
|
|
+ 'duplicate_ff_rows' => 0,
|
|
|
+ 'fbclid_cookie_issues' => 0,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $fbclidBuckets = [];
|
|
|
+ $ffBuckets = [];
|
|
|
+
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $params = $this->decodeJson($row->Params);
|
|
|
+ $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
|
|
|
+ $paramAnalysis = $this->analyzeMarketingParams($params);
|
|
|
+ $cookieMap = $this->parseCookieString($row->Cookie ?? '');
|
|
|
+
|
|
|
+ if (stripos($row->Params ?? '', 'fbclid') !== false) {
|
|
|
+ $stats['fbclid_count']++;
|
|
|
+ }
|
|
|
+ if ($uaInfo['app'] === 'Facebook') {
|
|
|
+ $stats['fb_inapp_count']++;
|
|
|
+ }
|
|
|
+ if ($uaInfo['app'] === 'Instagram') {
|
|
|
+ $stats['ig_inapp_count']++;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->incrementBucket($stats['platforms'], $row->Platform ?: 'unknown');
|
|
|
+ $this->incrementBucket($stats['url_signs'], (string)($row->UrlSign ?: 'unknown'));
|
|
|
+ $this->incrementBucket($stats['origins'], $row->Origin ?: 'unknown');
|
|
|
+ $this->incrementBucket($stats['utm_sources'], $params['utm_source'] ?? 'unknown');
|
|
|
+
|
|
|
+ foreach ($paramAnalysis['categories'] as $category => $entries) {
|
|
|
+ $label = $this->getParamCategoryLabel($category);
|
|
|
+ $this->incrementBucket($stats['param_categories'], $label);
|
|
|
+
|
|
|
+ if (!isset($stats['param_category_stats'][$category])) {
|
|
|
+ $stats['param_category_stats'][$category] = [
|
|
|
+ 'label' => $label,
|
|
|
+ 'keys' => [],
|
|
|
+ 'values' => [],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($entries as $entry) {
|
|
|
+ $this->incrementBucket($stats['param_category_stats'][$category]['keys'], $entry['key']);
|
|
|
+ $this->incrementBucket($stats['param_keys'], $entry['key']);
|
|
|
+
|
|
|
+ $valueLabel = $entry['key'] . '=' . $this->truncateParamValue($entry['value']);
|
|
|
+ $this->incrementBucket($stats['param_category_stats'][$category]['values'], $valueLabel);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $fbclid = $paramAnalysis['primary']['fbclid'];
|
|
|
+ if ($fbclid !== '') {
|
|
|
+ $fbclidBuckets[$fbclid] = ($fbclidBuckets[$fbclid] ?? 0) + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ff = trim((string)($row->FF ?? ''));
|
|
|
+ if ($ff !== '') {
|
|
|
+ $ffBuckets[$ff] = ($ffBuckets[$ff] ?? 0) + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ $fbclidCookieCheck = $this->validateFbclidCookieConsistency(
|
|
|
+ $paramAnalysis['primary']['fbclid'],
|
|
|
+ $cookieMap['_fbc'] ?? ''
|
|
|
+ );
|
|
|
+ if (!$fbclidCookieCheck['ok']) {
|
|
|
+ $stats['fbclid_cookie_issues']++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $stats['paid_users'] = $this->loadPaidUserCount(
|
|
|
+ $rows->pluck('UserID')->filter()->map(function ($userId) {
|
|
|
+ return (int)$userId;
|
|
|
+ })->unique()->values()->all()
|
|
|
+ );
|
|
|
+
|
|
|
+ arsort($stats['platforms']);
|
|
|
+ arsort($stats['url_signs']);
|
|
|
+ arsort($stats['origins']);
|
|
|
+ arsort($stats['utm_sources']);
|
|
|
+ arsort($stats['param_categories']);
|
|
|
+ arsort($stats['param_keys']);
|
|
|
+
|
|
|
+ foreach ($stats['param_category_stats'] as &$categoryStats) {
|
|
|
+ arsort($categoryStats['keys']);
|
|
|
+ arsort($categoryStats['values']);
|
|
|
+ }
|
|
|
+ unset($categoryStats);
|
|
|
+
|
|
|
+ foreach ($fbclidBuckets as $count) {
|
|
|
+ if ($count > 1) {
|
|
|
+ $stats['duplicate_fbclid_groups']++;
|
|
|
+ $stats['duplicate_fbclid_rows'] += $count;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($ffBuckets as $count) {
|
|
|
+ if ($count > 1) {
|
|
|
+ $stats['duplicate_ff_groups']++;
|
|
|
+ $stats['duplicate_ff_rows'] += $count;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $stats;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function buildFbclidGroups($query)
|
|
|
+ {
|
|
|
+ $rows = $query->get([
|
|
|
+ 'ac.ID',
|
|
|
+ 'ac.Params',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $groupsByFbclid = [];
|
|
|
+
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $params = $this->decodeJson($row->Params);
|
|
|
+ $fbclid = $this->analyzeMarketingParams($params)['primary']['fbclid'];
|
|
|
+ if ($fbclid === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $groupsByFbclid[$fbclid][] = (int)$row->ID;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->finalizeDuplicateGroups($groupsByFbclid, 'fbclid');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function buildDuplicateValueGroups($query, $field, $type)
|
|
|
+ {
|
|
|
+ $rows = $query->get([
|
|
|
+ 'ac.ID',
|
|
|
+ 'ac.' . $field,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $groups = [];
|
|
|
+
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ $value = trim((string)($row->{$field} ?? ''));
|
|
|
+ if ($value === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $groups[$value][] = (int)$row->ID;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->finalizeDuplicateGroups($groups, $type);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function finalizeDuplicateGroups(array $groupedIds, $type)
|
|
|
+ {
|
|
|
+ $palettes = [
|
|
|
+ 'fbclid' => ['#fff3cd', '#d1ecf1', '#d4edda', '#f8d7da', '#e2d9f3', '#fde2b8', '#d6eaf8', '#f5c6cb'],
|
|
|
+ 'ff' => ['#e8f5e9', '#e3f2fd', '#fff8e1', '#fce4ec', '#ede7f6', '#e0f7fa', '#f1f8e9', '#fff3e0'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ $palette = $palettes[$type] ?? ['#f5f5f5'];
|
|
|
+
|
|
|
+ $groups = [];
|
|
|
+ $rowMap = [];
|
|
|
+ $groupIndex = 1;
|
|
|
+
|
|
|
+ foreach ($groupedIds as $value => $ids) {
|
|
|
+ $ids = array_values(array_unique($ids));
|
|
|
+ if (count($ids) < 2) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $color = $palette[($groupIndex - 1) % count($palette)];
|
|
|
+ $group = [
|
|
|
+ 'index' => $groupIndex,
|
|
|
+ 'value' => $value,
|
|
|
+ 'count' => count($ids),
|
|
|
+ 'color' => $color,
|
|
|
+ 'row_ids' => $ids,
|
|
|
+ 'type' => $type,
|
|
|
+ ];
|
|
|
+
|
|
|
+ $groups[] = $group;
|
|
|
+
|
|
|
+ foreach ($ids as $id) {
|
|
|
+ $rowMap[$id] = $group;
|
|
|
+ }
|
|
|
+
|
|
|
+ $groupIndex++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'groups' => $groups,
|
|
|
+ 'rows' => $rowMap,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function validateFbclidCookieConsistency($paramFbclid, $cookieFbc)
|
|
|
+ {
|
|
|
+ $paramFbclid = trim((string)$paramFbclid);
|
|
|
+ $cookieFbc = trim((string)$cookieFbc);
|
|
|
+ $cookieFbclid = $this->extractFbclidFromFbc($cookieFbc);
|
|
|
+
|
|
|
+ if ($paramFbclid === '' && $cookieFbclid === '') {
|
|
|
+ return [
|
|
|
+ 'ok' => true,
|
|
|
+ 'status' => 'none',
|
|
|
+ 'cookie_fbclid' => '',
|
|
|
+ 'message' => '',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($paramFbclid !== '' && $cookieFbclid === '') {
|
|
|
+ return [
|
|
|
+ 'ok' => false,
|
|
|
+ 'status' => 'missing_cookie_fbc',
|
|
|
+ 'cookie_fbclid' => '',
|
|
|
+ 'message' => 'Params 有 fbclid,但 Cookie 未解析出 _fbc/fbclid',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($paramFbclid === '' && $cookieFbclid !== '') {
|
|
|
+ return [
|
|
|
+ 'ok' => false,
|
|
|
+ 'status' => 'missing_param_fbclid',
|
|
|
+ 'cookie_fbclid' => $cookieFbclid,
|
|
|
+ 'message' => 'Cookie 有 _fbc/fbclid,但 Params 缺少 fbclid',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($paramFbclid === $cookieFbclid) {
|
|
|
+ return [
|
|
|
+ 'ok' => true,
|
|
|
+ 'status' => 'match',
|
|
|
+ 'cookie_fbclid' => $cookieFbclid,
|
|
|
+ 'message' => '匹配',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'ok' => false,
|
|
|
+ 'status' => 'mismatch',
|
|
|
+ 'cookie_fbclid' => $cookieFbclid,
|
|
|
+ 'message' => 'Params.fbclid 与 Cookie._fbc 中的 fbclid 不一致',
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function extractFbclidFromFbc($cookieFbc)
|
|
|
+ {
|
|
|
+ $cookieFbc = trim((string)$cookieFbc);
|
|
|
+ if ($cookieFbc === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $parts = explode('.', $cookieFbc, 4);
|
|
|
+ if (count($parts) === 4 && $parts[0] === 'fb' && in_array($parts[1], ['1', '2'], true)) {
|
|
|
+ return trim((string)$parts[3]);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $cookieFbc;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function incrementBucket(array &$bucket, $key)
|
|
|
+ {
|
|
|
+ $bucket[$key] = ($bucket[$key] ?? 0) + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function splitCsv($value)
|
|
|
+ {
|
|
|
+ return array_values(array_filter(array_map('trim', explode(',', (string)$value)), function ($item) {
|
|
|
+ return $item !== '';
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function normalizeDateBoundary($value, $endOfDay = false)
|
|
|
+ {
|
|
|
+ $value = trim((string)$value);
|
|
|
+ if ($value === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $value = substr(str_replace('T', ' ', $value), 0, 10);
|
|
|
+ return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function decodeJson($json)
|
|
|
+ {
|
|
|
+ $data = json_decode((string)$json, true);
|
|
|
+ return is_array($data) ? $data : [];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function analyzeMarketingParams(array $params)
|
|
|
+ {
|
|
|
+ $analysis = [
|
|
|
+ 'flat' => [],
|
|
|
+ 'categories' => [],
|
|
|
+ 'primary' => [
|
|
|
+ 'channel' => '',
|
|
|
+ 'utm_source' => '',
|
|
|
+ 'utm_medium' => '',
|
|
|
+ 'utm_campaign' => '',
|
|
|
+ 'campaign' => '',
|
|
|
+ 'adgroup' => '',
|
|
|
+ 'creative' => '',
|
|
|
+ 'pixel' => '',
|
|
|
+ 'fbclid' => '',
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+
|
|
|
+ foreach ($params as $key => $value) {
|
|
|
+ if (is_array($value) || is_object($value)) {
|
|
|
+ $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
+ }
|
|
|
+
|
|
|
+ $key = trim((string)$key);
|
|
|
+ $value = trim((string)$value);
|
|
|
+ if ($key === '' || $value === '') {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $category = $this->detectParamCategory($key);
|
|
|
+
|
|
|
+ $analysis['flat'][$key] = $value;
|
|
|
+ $analysis['categories'][$category][] = [
|
|
|
+ 'key' => $key,
|
|
|
+ 'value' => $value,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ $analysis['primary']['channel'] = $analysis['flat']['c'] ?? ($analysis['flat']['channel'] ?? '');
|
|
|
+ $analysis['primary']['utm_source'] = $analysis['flat']['utm_source'] ?? '';
|
|
|
+ $analysis['primary']['utm_medium'] = $analysis['flat']['utm_medium'] ?? '';
|
|
|
+ $analysis['primary']['utm_campaign'] = $analysis['flat']['utm_campaign'] ?? '';
|
|
|
+ $analysis['primary']['campaign'] = $analysis['flat']['campaign'] ?? ($analysis['flat']['utm_campaign'] ?? '');
|
|
|
+ $analysis['primary']['adgroup'] = $analysis['flat']['adgroup'] ?? ($analysis['flat']['adset'] ?? ($analysis['flat']['utm_term'] ?? ''));
|
|
|
+ $analysis['primary']['creative'] = $analysis['flat']['creative'] ?? ($analysis['flat']['utm_content'] ?? '');
|
|
|
+ $analysis['primary']['pixel'] = $analysis['flat']['pixel'] ?? ($analysis['flat']['pixelID'] ?? ($analysis['flat']['pixel_id'] ?? ''));
|
|
|
+ $analysis['primary']['fbclid'] = $analysis['flat']['fbclid'] ?? '';
|
|
|
+
|
|
|
+ return $analysis;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function detectParamCategory($key)
|
|
|
+ {
|
|
|
+ $normalizedKey = strtolower(trim((string)$key));
|
|
|
+
|
|
|
+ if (strpos($normalizedKey, 'utm_') === 0) {
|
|
|
+ return 'utm';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (in_array($normalizedKey, ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'], true)) {
|
|
|
+ return 'campaign';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (in_array($normalizedKey, ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelid', 'pixel_id'], true)) {
|
|
|
+ return 'attribution';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (in_array($normalizedKey, ['c', 'channel', 'source', 'media_source', 'utm_source_platform', 'pid', 'af_channel', 'sub_channel'], true)) {
|
|
|
+ return 'channel';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (preg_match('/(_id|id)$/', $normalizedKey)) {
|
|
|
+ return 'identifier';
|
|
|
+ }
|
|
|
+
|
|
|
+ return 'custom';
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function getParamCategoryLabel($category)
|
|
|
+ {
|
|
|
+ $labels = [
|
|
|
+ 'utm' => 'UTM',
|
|
|
+ 'campaign' => 'Campaign',
|
|
|
+ 'attribution' => 'Attribution',
|
|
|
+ 'channel' => 'Channel',
|
|
|
+ 'identifier' => 'Identifier',
|
|
|
+ 'custom' => 'Custom',
|
|
|
+ ];
|
|
|
+
|
|
|
+ return $labels[$category] ?? ucfirst($category);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function getCategoryKeys($category)
|
|
|
+ {
|
|
|
+ $map = [
|
|
|
+ 'utm' => ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform'],
|
|
|
+ 'campaign' => ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'],
|
|
|
+ 'attribution' => ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelID', 'pixel_id'],
|
|
|
+ 'channel' => ['c', 'channel', 'source', 'media_source', 'pid', 'af_channel', 'sub_channel'],
|
|
|
+ 'identifier' => ['utm_id', 'campaign_id', 'adgroup_id', 'adset_id', 'creative_id', 'pixel_id', 'pixelID'],
|
|
|
+ ];
|
|
|
+
|
|
|
+ return $map[$category] ?? [];
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function truncateParamValue($value, $length = 36)
|
|
|
+ {
|
|
|
+ $value = (string)$value;
|
|
|
+ if ($value === '') {
|
|
|
+ return '-';
|
|
|
+ }
|
|
|
+
|
|
|
+ return mb_strlen($value) > $length ? mb_substr($value, 0, $length) . '...' : $value;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function makeCookieKey($row)
|
|
|
+ {
|
|
|
+ if (!empty($row->FPID)) {
|
|
|
+ return 'fpid:' . $row->FPID;
|
|
|
+ }
|
|
|
+ if (!empty($row->FF)) {
|
|
|
+ return 'ff:' . $row->FF;
|
|
|
+ }
|
|
|
+ if (!empty($row->UserID)) {
|
|
|
+ return 'uid:' . $row->UserID;
|
|
|
+ }
|
|
|
+ return 'row:' . ($row->ID ?? '');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function parseCookieString($cookieString)
|
|
|
+ {
|
|
|
+ $cookies = [];
|
|
|
+ foreach (explode(';', (string)$cookieString) as $part) {
|
|
|
+ $part = trim($part);
|
|
|
+ if ($part === '' || strpos($part, '=') === false) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ [$name, $value] = explode('=', $part, 2);
|
|
|
+ $cookies[trim($name)] = trim($value);
|
|
|
+ }
|
|
|
+ return $cookies;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function analyzeUserAgent($ua)
|
|
|
+ {
|
|
|
+ $ua = (string)$ua;
|
|
|
+ $app = 'Browser';
|
|
|
+ if (stripos($ua, 'Instagram') !== false) {
|
|
|
+ $app = 'Instagram';
|
|
|
+ } elseif (stripos($ua, 'FBAN') !== false || stripos($ua, 'FBAV') !== false || stripos($ua, 'Facebook') !== false) {
|
|
|
+ $app = 'Facebook';
|
|
|
+ }
|
|
|
+
|
|
|
+ $os = 'Unknown';
|
|
|
+ if (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false || stripos($ua, 'iOS') !== false) {
|
|
|
+ $os = 'iOS';
|
|
|
+ } elseif (stripos($ua, 'Android') !== false) {
|
|
|
+ $os = 'Android';
|
|
|
+ }
|
|
|
+
|
|
|
+ $device = 'Unknown';
|
|
|
+ if (stripos($ua, 'iPhone') !== false) {
|
|
|
+ $device = 'iPhone';
|
|
|
+ } elseif (stripos($ua, 'iPad') !== false) {
|
|
|
+ $device = 'iPad';
|
|
|
+ } elseif (stripos($ua, 'Android') !== false) {
|
|
|
+ $device = 'Android';
|
|
|
+ }
|
|
|
+
|
|
|
+ return compact('app', 'os', 'device');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function loadPaidUserCount(array $userIds)
|
|
|
+ {
|
|
|
+ if (empty($userIds)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::connection('read')
|
|
|
+ ->table('agent.dbo.order')
|
|
|
+ ->whereIn('user_id', $userIds)
|
|
|
+ ->where('pay_status', 1)
|
|
|
+ ->distinct('user_id')
|
|
|
+ ->count('user_id');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function loadPayStats(array $userIds)
|
|
|
+ {
|
|
|
+ if (empty($userIds)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return DB::connection('read')
|
|
|
+ ->table('agent.dbo.order')
|
|
|
+ ->whereIn('user_id', $userIds)
|
|
|
+ ->where('pay_status', 1)
|
|
|
+ ->groupBy('user_id')
|
|
|
+ ->selectRaw('user_id, count(*) as pay_order_count, cast(sum(amount) as decimal(18,2)) as pay_amount_sum, max(pay_at) as last_pay_at')
|
|
|
+ ->get()
|
|
|
+ ->keyBy('user_id')
|
|
|
+ ->all();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function loadAdjustEventStats(array $userIds, array $filters)
|
|
|
+ {
|
|
|
+ if (empty($userIds)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ $result = [];
|
|
|
+ $userIdMap = array_fill_keys(array_map('strval', $userIds), true);
|
|
|
+
|
|
|
+ foreach ($this->resolveAdjustLogFiles($filters) as $logFile) {
|
|
|
+ $handle = @fopen($logFile, 'r');
|
|
|
+ if ($handle === false) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $currentUserId = null;
|
|
|
+
|
|
|
+ while (($line = fgets($handle)) !== false) {
|
|
|
+ $userId = $this->extractAdjustUserId($line);
|
|
|
+ $status = $this->classifyAdjustLine($line);
|
|
|
+
|
|
|
+ if ($userId !== null && isset($userIdMap[(string)$userId])) {
|
|
|
+ $currentUserId = $userId;
|
|
|
+ } elseif ($userId === null && $currentUserId !== null && in_array($status, ['request', 'success', 'skipped', 'failed'], true)) {
|
|
|
+ $userId = $currentUserId;
|
|
|
+ } else {
|
|
|
+ if ($status === 'enter') {
|
|
|
+ $currentUserId = null;
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isset($result[$userId])) {
|
|
|
+ $result[$userId] = ['status' => 'enter', 'logs' => []];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($status !== null) {
|
|
|
+ $result[$userId]['status'] = $status;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (count($result[$userId]['logs']) < 50) {
|
|
|
+ $result[$userId]['logs'][] = trim($line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fclose($handle);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function resolveAdjustLogFiles(array $filters)
|
|
|
+ {
|
|
|
+ $start = substr($filters['date_start'] ?: $filters['register_start'], 0, 10);
|
|
|
+ $end = substr($filters['date_end'] ?: $filters['register_end'], 0, 10);
|
|
|
+ if ($start === '') {
|
|
|
+ $start = date('Y-m-d');
|
|
|
+ }
|
|
|
+ if ($end === '') {
|
|
|
+ $end = $start;
|
|
|
+ }
|
|
|
+
|
|
|
+ $startTs = strtotime($start);
|
|
|
+ $endTs = strtotime($end);
|
|
|
+ if ($startTs === false || $endTs === false) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ if ($endTs < $startTs) {
|
|
|
+ [$startTs, $endTs] = [$endTs, $startTs];
|
|
|
+ }
|
|
|
+
|
|
|
+ $files = [];
|
|
|
+ $days = 0;
|
|
|
+ for ($time = $startTs; $time <= $endTs && $days < 7; $time += 86400, $days++) {
|
|
|
+ $file = storage_path('logs/adjustEvent-' . date('Y-m-d', $time) . '.log');
|
|
|
+ if (file_exists($file)) {
|
|
|
+ $files[] = $file;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $files;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function extractAdjustUserId($line)
|
|
|
+ {
|
|
|
+ if (preg_match('/"UserID":"?(\d+)"?/', $line, $matches)) {
|
|
|
+ return (int)$matches[1];
|
|
|
+ }
|
|
|
+ if (preg_match('/"user_id":"?(\d+)"?/', $line, $matches)) {
|
|
|
+ return (int)$matches[1];
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function classifyAdjustLine($line)
|
|
|
+ {
|
|
|
+ if (strpos($line, 'facebook s2s response') !== false) {
|
|
|
+ return 'success';
|
|
|
+ }
|
|
|
+ if (strpos($line, 'facebook s2s request') !== false) {
|
|
|
+ return 'request';
|
|
|
+ }
|
|
|
+ if (strpos($line, 'facebook s2s skipped') !== false) {
|
|
|
+ return 'skipped';
|
|
|
+ }
|
|
|
+ if (strpos($line, 'facebook s2s failed') !== false) {
|
|
|
+ return 'failed';
|
|
|
+ }
|
|
|
+ if (strpos($line, 'enter:') !== false) {
|
|
|
+ return 'enter';
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function extractChannelFromCookie($userID, $user = null)
|
|
|
+ {
|
|
|
+ $cookieInfo = \App\Services\ApkService::loadCookie($userID, $user->FPID ?? '', $user->FF ?? '');
|
|
|
+ if (!$cookieInfo || empty($cookieInfo['Params'])) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ $params = json_decode($cookieInfo['Params'], true);
|
|
|
+ if (!is_array($params)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $params['c'] ?? null;
|
|
|
+ }
|
|
|
+}
|