修复adjust归因失败
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
/**
|
||||
* Adjust / MMP 归因 URL 合并:宏占位符替换与安全 query 重建
|
||||
*/
|
||||
class AttributionUrlMerger
|
||||
{
|
||||
/** @var string[] */
|
||||
private static $adjustHostSuffixes = [
|
||||
'adjust.com',
|
||||
'go.link',
|
||||
'aindl.com',
|
||||
];
|
||||
|
||||
/** @var string[] 广告平台 click ID,透传到 DB_FP 落地页供 Adjust JS 读取 */
|
||||
private static $passthroughKeys = [
|
||||
'fbclid',
|
||||
'gclid',
|
||||
'wbraid',
|
||||
'gbraid',
|
||||
'ttclid',
|
||||
];
|
||||
|
||||
/** @var string[] cloak 内部参数,不进入最终 Adjust URL */
|
||||
private static $stripKeys = [
|
||||
'ad_spm_id',
|
||||
'time',
|
||||
'utm_source',
|
||||
'utm_medium',
|
||||
'utm_campaign',
|
||||
'utm_term',
|
||||
'utm_content',
|
||||
'site_source',
|
||||
'placement',
|
||||
'campaign_id',
|
||||
'adgroup_id',
|
||||
'network',
|
||||
];
|
||||
|
||||
/** @var array<string,string> Adjust 默认 p1–p6 → Facebook 宏 */
|
||||
private static $defaultMacroParams = [
|
||||
'p1' => '{{campaign.name}}',
|
||||
'p2' => '{{campaign.id}}',
|
||||
'p3' => '{{adset.name}}',
|
||||
'p4' => '{{adset.id}}',
|
||||
'p5' => '{{ad.name}}',
|
||||
'p6' => '{{ad.id}}',
|
||||
];
|
||||
|
||||
/**
|
||||
* 目标 URL 是否需要归因宏合并
|
||||
*/
|
||||
public static function needsAttributionMerge($targetUrl)
|
||||
{
|
||||
$targetUrl = trim((string) $targetUrl);
|
||||
if ($targetUrl === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stripos($targetUrl, '%7B%7B') !== false || stripos($targetUrl, '%7D%7D') !== false) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('/\{\{[a-zA-Z0-9_.]+\}\}/', $targetUrl)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parts = parse_url($targetUrl);
|
||||
$host = strtolower((string) ($parts['host'] ?? ''));
|
||||
|
||||
return self::isAdjustHost($host);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 DB_FP URL 解析需追加到广告链接的宏参数(键名 => Facebook 宏模板)
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function parseAdjustMacroKeys($targetUrl)
|
||||
{
|
||||
$targetUrl = trim((string) $targetUrl);
|
||||
if ($targetUrl === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = parse_url($targetUrl);
|
||||
$query = [];
|
||||
if (!empty($parts['query'])) {
|
||||
parse_str($parts['query'], $query);
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
foreach ($query as $key => $value) {
|
||||
if (!self::isMacroPlaceholder($value)) {
|
||||
continue;
|
||||
}
|
||||
$macro = self::extractMacroTemplate($value);
|
||||
if ($macro !== '') {
|
||||
$keys[(string) $key] = $macro;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($keys) && self::isAdjustHost(strtolower((string) ($parts['host'] ?? '')))) {
|
||||
return self::$defaultMacroParams;
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将落地页 query 合并进目标 URL,替换宏占位符
|
||||
*
|
||||
* @param array<string,mixed> $incomingQuery
|
||||
*/
|
||||
public static function merge($targetUrl, array $incomingQuery)
|
||||
{
|
||||
$targetUrl = trim((string) $targetUrl);
|
||||
if ($targetUrl === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parts = parse_url($targetUrl);
|
||||
if ($parts === false) {
|
||||
return $targetUrl;
|
||||
}
|
||||
|
||||
$targetQuery = [];
|
||||
if (!empty($parts['query'])) {
|
||||
parse_str($parts['query'], $targetQuery);
|
||||
}
|
||||
|
||||
$merged = [];
|
||||
foreach ($targetQuery as $key => $value) {
|
||||
$key = (string) $key;
|
||||
$value = (string) $value;
|
||||
|
||||
if (self::isMacroPlaceholder($value) && self::hasIncomingValue($incomingQuery, $key)) {
|
||||
$merged[$key] = (string) $incomingQuery[$key];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!self::isMacroPlaceholder($value)) {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (self::$passthroughKeys as $ptKey) {
|
||||
if (!self::hasIncomingValue($incomingQuery, $ptKey)) {
|
||||
continue;
|
||||
}
|
||||
$merged[$ptKey] = (string) $incomingQuery[$ptKey];
|
||||
}
|
||||
|
||||
return self::buildUrl($parts, $merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $query
|
||||
*/
|
||||
private static function buildUrl(array $parts, array $query)
|
||||
{
|
||||
$scheme = $parts['scheme'] ?? 'https';
|
||||
$host = $parts['host'] ?? '';
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
$path = $parts['path'] ?? '';
|
||||
$frag = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
|
||||
|
||||
$pairs = [];
|
||||
foreach ($query as $key => $value) {
|
||||
if (in_array((string) $key, self::$stripKeys, true)) {
|
||||
continue;
|
||||
}
|
||||
$pairs[] = rawurlencode((string) $key) . '=' . rawurlencode((string) $value);
|
||||
}
|
||||
|
||||
$queryStr = implode('&', $pairs);
|
||||
$url = $scheme . '://' . $host . $port . $path;
|
||||
if ($queryStr !== '') {
|
||||
$url .= '?' . $queryStr;
|
||||
}
|
||||
|
||||
return $url . $frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function isMacroPlaceholder($value)
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^\{\{[a-zA-Z0-9_.]+\}\}$/', $value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$decoded = rawurldecode($value);
|
||||
if ($decoded !== $value && preg_match('/^\{\{[a-zA-Z0-9_.]+\}\}$/', $decoded)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) preg_match('/^%7B%7B[a-zA-Z0-9_.]+%7D%7D$/i', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function extractMacroTemplate($value)
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('/^\{\{([a-zA-Z0-9_.]+)\}\}$/', $value, $m)) {
|
||||
return '{{' . $m[1] . '}}';
|
||||
}
|
||||
|
||||
$decoded = rawurldecode($value);
|
||||
if (preg_match('/^\{\{([a-zA-Z0-9_.]+)\}\}$/', $decoded, $m)) {
|
||||
return '{{' . $m[1] . '}}';
|
||||
}
|
||||
|
||||
if (preg_match('/^%7B%7B([a-zA-Z0-9_.]+)%7D%7D$/i', $value, $m)) {
|
||||
return '{{' . $m[1] . '}}';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $incomingQuery
|
||||
*/
|
||||
private static function hasIncomingValue(array $incomingQuery, $key)
|
||||
{
|
||||
if (!array_key_exists($key, $incomingQuery)) {
|
||||
return false;
|
||||
}
|
||||
$value = $incomingQuery[$key];
|
||||
if ($value === null || $value === '') {
|
||||
return false;
|
||||
}
|
||||
if (is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
if (self::isMacroPlaceholder($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function isAdjustHost($host)
|
||||
{
|
||||
if ($host === '') {
|
||||
return false;
|
||||
}
|
||||
foreach (self::$adjustHostSuffixes as $suffix) {
|
||||
if ($host === $suffix || substr($host, -strlen('.' . $suffix)) === '.' . $suffix) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/AttributionUrlMerger.php';
|
||||
require_once dirname(__DIR__) . '/CloakAdSourceGuard.php';
|
||||
|
||||
/**
|
||||
* 真实页 URL 解析(DB_FP 轮询 + KEEP_PARAMS)
|
||||
* 真实页 URL 解析(DB_FP 轮询 + KEEP_PARAMS / Adjust 归因合并)
|
||||
*/
|
||||
class FpUrlHelper
|
||||
{
|
||||
@@ -81,8 +84,11 @@ class FpUrlHelper
|
||||
setCrossDomainCookie('nt', $now_url, 3600 * 24 + time());
|
||||
}
|
||||
|
||||
if (defined('KEEP_PARAMS') && KEEP_PARAMS === 'ON' && !empty($_GET) && $fp_url !== '') {
|
||||
$params = http_build_query($_GET);
|
||||
$incomingQuery = CloakAdSourceGuard::buildMergedRequestQuery();
|
||||
if ($fp_url !== '' && AttributionUrlMerger::needsAttributionMerge($fp_url)) {
|
||||
$fp_url = AttributionUrlMerger::merge($fp_url, $incomingQuery);
|
||||
} elseif (defined('KEEP_PARAMS') && KEEP_PARAMS === 'ON' && !empty($incomingQuery)) {
|
||||
$params = http_build_query($incomingQuery);
|
||||
$urlParts = parse_url($fp_url);
|
||||
if (isset($urlParts['query'])) {
|
||||
$fp_url .= '&' . $params;
|
||||
|
||||
@@ -35,10 +35,12 @@ class FpPageRenderer
|
||||
exit;
|
||||
}
|
||||
if ($show_content == '302') {
|
||||
header('location:' . $fp_url);
|
||||
header('Referrer-Policy: origin-when-cross-origin');
|
||||
header('Location: ' . $fp_url, true, 302);
|
||||
exit;
|
||||
}
|
||||
if ($show_content == 'js') {
|
||||
$fp_url_js = htmlspecialchars($fp_url, ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<SCRIPT LANGUAGE="JavaScript">
|
||||
var time = 1;
|
||||
@@ -48,7 +50,7 @@ class FpPageRenderer
|
||||
timelong ++;
|
||||
}
|
||||
function redirect(){
|
||||
window.location.href="<?php echo $fp_url; ?>";
|
||||
window.location.href="<?php echo $fp_url_js; ?>";
|
||||
}
|
||||
timer=setInterval('diplaytime()', 300);
|
||||
timer=setTimeout('redirect()',time * 300);
|
||||
|
||||
@@ -82,7 +82,8 @@ class RouteResolver
|
||||
exit;
|
||||
}
|
||||
if ($redirect_method == '302') {
|
||||
header('Location: ' . $zp_url);
|
||||
header('Referrer-Policy: origin-when-cross-origin');
|
||||
header('Location: ' . $zp_url, true, 302);
|
||||
exit;
|
||||
}
|
||||
if ($redirect_method == 'js') {
|
||||
|
||||
@@ -129,11 +129,12 @@ class CloakAdSourceGuard
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $enabledPlatforms PLATFORM_* 常量列表
|
||||
* @param string $spmId
|
||||
* @param array $enabledPlatforms PLATFORM_* 常量列表
|
||||
* @param string $spmId
|
||||
* @param string|array|null $fpUrls DB_FP 真实页 URL(用于 Adjust 宏检测)
|
||||
* @return string query 字符串(不含前导 ?)
|
||||
*/
|
||||
public static function buildAdLinkQuery(array $enabledPlatforms, $spmId)
|
||||
public static function buildAdLinkQuery(array $enabledPlatforms, $spmId, $fpUrls = null)
|
||||
{
|
||||
$spmId = trim((string) $spmId);
|
||||
if (empty($enabledPlatforms)) {
|
||||
@@ -154,6 +155,10 @@ class CloakAdSourceGuard
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array(self::PLATFORM_FB, $enabledPlatforms, true)) {
|
||||
$parts = array_merge($parts, self::buildAdjustMacroQueryParts($fpUrls));
|
||||
}
|
||||
|
||||
if ($spmId !== '') {
|
||||
$parts[] = 'ad_spm_id=' . rawurlencode($spmId);
|
||||
}
|
||||
@@ -161,6 +166,41 @@ class CloakAdSourceGuard
|
||||
return implode('&', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* DB_FP 为 Adjust 链接时,追加 p1–p6 等 Facebook 宏到广告链接
|
||||
*
|
||||
* @param string|array|null $fpUrls
|
||||
* @return string[]
|
||||
*/
|
||||
private static function buildAdjustMacroQueryParts($fpUrls)
|
||||
{
|
||||
require_once __DIR__ . '/Cloak/AttributionUrlMerger.php';
|
||||
|
||||
$fpUrl = '';
|
||||
if (is_array($fpUrls)) {
|
||||
foreach ($fpUrls as $url) {
|
||||
$url = trim((string) $url);
|
||||
if ($url !== '') {
|
||||
$fpUrl = $url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fpUrl = trim((string) $fpUrls);
|
||||
}
|
||||
|
||||
if ($fpUrl === '' || !AttributionUrlMerger::needsAttributionMerge($fpUrl)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
foreach (AttributionUrlMerger::parseAdjustMacroKeys($fpUrl) as $key => $macro) {
|
||||
$parts[] = rawurlencode((string) $key) . '=' . $macro;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 10–12 位随机字母数字 ad_spm_id
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user