修复adjust归因失败

This commit is contained in:
root
2026-06-27 23:58:54 +08:00
parent 51a6a850b3
commit c3c58198ab
9 changed files with 439 additions and 145 deletions
+266
View File
@@ -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 默认 p1p6 → 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;
}
}
+9 -3
View File
@@ -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;
+4 -2
View File
@@ -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);
+2 -1
View File
@@ -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') {
+43 -3
View File
@@ -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 链接时,追加 p1p6 等 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;
}
/**
* 生成 1012 位随机字母数字 ad_spm_id
*/