|
|
@@ -0,0 +1,222 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Game\Services;
|
|
|
+
|
|
|
+use App\Game\GlobalUserInfo;
|
|
|
+use App\Game\WebChannelConfig;
|
|
|
+use App\Http\helper\HttpCurl;
|
|
|
+use App\Jobs\FacebookServerEvent;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+
|
|
|
+class FacebookEventService
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 通过服务端 Facebook Conversions API 上报事件
|
|
|
+ *
|
|
|
+ * 去重策略(前后端统一):
|
|
|
+ * 仅依赖 Facebook 官方的 event_id 去重:
|
|
|
+ * - 前端 fbq 上报时必须使用同一个 event_id
|
|
|
+ * - 服务端调用 Conversions API 时也传入同一个 event_id
|
|
|
+ * - Facebook 会自动把 Browser + Server 事件按照 event_id 合并
|
|
|
+ *
|
|
|
+ * @param string $eventName
|
|
|
+ * @param string $eventId
|
|
|
+ * @param int|string $userId
|
|
|
+ * @param float|int|null $value
|
|
|
+ * @param string|null $currency
|
|
|
+ * @param string $pixelId
|
|
|
+ * @param string $accessToken
|
|
|
+ * @param array $extraCustomData
|
|
|
+ */
|
|
|
+ public static function trackEvent(
|
|
|
+ string $eventName,
|
|
|
+ string $eventId,
|
|
|
+ $userId,
|
|
|
+ $value = null,
|
|
|
+ ?string $currency = null,
|
|
|
+ string $pixelId = '',
|
|
|
+ string $accessToken = '',
|
|
|
+ array $extraCustomData = []
|
|
|
+ ): void {
|
|
|
+ if (empty($pixelId) || empty($accessToken)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $eventTime = time();
|
|
|
+ $externalId = (string)$userId;
|
|
|
+
|
|
|
+ // Facebook Conversions API 要求 user_data 做哈希,这里只对 external_id 做一次 sha256
|
|
|
+ $userData = [
|
|
|
+ 'external_id' => hash('sha256', $externalId),
|
|
|
+ ];
|
|
|
+
|
|
|
+ $customData = array_merge(
|
|
|
+ array_filter([
|
|
|
+ 'currency' => $currency,
|
|
|
+ 'value' => $value,
|
|
|
+ ], static function ($v) {
|
|
|
+ return $v !== null && $v !== '';
|
|
|
+ }),
|
|
|
+ $extraCustomData
|
|
|
+ );
|
|
|
+
|
|
|
+ $payload = [
|
|
|
+ 'data' => [
|
|
|
+ [
|
|
|
+ 'event_name' => $eventName,
|
|
|
+ 'event_time' => $eventTime,
|
|
|
+ 'action_source' => 'website',
|
|
|
+ 'event_id' => $eventId,
|
|
|
+ 'user_data' => $userData,
|
|
|
+ 'custom_data' => $customData,
|
|
|
+ ],
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+
|
|
|
+ $url = sprintf(
|
|
|
+ 'https://graph.facebook.com/v25.0/%s/events?access_token=%s',
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+
|
|
|
+ try {
|
|
|
+ $http = new HttpCurl();
|
|
|
+ $response = $http->curlPost($url, $payload, 'json', true);
|
|
|
+
|
|
|
+ Log::info('FacebookEventService trackEvent success', [
|
|
|
+ 'event_name' => $eventName,
|
|
|
+ 'event_id' => $eventId,
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'pixel_id' => $pixelId,
|
|
|
+ 'payload' => $payload,
|
|
|
+ 'response' => $response,
|
|
|
+ ]);
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::error('FacebookEventService trackEvent error', [
|
|
|
+ 'event_name' => $eventName,
|
|
|
+ 'event_id' => $eventId,
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'pixel_id' => $pixelId,
|
|
|
+ 'message' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册完成事件(对应前端 CompleteRegistration)
|
|
|
+ */
|
|
|
+ public static function trackCompleteRegistration(GlobalUserInfo $user, WebChannelConfig $config): void
|
|
|
+ {
|
|
|
+ if (empty($config->PlatformID) || empty($config->PlatformToken)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $pixelId = $config->PlatformID;
|
|
|
+ $accessToken = $config->PlatformToken;
|
|
|
+
|
|
|
+ // 建议前端也使用相同的 event_id:reg_{UserID}
|
|
|
+ $eventId = 'reg_' . $user->UserID;
|
|
|
+
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'CompleteRegistration',
|
|
|
+ $eventId,
|
|
|
+ $user->UserID,
|
|
|
+ null,
|
|
|
+ null,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 支付事件(对应前端 pay + firstpayD0/firstpayD1/payagain/Purchase)
|
|
|
+ *
|
|
|
+ * @param int|string $userId
|
|
|
+ * @param string $orderSn
|
|
|
+ * @param float|int $amount
|
|
|
+ * @param string $currency
|
|
|
+ * @param bool $isFirst
|
|
|
+ * @param bool $isD0
|
|
|
+ * @param int $channel
|
|
|
+ */
|
|
|
+ public static function trackPayEvent(
|
|
|
+ $userId,
|
|
|
+ string $orderSn,
|
|
|
+ $amount,
|
|
|
+ string $currency,
|
|
|
+ bool $isFirst,
|
|
|
+ bool $isD0,
|
|
|
+ int $channel
|
|
|
+ ): void {
|
|
|
+ $config = WebChannelConfig::getByChannel($channel);
|
|
|
+ if (!$config || empty($config->PlatformID) || empty($config->PlatformToken)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ $pixelId = $config->PlatformID;
|
|
|
+ $accessToken = $config->PlatformToken;
|
|
|
+
|
|
|
+ // 统一使用同一个 event_id(与前端保持一致)
|
|
|
+ // 前端所有支付相关事件共用:purchase_{order_sn}
|
|
|
+ $eventId = 'purchase_' . $orderSn;
|
|
|
+
|
|
|
+ // 标准 Purchase 事件(所有支付都会上报)
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'Purchase',
|
|
|
+ $eventId,
|
|
|
+ $userId,
|
|
|
+ $amount,
|
|
|
+ $currency,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+
|
|
|
+ // 首次支付
|
|
|
+ if ($isFirst) {
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'firstpayD1',
|
|
|
+ $eventId,
|
|
|
+ $userId,
|
|
|
+ $amount,
|
|
|
+ $currency,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+
|
|
|
+ if ($isD0) {
|
|
|
+ // D0 首充自定义事件
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'firstpayD0',
|
|
|
+ $eventId,
|
|
|
+ $userId,
|
|
|
+ $amount,
|
|
|
+ $currency,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+
|
|
|
+ // D0 首充标准事件:AddToWishlist(与前端保持一致)
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'AddToWishlist',
|
|
|
+ $eventId,
|
|
|
+ $userId,
|
|
|
+ $amount,
|
|
|
+ $currency,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } elseif (!$isD0) {
|
|
|
+ // 复充(非首次且非 D0)
|
|
|
+ FacebookServerEvent::dispatch(
|
|
|
+ 'payagain',
|
|
|
+ $eventId,
|
|
|
+ $userId,
|
|
|
+ $amount,
|
|
|
+ $currency,
|
|
|
+ $pixelId,
|
|
|
+ $accessToken
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|