170 lines
5.8 KiB
PHP
170 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
use think\Log;
|
||
|
||
/**
|
||
* 分流中转页服务端像素回传(Facebook CAPI / TikTok Events API)
|
||
*/
|
||
class SplitPixelPostbackService
|
||
{
|
||
/**
|
||
* @param array{facebook: array<int, array<string, mixed>>, tiktok: array<int, array<string, mixed>>} $config
|
||
*/
|
||
public static function dispatch(array $config, string $clientIp, string $userAgent, string $eventSourceUrl = ''): void
|
||
{
|
||
foreach (SplitPixelConfigService::getEnabledSorted($config, SplitPixelConfigService::PLATFORM_FACEBOOK) as $row) {
|
||
if ((int) ($row['server_postback'] ?? 0) !== 1) {
|
||
continue;
|
||
}
|
||
$token = trim((string) ($row['access_token'] ?? ''));
|
||
if ($token === '') {
|
||
continue;
|
||
}
|
||
self::sendFacebook($row, $token, $clientIp, $userAgent, $eventSourceUrl);
|
||
}
|
||
|
||
foreach (SplitPixelConfigService::getEnabledSorted($config, SplitPixelConfigService::PLATFORM_TIKTOK) as $row) {
|
||
if ((int) ($row['server_postback'] ?? 0) !== 1) {
|
||
continue;
|
||
}
|
||
$token = trim((string) ($row['access_token'] ?? ''));
|
||
if ($token === '') {
|
||
continue;
|
||
}
|
||
self::sendTikTok($row, $token, $clientIp, $userAgent, $eventSourceUrl);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $row
|
||
*/
|
||
private static function sendFacebook(array $row, string $accessToken, string $clientIp, string $userAgent, string $eventSourceUrl): void
|
||
{
|
||
$pixelId = trim((string) ($row['pixel_id'] ?? ''));
|
||
if ($pixelId === '') {
|
||
return;
|
||
}
|
||
|
||
$eventName = (string) ($row['event'] ?? 'PageView');
|
||
$testCode = trim((string) ($row['test_code'] ?? ''));
|
||
|
||
$payload = [
|
||
'data' => [[
|
||
'event_name' => $eventName,
|
||
'event_time' => time(),
|
||
'action_source' => 'website',
|
||
'user_data' => array_filter([
|
||
'client_ip_address' => $clientIp !== '' ? $clientIp : null,
|
||
'client_user_agent' => $userAgent !== '' ? $userAgent : null,
|
||
]),
|
||
]],
|
||
];
|
||
if ($eventSourceUrl !== '') {
|
||
$payload['data'][0]['event_source_url'] = $eventSourceUrl;
|
||
}
|
||
if ($testCode !== '') {
|
||
$payload['test_event_code'] = $testCode;
|
||
}
|
||
|
||
$url = 'https://graph.facebook.com/v19.0/' . rawurlencode($pixelId) . '/events?access_token=' . rawurlencode($accessToken);
|
||
self::postJson($url, $payload, [], 'facebook', $pixelId);
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $row
|
||
*/
|
||
private static function sendTikTok(array $row, string $accessToken, string $clientIp, string $userAgent, string $eventSourceUrl): void
|
||
{
|
||
$pixelId = trim((string) ($row['pixel_id'] ?? ''));
|
||
if ($pixelId === '') {
|
||
return;
|
||
}
|
||
|
||
$event = self::mapTikTokServerEvent((string) ($row['event'] ?? 'PageView'));
|
||
$testCode = trim((string) ($row['test_code'] ?? ''));
|
||
|
||
$payload = [
|
||
'pixel_code' => $pixelId,
|
||
'event' => $event,
|
||
'event_id' => uniqid('split_', true),
|
||
'timestamp' => gmdate('c'),
|
||
'context' => array_filter([
|
||
'ip' => $clientIp !== '' ? $clientIp : null,
|
||
'user_agent' => $userAgent !== '' ? $userAgent : null,
|
||
'page' => $eventSourceUrl !== '' ? ['url' => $eventSourceUrl] : null,
|
||
]),
|
||
];
|
||
if ($testCode !== '') {
|
||
$payload['test_event_code'] = $testCode;
|
||
}
|
||
|
||
$url = 'https://business-api.tiktok.com/open_api/v1.3/event/track/';
|
||
self::postJson($url, $payload, [
|
||
'Access-Token: ' . $accessToken,
|
||
'Content-Type: application/json',
|
||
], 'tiktok', $pixelId);
|
||
}
|
||
|
||
private static function mapTikTokServerEvent(string $event): string
|
||
{
|
||
$map = [
|
||
'PageView' => 'Pageview',
|
||
'Lead' => 'SubmitForm',
|
||
'Contact' => 'Contact',
|
||
'AddToCart' => 'AddToCart',
|
||
'Purchase' => 'CompletePayment',
|
||
'Subscribe' => 'Subscribe',
|
||
];
|
||
|
||
return $map[$event] ?? 'Pageview';
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $payload
|
||
* @param array<int, string> $headers
|
||
*/
|
||
private static function postJson(string $url, array $payload, array $headers, string $platform, string $pixelId): void
|
||
{
|
||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if ($json === false) {
|
||
return;
|
||
}
|
||
|
||
$ch = curl_init($url);
|
||
if ($ch === false) {
|
||
return;
|
||
}
|
||
|
||
$defaultHeaders = ['Content-Type: application/json'];
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $json,
|
||
CURLOPT_HTTPHEADER => array_merge($defaultHeaders, $headers),
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 3,
|
||
CURLOPT_CONNECTTIMEOUT => 2,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$errno = curl_errno($ch);
|
||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($errno !== 0 || ($httpCode >= 400 && $httpCode !== 0)) {
|
||
Log::error(sprintf(
|
||
'Split pixel postback failed platform=%s pixel=%s http=%d curl=%d',
|
||
$platform,
|
||
$pixelId,
|
||
$httpCode,
|
||
$errno
|
||
));
|
||
} elseif (is_string($response) && strpos($response, '"error"') !== false) {
|
||
Log::error(sprintf('Split pixel postback api error platform=%s pixel=%s', $platform, $pixelId));
|
||
}
|
||
}
|
||
}
|