AccountCookieController.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  1. <?php
  2. namespace App\Http\Controllers\Admin;
  3. use App\Http\Controllers\Controller;
  4. use App\Models\AccountsInfo;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Support\Facades\DB;
  7. class AccountCookieController extends Controller
  8. {
  9. public function index(Request $request)
  10. {
  11. $filters = $this->buildFilters($request);
  12. $cookieSubQuery = DB::connection('read')->raw("
  13. (
  14. SELECT *,
  15. ROW_NUMBER() OVER (
  16. PARTITION BY
  17. ISNULL(CAST(UserID AS VARCHAR(50)), '')
  18. ORDER BY CreateTime DESC, ID DESC
  19. ) AS rn
  20. FROM QPAccountsDB.dbo.AccountCookie
  21. ) ac
  22. ");
  23. $query = DB::connection('read')
  24. ->table($cookieSubQuery)
  25. ->leftJoin(AccountsInfo::TABLE . ' as ai', 'ai.UserID', '=', 'ac.UserID')
  26. ->select([
  27. 'ac.ID',
  28. 'ac.UserID',
  29. 'ai.GameID',
  30. 'ai.Channel as AccountChannel',
  31. 'ai.RegisterDate',
  32. 'ac.UrlSign',
  33. 'ac.Platform',
  34. 'ac.CreateTime',
  35. 'ac.IP',
  36. 'ac.Locale',
  37. 'ac.Origin',
  38. 'ac.FPID',
  39. 'ac.FF',
  40. 'ac.ClickUA',
  41. 'ac.GameUA',
  42. 'ac.Params',
  43. 'ac.Cookie',
  44. ]);
  45. $this->applyFilters($query, $filters);
  46. $query->where('ac.rn', 1);
  47. $stats = $this->buildStats(clone $query);
  48. $fbclidGroups = $this->buildFbclidGroups(clone $query);
  49. $ffGroups = $this->buildDuplicateValueGroups(clone $query, 'FF', 'ff');
  50. $list = $query
  51. ->orderBy('ac.CreateTime', 'desc')
  52. ->paginate($filters['page_size'])
  53. ->appends($request->query());
  54. $userIds = $list->getCollection()
  55. ->pluck('UserID')
  56. ->filter(function ($userId) {
  57. return !empty($userId);
  58. })
  59. ->map(function ($userId) {
  60. return (int)$userId;
  61. })
  62. ->unique()
  63. ->values()
  64. ->all();
  65. $payMap = $this->loadPayStats($userIds);
  66. $adjustMap = $this->loadAdjustEventStats($userIds, $filters);
  67. $items = $list->getCollection()->map(function ($row) {
  68. $params = $this->decodeJson($row->Params);
  69. $cookieMap = $this->parseCookieString($row->Cookie);
  70. $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
  71. $paramAnalysis = $this->analyzeMarketingParams($params);
  72. $row->HasFbclid = stripos($row->Params ?? '', 'fbclid') !== false;
  73. $row->Fbp = $cookieMap['_fbp'] ?? '';
  74. $row->Fbc = $cookieMap['_fbc'] ?? ($params['fbclid'] ?? '');
  75. $row->Pixel = $paramAnalysis['primary']['pixel'];
  76. $row->UtmSource = $paramAnalysis['primary']['utm_source'];
  77. $row->UtmMedium = $paramAnalysis['primary']['utm_medium'];
  78. $row->UtmCampaign = $paramAnalysis['primary']['utm_campaign'];
  79. $row->ParamChannel = $paramAnalysis['primary']['channel'];
  80. $row->ParamCampaign = $paramAnalysis['primary']['campaign'];
  81. $row->ParamAdgroup = $paramAnalysis['primary']['adgroup'];
  82. $row->ParamCreative = $paramAnalysis['primary']['creative'];
  83. $row->UaApp = $uaInfo['app'];
  84. $row->UaOs = $uaInfo['os'];
  85. $row->UaDevice = $uaInfo['device'];
  86. $row->CookieKey = $this->makeCookieKey($row);
  87. $row->ParamsDecoded = $params;
  88. $row->ParamAnalysis = $paramAnalysis;
  89. $row->FbclidValue = $paramAnalysis['primary']['fbclid'];
  90. $row->FbclidGroup = null;
  91. $row->FFGroup = null;
  92. $row->FbclidCookieCheck = $this->validateFbclidCookieConsistency(
  93. $paramAnalysis['primary']['fbclid'],
  94. $cookieMap['_fbc'] ?? ''
  95. );
  96. return $row;
  97. });
  98. $items = $items->map(function ($row) use ($payMap, $adjustMap, $fbclidGroups, $ffGroups) {
  99. $pay = $payMap[(int)($row->UserID ?? 0)] ?? null;
  100. $adjust = $adjustMap[(int)($row->UserID ?? 0)] ?? null;
  101. $fbclidGroup = $fbclidGroups['rows'][$row->ID] ?? null;
  102. $ffGroup = $ffGroups['rows'][$row->ID] ?? null;
  103. $row->PayOrderCount = $pay->pay_order_count ?? 0;
  104. $row->PayAmountSum = $pay->pay_amount_sum ?? 0;
  105. $row->PayAmountDisplay = (string)round(((float)($row->PayAmountSum ?? 0)) / 100);
  106. $row->LastPayAt = $pay->last_pay_at ?? '';
  107. $row->AdjustStatus = $adjust['status'] ?? 'none';
  108. $row->AdjustLogs = $adjust['logs'] ?? [];
  109. $row->FbclidGroup = $fbclidGroup;
  110. $row->FFGroup = $ffGroup;
  111. return $row;
  112. });
  113. $list->setCollection($items);
  114. return view('admin.account_cookie.index', [
  115. 'list' => $list,
  116. 'filters' => $filters,
  117. 'stats' => $stats,
  118. 'fbclidGroups' => $fbclidGroups['groups'],
  119. 'ffGroups' => $ffGroups['groups'],
  120. ]);
  121. }
  122. protected function buildFilters(Request $request)
  123. {
  124. $gameId = trim((string)$request->input('GameID', ''));
  125. $resolvedUserId = '';
  126. if ($gameId !== '') {
  127. $resolvedUserId = AccountsInfo::query()->where('GameID', $gameId)->value('UserID') ?: '';
  128. }
  129. $dateStartRaw = trim((string)$request->input('date_start', date('Y-m-d')));
  130. $dateEndRaw = trim((string)$request->input('date_end', date('Y-m-d')));
  131. $registerStartRaw = trim((string)$request->input('register_start', date('Y-m-d')));
  132. $registerEndRaw = trim((string)$request->input('register_end', date('Y-m-d')));
  133. return [
  134. 'user_id' => trim((string)$request->input('UserID', '')),
  135. 'game_id' => $gameId,
  136. 'resolved_user_id' => $resolvedUserId,
  137. 'url_signs' => $this->splitCsv($request->input('UrlSign', '')),
  138. 'account_channels' => $this->splitCsv($request->input('AccountChannel', '')),
  139. 'platform' => trim((string)$request->input('Platform', '')),
  140. 'date_start_raw' => $dateStartRaw,
  141. 'date_end_raw' => $dateEndRaw,
  142. 'register_start_raw' => $registerStartRaw,
  143. 'register_end_raw' => $registerEndRaw,
  144. 'date_start' => $this->normalizeDateBoundary($dateStartRaw, false),
  145. 'date_end' => $this->normalizeDateBoundary($dateEndRaw, true),
  146. 'register_start' => $this->normalizeDateBoundary($registerStartRaw, false),
  147. 'register_end' => $this->normalizeDateBoundary($registerEndRaw, true),
  148. 'has_fbclid' => (int)$request->input('has_fbclid', 1),
  149. 'origin' => trim((string)$request->input('Origin', '')),
  150. 'ip' => trim((string)$request->input('IP', '')),
  151. 'ua' => trim((string)$request->input('UA', '')),
  152. 'param_category' => trim((string)$request->input('param_category', '')),
  153. 'param_key' => trim((string)$request->input('param_key', '')),
  154. 'param_value' => trim((string)$request->input('param_value', '')),
  155. 'page_size' => max(20, min((int)$request->input('page_size', 100), 500)),
  156. ];
  157. }
  158. protected function applyFilters($query, array $filters)
  159. {
  160. if ($filters['user_id'] !== '') {
  161. $query->where('ac.UserID', $filters['user_id']);
  162. }
  163. if ($filters['game_id'] !== '') {
  164. if ($filters['resolved_user_id'] !== '') {
  165. $query->where('ac.UserID', $filters['resolved_user_id']);
  166. } else {
  167. $query->whereRaw('1 = 0');
  168. }
  169. }
  170. if (!empty($filters['url_signs'])) {
  171. $query->whereIn('ac.UrlSign', $filters['url_signs']);
  172. }
  173. if (!empty($filters['account_channels'])) {
  174. $query->whereIn('ai.Channel', $filters['account_channels']);
  175. }
  176. if ($filters['platform'] !== '') {
  177. $query->where('ac.Platform', $filters['platform']);
  178. }
  179. if ($filters['date_start'] !== '') {
  180. $query->where('ac.CreateTime', '>=', $filters['date_start']);
  181. }
  182. if ($filters['date_end'] !== '') {
  183. $query->where('ac.CreateTime', '<=', $filters['date_end']);
  184. }
  185. if ($filters['register_start'] !== '') {
  186. $query->where('ai.RegisterDate', '>=', $filters['register_start']);
  187. }
  188. if ($filters['register_end'] !== '') {
  189. $query->where('ai.RegisterDate', '<=', $filters['register_end']);
  190. }
  191. if ($filters['has_fbclid'] === 1) {
  192. $query->where('ac.Params', 'like', '%fbclid%');
  193. } elseif ($filters['has_fbclid'] === 2) {
  194. $query->where('ac.Params', 'not like', '%fbclid%');
  195. }
  196. if ($filters['origin'] !== '') {
  197. $query->where('ac.Origin', 'like', '%' . $filters['origin'] . '%');
  198. }
  199. if ($filters['ip'] !== '') {
  200. $query->where('ac.IP', 'like', '%' . $filters['ip'] . '%');
  201. }
  202. if ($filters['ua'] !== '') {
  203. $query->where(function ($subQuery) use ($filters) {
  204. $subQuery->where('ac.ClickUA', 'like', '%' . $filters['ua'] . '%')
  205. ->orWhere('ac.GameUA', 'like', '%' . $filters['ua'] . '%');
  206. });
  207. }
  208. if ($filters['param_key'] !== '') {
  209. $query->where('ac.Params', 'like', '%"' . $filters['param_key'] . '"%');
  210. }
  211. if ($filters['param_value'] !== '') {
  212. $query->where('ac.Params', 'like', '%' . $filters['param_value'] . '%');
  213. }
  214. if ($filters['param_category'] !== '') {
  215. $keys = $this->getCategoryKeys($filters['param_category']);
  216. if (empty($keys)) {
  217. $query->whereRaw('1 = 0');
  218. } else {
  219. $query->where(function ($subQuery) use ($keys) {
  220. foreach ($keys as $key) {
  221. $subQuery->orWhere('ac.Params', 'like', '%"' . $key . '"%');
  222. }
  223. });
  224. }
  225. }
  226. }
  227. protected function buildStats($query)
  228. {
  229. $rows = $query->get([
  230. 'ac.ID',
  231. 'ac.UserID',
  232. 'ac.FPID',
  233. 'ac.FF',
  234. 'ac.UrlSign',
  235. 'ac.Platform',
  236. 'ac.IP',
  237. 'ac.Origin',
  238. 'ac.ClickUA',
  239. 'ac.GameUA',
  240. 'ac.Params',
  241. ]);
  242. $stats = [
  243. 'total' => $rows->count(),
  244. 'unique_users' => $rows->pluck('UserID')->filter()->unique()->count(),
  245. 'unique_ips' => $rows->pluck('IP')->filter()->unique()->count(),
  246. 'unique_cookies' => $rows->map(function ($row) {
  247. return $this->makeCookieKey($row);
  248. })->filter()->unique()->count(),
  249. 'registered_users' => $rows->filter(function ($row) {
  250. return !empty($row->UserID);
  251. })->pluck('UserID')->unique()->count(),
  252. 'paid_users' => 0,
  253. 'fbclid_count' => 0,
  254. 'fb_inapp_count' => 0,
  255. 'ig_inapp_count' => 0,
  256. 'platforms' => [],
  257. 'url_signs' => [],
  258. 'origins' => [],
  259. 'utm_sources' => [],
  260. 'param_categories' => [],
  261. 'param_keys' => [],
  262. 'param_category_stats' => [],
  263. 'duplicate_fbclid_groups' => 0,
  264. 'duplicate_fbclid_rows' => 0,
  265. 'duplicate_ff_groups' => 0,
  266. 'duplicate_ff_rows' => 0,
  267. 'fbclid_cookie_issues' => 0,
  268. ];
  269. $fbclidBuckets = [];
  270. $ffBuckets = [];
  271. foreach ($rows as $row) {
  272. $params = $this->decodeJson($row->Params);
  273. $uaInfo = $this->analyzeUserAgent($row->ClickUA ?: $row->GameUA);
  274. $paramAnalysis = $this->analyzeMarketingParams($params);
  275. $cookieMap = $this->parseCookieString($row->Cookie ?? '');
  276. if (stripos($row->Params ?? '', 'fbclid') !== false) {
  277. $stats['fbclid_count']++;
  278. }
  279. if ($uaInfo['app'] === 'Facebook') {
  280. $stats['fb_inapp_count']++;
  281. }
  282. if ($uaInfo['app'] === 'Instagram') {
  283. $stats['ig_inapp_count']++;
  284. }
  285. $this->incrementBucket($stats['platforms'], $row->Platform ?: 'unknown');
  286. $this->incrementBucket($stats['url_signs'], (string)($row->UrlSign ?: 'unknown'));
  287. $this->incrementBucket($stats['origins'], $row->Origin ?: 'unknown');
  288. $this->incrementBucket($stats['utm_sources'], $params['utm_source'] ?? 'unknown');
  289. foreach ($paramAnalysis['categories'] as $category => $entries) {
  290. $label = $this->getParamCategoryLabel($category);
  291. $this->incrementBucket($stats['param_categories'], $label);
  292. if (!isset($stats['param_category_stats'][$category])) {
  293. $stats['param_category_stats'][$category] = [
  294. 'label' => $label,
  295. 'keys' => [],
  296. 'values' => [],
  297. ];
  298. }
  299. foreach ($entries as $entry) {
  300. $this->incrementBucket($stats['param_category_stats'][$category]['keys'], $entry['key']);
  301. $this->incrementBucket($stats['param_keys'], $entry['key']);
  302. $valueLabel = $entry['key'] . '=' . $this->truncateParamValue($entry['value']);
  303. $this->incrementBucket($stats['param_category_stats'][$category]['values'], $valueLabel);
  304. }
  305. }
  306. $fbclid = $paramAnalysis['primary']['fbclid'];
  307. if ($fbclid !== '') {
  308. $fbclidBuckets[$fbclid] = ($fbclidBuckets[$fbclid] ?? 0) + 1;
  309. }
  310. $ff = trim((string)($row->FF ?? ''));
  311. if ($ff !== '') {
  312. $ffBuckets[$ff] = ($ffBuckets[$ff] ?? 0) + 1;
  313. }
  314. $fbclidCookieCheck = $this->validateFbclidCookieConsistency(
  315. $paramAnalysis['primary']['fbclid'],
  316. $cookieMap['_fbc'] ?? ''
  317. );
  318. if (!$fbclidCookieCheck['ok']) {
  319. $stats['fbclid_cookie_issues']++;
  320. }
  321. }
  322. $stats['paid_users'] = $this->loadPaidUserCount(
  323. $rows->pluck('UserID')->filter()->map(function ($userId) {
  324. return (int)$userId;
  325. })->unique()->values()->all()
  326. );
  327. arsort($stats['platforms']);
  328. arsort($stats['url_signs']);
  329. arsort($stats['origins']);
  330. arsort($stats['utm_sources']);
  331. arsort($stats['param_categories']);
  332. arsort($stats['param_keys']);
  333. foreach ($stats['param_category_stats'] as &$categoryStats) {
  334. arsort($categoryStats['keys']);
  335. arsort($categoryStats['values']);
  336. }
  337. unset($categoryStats);
  338. foreach ($fbclidBuckets as $count) {
  339. if ($count > 1) {
  340. $stats['duplicate_fbclid_groups']++;
  341. $stats['duplicate_fbclid_rows'] += $count;
  342. }
  343. }
  344. foreach ($ffBuckets as $count) {
  345. if ($count > 1) {
  346. $stats['duplicate_ff_groups']++;
  347. $stats['duplicate_ff_rows'] += $count;
  348. }
  349. }
  350. return $stats;
  351. }
  352. protected function buildFbclidGroups($query)
  353. {
  354. $rows = $query->get([
  355. 'ac.ID',
  356. 'ac.Params',
  357. ]);
  358. $groupsByFbclid = [];
  359. foreach ($rows as $row) {
  360. $params = $this->decodeJson($row->Params);
  361. $fbclid = $this->analyzeMarketingParams($params)['primary']['fbclid'];
  362. if ($fbclid === '') {
  363. continue;
  364. }
  365. $groupsByFbclid[$fbclid][] = (int)$row->ID;
  366. }
  367. return $this->finalizeDuplicateGroups($groupsByFbclid, 'fbclid');
  368. }
  369. protected function buildDuplicateValueGroups($query, $field, $type)
  370. {
  371. $rows = $query->get([
  372. 'ac.ID',
  373. 'ac.' . $field,
  374. ]);
  375. $groups = [];
  376. foreach ($rows as $row) {
  377. $value = trim((string)($row->{$field} ?? ''));
  378. if ($value === '') {
  379. continue;
  380. }
  381. $groups[$value][] = (int)$row->ID;
  382. }
  383. return $this->finalizeDuplicateGroups($groups, $type);
  384. }
  385. protected function finalizeDuplicateGroups(array $groupedIds, $type)
  386. {
  387. $palettes = [
  388. 'fbclid' => ['#fff3cd', '#d1ecf1', '#d4edda', '#f8d7da', '#e2d9f3', '#fde2b8', '#d6eaf8', '#f5c6cb'],
  389. 'ff' => ['#e8f5e9', '#e3f2fd', '#fff8e1', '#fce4ec', '#ede7f6', '#e0f7fa', '#f1f8e9', '#fff3e0'],
  390. ];
  391. $palette = $palettes[$type] ?? ['#f5f5f5'];
  392. $groups = [];
  393. $rowMap = [];
  394. $groupIndex = 1;
  395. foreach ($groupedIds as $value => $ids) {
  396. $ids = array_values(array_unique($ids));
  397. if (count($ids) < 2) {
  398. continue;
  399. }
  400. $color = $palette[($groupIndex - 1) % count($palette)];
  401. $group = [
  402. 'index' => $groupIndex,
  403. 'value' => $value,
  404. 'count' => count($ids),
  405. 'color' => $color,
  406. 'row_ids' => $ids,
  407. 'type' => $type,
  408. ];
  409. $groups[] = $group;
  410. foreach ($ids as $id) {
  411. $rowMap[$id] = $group;
  412. }
  413. $groupIndex++;
  414. }
  415. return [
  416. 'groups' => $groups,
  417. 'rows' => $rowMap,
  418. ];
  419. }
  420. protected function validateFbclidCookieConsistency($paramFbclid, $cookieFbc)
  421. {
  422. $paramFbclid = trim((string)$paramFbclid);
  423. $cookieFbc = trim((string)$cookieFbc);
  424. $cookieFbclid = $this->extractFbclidFromFbc($cookieFbc);
  425. if ($paramFbclid === '' && $cookieFbclid === '') {
  426. return [
  427. 'ok' => true,
  428. 'status' => 'none',
  429. 'cookie_fbclid' => '',
  430. 'message' => '',
  431. ];
  432. }
  433. if ($paramFbclid !== '' && $cookieFbclid === '') {
  434. return [
  435. 'ok' => false,
  436. 'status' => 'missing_cookie_fbc',
  437. 'cookie_fbclid' => '',
  438. 'message' => 'Params 有 fbclid,但 Cookie 未解析出 _fbc/fbclid',
  439. ];
  440. }
  441. if ($paramFbclid === '' && $cookieFbclid !== '') {
  442. return [
  443. 'ok' => false,
  444. 'status' => 'missing_param_fbclid',
  445. 'cookie_fbclid' => $cookieFbclid,
  446. 'message' => 'Cookie 有 _fbc/fbclid,但 Params 缺少 fbclid',
  447. ];
  448. }
  449. if ($paramFbclid === $cookieFbclid) {
  450. return [
  451. 'ok' => true,
  452. 'status' => 'match',
  453. 'cookie_fbclid' => $cookieFbclid,
  454. 'message' => '匹配',
  455. ];
  456. }
  457. return [
  458. 'ok' => false,
  459. 'status' => 'mismatch',
  460. 'cookie_fbclid' => $cookieFbclid,
  461. 'message' => 'Params.fbclid 与 Cookie._fbc 中的 fbclid 不一致',
  462. ];
  463. }
  464. protected function extractFbclidFromFbc($cookieFbc)
  465. {
  466. $cookieFbc = trim((string)$cookieFbc);
  467. if ($cookieFbc === '') {
  468. return '';
  469. }
  470. $parts = explode('.', $cookieFbc, 4);
  471. if (count($parts) === 4 && $parts[0] === 'fb' && in_array($parts[1], ['1', '2'], true)) {
  472. return trim((string)$parts[3]);
  473. }
  474. return $cookieFbc;
  475. }
  476. protected function incrementBucket(array &$bucket, $key)
  477. {
  478. $bucket[$key] = ($bucket[$key] ?? 0) + 1;
  479. }
  480. protected function splitCsv($value)
  481. {
  482. return array_values(array_filter(array_map('trim', explode(',', (string)$value)), function ($item) {
  483. return $item !== '';
  484. }));
  485. }
  486. protected function normalizeDateBoundary($value, $endOfDay = false)
  487. {
  488. $value = trim((string)$value);
  489. if ($value === '') {
  490. return '';
  491. }
  492. $value = substr(str_replace('T', ' ', $value), 0, 10);
  493. return $value . ($endOfDay ? ' 23:59:59' : ' 00:00:00');
  494. }
  495. protected function decodeJson($json)
  496. {
  497. $data = json_decode((string)$json, true);
  498. return is_array($data) ? $data : [];
  499. }
  500. protected function analyzeMarketingParams(array $params)
  501. {
  502. $analysis = [
  503. 'flat' => [],
  504. 'categories' => [],
  505. 'primary' => [
  506. 'channel' => '',
  507. 'utm_source' => '',
  508. 'utm_medium' => '',
  509. 'utm_campaign' => '',
  510. 'campaign' => '',
  511. 'adgroup' => '',
  512. 'creative' => '',
  513. 'pixel' => '',
  514. 'fbclid' => '',
  515. ],
  516. ];
  517. foreach ($params as $key => $value) {
  518. if (is_array($value) || is_object($value)) {
  519. $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  520. }
  521. $key = trim((string)$key);
  522. $value = trim((string)$value);
  523. if ($key === '' || $value === '') {
  524. continue;
  525. }
  526. $category = $this->detectParamCategory($key);
  527. $analysis['flat'][$key] = $value;
  528. $analysis['categories'][$category][] = [
  529. 'key' => $key,
  530. 'value' => $value,
  531. ];
  532. }
  533. $analysis['primary']['channel'] = $analysis['flat']['c'] ?? ($analysis['flat']['channel'] ?? '');
  534. $analysis['primary']['utm_source'] = $analysis['flat']['utm_source'] ?? '';
  535. $analysis['primary']['utm_medium'] = $analysis['flat']['utm_medium'] ?? '';
  536. $analysis['primary']['utm_campaign'] = $analysis['flat']['utm_campaign'] ?? '';
  537. $analysis['primary']['campaign'] = $analysis['flat']['campaign'] ?? ($analysis['flat']['utm_campaign'] ?? '');
  538. $analysis['primary']['adgroup'] = $analysis['flat']['adgroup'] ?? ($analysis['flat']['adset'] ?? ($analysis['flat']['utm_term'] ?? ''));
  539. $analysis['primary']['creative'] = $analysis['flat']['creative'] ?? ($analysis['flat']['utm_content'] ?? '');
  540. $analysis['primary']['pixel'] = $analysis['flat']['pixel'] ?? ($analysis['flat']['pixelID'] ?? ($analysis['flat']['pixel_id'] ?? ''));
  541. $analysis['primary']['fbclid'] = $analysis['flat']['fbclid'] ?? '';
  542. return $analysis;
  543. }
  544. protected function detectParamCategory($key)
  545. {
  546. $normalizedKey = strtolower(trim((string)$key));
  547. if (strpos($normalizedKey, 'utm_') === 0) {
  548. return 'utm';
  549. }
  550. if (in_array($normalizedKey, ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'], true)) {
  551. return 'campaign';
  552. }
  553. if (in_array($normalizedKey, ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelid', 'pixel_id'], true)) {
  554. return 'attribution';
  555. }
  556. if (in_array($normalizedKey, ['c', 'channel', 'source', 'media_source', 'utm_source_platform', 'pid', 'af_channel', 'sub_channel'], true)) {
  557. return 'channel';
  558. }
  559. if (preg_match('/(_id|id)$/', $normalizedKey)) {
  560. return 'identifier';
  561. }
  562. return 'custom';
  563. }
  564. protected function getParamCategoryLabel($category)
  565. {
  566. $labels = [
  567. 'utm' => 'UTM',
  568. 'campaign' => 'Campaign',
  569. 'attribution' => 'Attribution',
  570. 'channel' => 'Channel',
  571. 'identifier' => 'Identifier',
  572. 'custom' => 'Custom',
  573. ];
  574. return $labels[$category] ?? ucfirst($category);
  575. }
  576. protected function getCategoryKeys($category)
  577. {
  578. $map = [
  579. 'utm' => ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_id', 'utm_source_platform'],
  580. 'campaign' => ['campaign', 'adgroup', 'adset', 'ad_id', 'creative', 'creative_id', 'placement', 'site_source_name'],
  581. 'attribution' => ['fbclid', 'gclid', 'ttclid', 'msclkid', 'wbraid', 'gbraid', 'fbc', 'fbp', '_fbc', '_fbp', 'pixel', 'pixelID', 'pixel_id'],
  582. 'channel' => ['c', 'channel', 'source', 'media_source', 'pid', 'af_channel', 'sub_channel'],
  583. 'identifier' => ['utm_id', 'campaign_id', 'adgroup_id', 'adset_id', 'creative_id', 'pixel_id', 'pixelID'],
  584. ];
  585. return $map[$category] ?? [];
  586. }
  587. protected function truncateParamValue($value, $length = 36)
  588. {
  589. $value = (string)$value;
  590. if ($value === '') {
  591. return '-';
  592. }
  593. return mb_strlen($value) > $length ? mb_substr($value, 0, $length) . '...' : $value;
  594. }
  595. protected function makeCookieKey($row)
  596. {
  597. if (!empty($row->FPID)) {
  598. return 'fpid:' . $row->FPID;
  599. }
  600. if (!empty($row->FF)) {
  601. return 'ff:' . $row->FF;
  602. }
  603. if (!empty($row->UserID)) {
  604. return 'uid:' . $row->UserID;
  605. }
  606. return 'row:' . ($row->ID ?? '');
  607. }
  608. protected function parseCookieString($cookieString)
  609. {
  610. $cookies = [];
  611. foreach (explode(';', (string)$cookieString) as $part) {
  612. $part = trim($part);
  613. if ($part === '' || strpos($part, '=') === false) {
  614. continue;
  615. }
  616. [$name, $value] = explode('=', $part, 2);
  617. $cookies[trim($name)] = trim($value);
  618. }
  619. return $cookies;
  620. }
  621. protected function analyzeUserAgent($ua)
  622. {
  623. $ua = (string)$ua;
  624. $app = 'Browser';
  625. if (stripos($ua, 'Instagram') !== false) {
  626. $app = 'Instagram';
  627. } elseif (stripos($ua, 'FBAN') !== false || stripos($ua, 'FBAV') !== false || stripos($ua, 'Facebook') !== false) {
  628. $app = 'Facebook';
  629. }
  630. $os = 'Unknown';
  631. if (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false || stripos($ua, 'iOS') !== false) {
  632. $os = 'iOS';
  633. } elseif (stripos($ua, 'Android') !== false) {
  634. $os = 'Android';
  635. }
  636. $device = 'Unknown';
  637. if (stripos($ua, 'iPhone') !== false) {
  638. $device = 'iPhone';
  639. } elseif (stripos($ua, 'iPad') !== false) {
  640. $device = 'iPad';
  641. } elseif (stripos($ua, 'Android') !== false) {
  642. $device = 'Android';
  643. }
  644. return compact('app', 'os', 'device');
  645. }
  646. protected function loadPaidUserCount(array $userIds)
  647. {
  648. if (empty($userIds)) {
  649. return 0;
  650. }
  651. return DB::connection('read')
  652. ->table('agent.dbo.order')
  653. ->whereIn('user_id', $userIds)
  654. ->where('pay_status', 1)
  655. ->distinct('user_id')
  656. ->count('user_id');
  657. }
  658. protected function loadPayStats(array $userIds)
  659. {
  660. if (empty($userIds)) {
  661. return [];
  662. }
  663. return DB::connection('read')
  664. ->table('agent.dbo.order')
  665. ->whereIn('user_id', $userIds)
  666. ->where('pay_status', 1)
  667. ->groupBy('user_id')
  668. ->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')
  669. ->get()
  670. ->keyBy('user_id')
  671. ->all();
  672. }
  673. protected function loadAdjustEventStats(array $userIds, array $filters)
  674. {
  675. if (empty($userIds)) {
  676. return [];
  677. }
  678. $result = [];
  679. $userIdMap = array_fill_keys(array_map('strval', $userIds), true);
  680. foreach ($this->resolveAdjustLogFiles($filters) as $logFile) {
  681. $handle = @fopen($logFile, 'r');
  682. if ($handle === false) {
  683. continue;
  684. }
  685. $currentUserId = null;
  686. while (($line = fgets($handle)) !== false) {
  687. $userId = $this->extractAdjustUserId($line);
  688. $status = $this->classifyAdjustLine($line);
  689. if ($userId !== null && isset($userIdMap[(string)$userId])) {
  690. $currentUserId = $userId;
  691. } elseif ($userId === null && $currentUserId !== null && in_array($status, ['request', 'success', 'skipped', 'failed'], true)) {
  692. $userId = $currentUserId;
  693. } else {
  694. if ($status === 'enter') {
  695. $currentUserId = null;
  696. }
  697. continue;
  698. }
  699. if (!isset($result[$userId])) {
  700. $result[$userId] = ['status' => 'enter', 'logs' => []];
  701. }
  702. if ($status !== null) {
  703. $result[$userId]['status'] = $status;
  704. }
  705. if (count($result[$userId]['logs']) < 50) {
  706. $result[$userId]['logs'][] = trim($line);
  707. }
  708. }
  709. fclose($handle);
  710. }
  711. return $result;
  712. }
  713. protected function resolveAdjustLogFiles(array $filters)
  714. {
  715. $start = substr($filters['date_start'] ?: $filters['register_start'], 0, 10);
  716. $end = substr($filters['date_end'] ?: $filters['register_end'], 0, 10);
  717. if ($start === '') {
  718. $start = date('Y-m-d');
  719. }
  720. if ($end === '') {
  721. $end = $start;
  722. }
  723. $startTs = strtotime($start);
  724. $endTs = strtotime($end);
  725. if ($startTs === false || $endTs === false) {
  726. return [];
  727. }
  728. if ($endTs < $startTs) {
  729. [$startTs, $endTs] = [$endTs, $startTs];
  730. }
  731. $files = [];
  732. $days = 0;
  733. for ($time = $startTs; $time <= $endTs && $days < 7; $time += 86400, $days++) {
  734. $file = storage_path('logs/adjustEvent-' . date('Y-m-d', $time) . '.log');
  735. if (file_exists($file)) {
  736. $files[] = $file;
  737. }
  738. }
  739. return $files;
  740. }
  741. protected function extractAdjustUserId($line)
  742. {
  743. if (preg_match('/"UserID":"?(\d+)"?/', $line, $matches)) {
  744. return (int)$matches[1];
  745. }
  746. if (preg_match('/"user_id":"?(\d+)"?/', $line, $matches)) {
  747. return (int)$matches[1];
  748. }
  749. return null;
  750. }
  751. protected function classifyAdjustLine($line)
  752. {
  753. if (strpos($line, 'facebook s2s response') !== false) {
  754. return 'success';
  755. }
  756. if (strpos($line, 'facebook s2s request') !== false) {
  757. return 'request';
  758. }
  759. if (strpos($line, 'facebook s2s skipped') !== false) {
  760. return 'skipped';
  761. }
  762. if (strpos($line, 'facebook s2s failed') !== false) {
  763. return 'failed';
  764. }
  765. if (strpos($line, 'enter:') !== false) {
  766. return 'enter';
  767. }
  768. return null;
  769. }
  770. protected function extractChannelFromCookie($userID, $user = null)
  771. {
  772. $cookieInfo = \App\Services\ApkService::loadCookie($userID, $user->FPID ?? '', $user->FF ?? '');
  773. if (!$cookieInfo || empty($cookieInfo['Params'])) {
  774. return null;
  775. }
  776. $params = json_decode($cookieInfo['Params'], true);
  777. if (!is_array($params)) {
  778. return null;
  779. }
  780. return $params['c'] ?? null;
  781. }
  782. }