feat(affiliate): attribution modes, settlement hardening, trusted proxies, QR and poster

- 归因模式可配置(first_touch/last_touch)、停佣开关、懒结算与佣金基数降级复查
- aksrt_get_client_ip 接入 aksrt_trusted_proxies 信任代理名单,防 CDN 头伪造
- 收益页 Canvas 二维码 + 750x1000 推广海报(本地打包 qrcode.min.js,无 CDN)
- 契约测试固化归因/海报/信任代理链路
This commit is contained in:
kate522 committed 2026-09-21 20:53:22 +08:00
1 parent 2618530afb
commit 20387fdb06
14 files changed
+715 -6

No files matched your search

@@ -145,6 +145,17 @@ $status_labels = [
<button type="button" class="btn-primary btn-sm" data-affiliate-copy><?php esc_html_e('复制', 'aksrt'); ?></button>
<small data-affiliate-feedback role="status" aria-live="polite"></small>
</div>
<div class="uc-revenue-summary-row uc-revenue-summary-row--qr" data-affiliate-qr-row>
<span><?php esc_html_e('推广二维码', 'aksrt'); ?></span>
<div class="uc-revenue-qr">
<canvas data-affiliate-qr width="148" height="148" aria-label="<?php esc_attr_e('推广链接二维码', 'aksrt'); ?>"></canvas>
<div class="uc-revenue-qr__actions">
<button type="button" class="btn-sm" data-affiliate-qr-download><?php esc_html_e('下载二维码', 'aksrt'); ?></button>
<button type="button" class="btn-sm" data-affiliate-poster hidden><?php esc_html_e('生成推广海报', 'aksrt'); ?></button>
</div>
<small data-affiliate-qr-feedback role="status" aria-live="polite"></small>
</div>
</div>
<?php if ($affiliate_on) : ?><div class="uc-revenue-summary-row"><span><?php esc_html_e('佣金比例', 'aksrt'); ?></span><strong data-affiliate-ratio><?php echo esc_html((string) $commission_ratio); ?>%</strong></div><?php endif; ?>
<div class="uc-revenue-summary-row"><span><?php esc_html_e('推广人数', 'aksrt'); ?></span><div class="uc-revenue-count"><strong data-affiliate-promoters>—</strong><em><?php esc_html_e('人', 'aksrt'); ?></em></div></div>
<?php if ($affiliate_on) : ?>
+54
View File
@@ -570,6 +570,15 @@ add_action('admin_notices', static function () {
// ============================================================
function aksrt_get_client_ip(): string {
$remote = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '';
// 信任代理名单:过滤返回非空 CIDR/IP 列表时,仅当 REMOTE_ADDR 命中名单
// 才采纳 CDN / 反代头;否则客户端可伪造这些头绕过 IP 维度的风控。
$trusted = apply_filters('aksrt_trusted_proxies', []);
if (is_array($trusted) && $trusted !== [] && $remote !== '' && !aksrt_ip_in_list($remote, $trusted)) {
return $remote;
}
// Prefer CDN / reverse-proxy headers (Cloudflare first), then generic proxies.
$keys = [
'HTTP_CF_CONNECTING_IP',
@@ -601,6 +610,51 @@ function aksrt_get_client_ip(): string {
return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
/**
* IP 是否命中精确 IP 或 CIDR 名单(IPv4 与 IPv6)。
*
* @param string $ip 待判定 IP。
* @param array $list 精确 IP / CIDR 段列表。
*/
function aksrt_ip_in_list(string $ip, array $list): bool {
foreach ($list as $entry) {
$entry = trim((string) $entry);
if ($entry === '') {
continue;
}
if (!str_contains($entry, '/')) {
if (strcasecmp($entry, $ip) === 0) {
return true;
}
continue;
}
[$subnet, $prefix] = explode('/', $entry, 2);
$prefix = (int) $prefix;
$ip_binary = @inet_pton($ip);
$subnet_binary = @inet_pton($subnet);
if ($ip_binary === false || $subnet_binary === false || strlen($ip_binary) !== strlen($subnet_binary)) {
continue;
}
$max_bits = strlen($ip_binary) * 8;
if ($prefix < 0 || $prefix > $max_bits) {
continue;
}
$full_bytes = intdiv($prefix, 8);
$remain_bits = $prefix % 8;
if ($full_bytes > 0 && substr($ip_binary, 0, $full_bytes) !== substr($subnet_binary, 0, $full_bytes)) {
continue;
}
if ($remain_bits > 0) {
$mask = chr((0xFF << (8 - $remain_bits)) & 0xFF);
if ((substr($ip_binary, $full_bytes, 1) & $mask) !== (substr($subnet_binary, $full_bytes, 1) & $mask)) {
continue;
}
}
return true;
}
return false;
}
// Native wp-comments-post + any path that omits comment_author_IP.
// Ensures comment_author_IP (and thus aksrt_comment_addr) is the real client IP behind CDN.
add_filter('pre_comment_user_ip', static function ($ip) {