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

+14
View File
@@ -57,6 +57,11 @@ final class CommissionLedger {
if ($order_no === '' || $buyer <= 0 || $amount->minor() <= 0) return 0;
$promoter = ReferralAttribution::get_instance()->resolve($buyer);
if ($promoter <= 0) return 0;
// 开关开启时,推广者的推广码已被禁用(或缺失)则不再产生新佣金。
if (!empty($settings['disabled_code_stops_commission'])
&& !ReferralAttribution::get_instance()->is_promoter_code_active($promoter)) {
return 0;
}
// 返佣订单类型按推广者会员等级分档(参考主题:每档独立勾选)。
$promoter_level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($promoter) : 0;
if (!in_array($order_type, \AKSRT\Settings\PaymentRuntime::affiliate_order_types_for_level($promoter_level), true)) return 0;
@@ -98,6 +103,14 @@ final class CommissionLedger {
foreach ((array) $rows as $row) if ($this->settle_one((int)$row['id'])) $count++;
return $count;
}
/** 懒结算兜底:收益相关入口顺带结算到期佣金,弥补 wp-cron 在低流量站点的时效缺口。 */
private static bool $lazy_settled = false;
public function lazy_settle_due(): void {
if (self::$lazy_settled) return;
self::$lazy_settled = true;
if (!\AKSRT\Settings\Settings::affiliate_enabled()) return;
$this->settle_due(20);
}
private function settle_one(int $id, string $operation_key = '') {
global $wpdb; $table = $this->table('commission_ledger');
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id=%d", $id), ARRAY_A);
@@ -228,6 +241,7 @@ final class CommissionLedger {
return $negative ? -$minor : $minor;
}
public function overview(int $user_id): array {
$this->lazy_settle_due();
global $wpdb; $table=$this->table('commission_ledger');
$row=$wpdb->get_row($wpdb->prepare("SELECT COALESCE(SUM(original_amount+adjustment_total),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN original_amount+adjustment_total ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN original_amount+adjustment_total ELSE 0 END),0) settled FROM {$table} WHERE promoter_id=%d",$user_id),ARRAY_A) ?: [];
$withdrawn = '0.00';
+32 -5
View File
@@ -51,7 +51,12 @@ final class ReferralAttribution {
return;
}
if ($this->cookie_ref() !== '') {
$current = $this->cookie_ref();
if ($current === $raw) {
return;
}
// 首触模式:已有归因不覆盖;末触模式:最近一次点击覆盖之前的归因。
if ($current !== '' && $this->attribution_model() !== 'last_touch') {
return;
}
@@ -75,21 +80,28 @@ final class ReferralAttribution {
$submitted_ref = sanitize_key($submitted_ref);
$cookie_ref = $this->cookie_ref();
$submitted_valid = $submitted_ref !== '' && $this->resolve_promoter($submitted_ref) > 0;
$last_touch = $this->attribution_model() === 'last_touch';
$conflict = $cookie_ref !== '' && $submitted_ref !== '' && $submitted_ref !== $cookie_ref;
if ($conflict) {
self::$registration_notice = __('检测到不同的推广码,已按首次访问的推广关系绑定。', 'aksrt');
self::$registration_notice = $last_touch
? __('检测到不同的推广码,已按最近访问的推广关系绑定。', 'aksrt')
: __('检测到不同的推广码,已按首次访问的推广关系绑定。', 'aksrt');
}
$ref = $cookie_ref !== '' ? $cookie_ref : ($submitted_valid ? $submitted_ref : '');
// 首触:cookie 优先;末触:注册时填写的有效推广码优先。
$ref = $cookie_ref !== '' && !($last_touch && $submitted_valid)
? $cookie_ref
: ($submitted_valid ? $submitted_ref : $cookie_ref);
self::$pending_registration_ref = $ref;
$source = $ref === '' ? 'none' : ($ref === $cookie_ref ? 'cookie' : 'submitted');
return [
'ref' => $ref,
'valid' => $ref !== '',
'submitted_valid' => $submitted_valid,
'conflict' => $conflict,
'source' => $cookie_ref !== '' ? 'cookie' : ($submitted_valid ? 'submitted' : 'none'),
'source' => $source,
];
}
@@ -377,6 +389,7 @@ final class ReferralAttribution {
}
public function rest_me(): \WP_REST_Response {
\AKSRT\Payment\CommissionLedger::get_instance()->lazy_settle_due();
$user_id = get_current_user_id();
$link = $this->create_link($user_id);
$stats = $this->stats($user_id);
@@ -387,6 +400,8 @@ final class ReferralAttribution {
'ref' => $link['ref'],
'url' => $link['url'],
'status' => $link['status'],
'site_name' => get_bloginfo('name'),
'poster' => (array) ($affiliate['poster'] ?? ['enabled' => false]),
'features' => [
'registration' => !empty($registration['enabled']),
'points_reward' => !empty($reward['enabled']),
@@ -612,7 +627,19 @@ final class ReferralAttribution {
private function shared_settings(): array {
return class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_referral_settings()
: ['code_length' => 12, 'window_days' => 30];
: ['code_length' => 12, 'window_days' => 30, 'attribution_model' => 'first_touch'];
}
private function attribution_model(): string {
return (string) ($this->shared_settings()['attribution_model'] ?? 'first_touch');
}
/** 推广者的推广码当前是否处于激活状态(码缺失视为未激活)。 */
public function is_promoter_code_active(int $promoter_id): bool {
if ($promoter_id <= 0) {
return false;
}
return $this->code_record($promoter_id)['status'] === self::STATUS_ACTIVE;
}
private function registration_settings(): array {