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:
@@ -312,6 +312,10 @@ class SchemaPayment {
|
||||
]],
|
||||
['key' => 'pay_rebate_show_users', 'label' => '用户中心显示我推荐的用户', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_rebate_judgment', 'value' => 'all']],
|
||||
['key' => 'pay_rebate_self_s', 'label' => '自己推广自己', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true], 'help' => '允许使用自己的推广链接购买商品并获得佣金'],
|
||||
['key' => 'referral_attribution_model', 'label' => '推广归因模式', 'type' => 'radio', 'default' => 'first_touch', 'options' => [
|
||||
['label' => '首次触点(首触优先)', 'value' => 'first_touch'],
|
||||
['label' => '最近触点(末触覆盖)', 'value' => 'last_touch'],
|
||||
], 'help' => '首触:访客首次通过推广链接进入后,归因固定且不被后续链接覆盖;末触:最近一次点击的推广码覆盖之前的归因'],
|
||||
]],
|
||||
['key' => 'rules', 'title' => '返佣规则', 'fields' => [
|
||||
['key' => 'rebate_rule', 'label' => '为不同用户类型设置返佣规则', 'type' => 'tabbed', 'tabs' => [
|
||||
@@ -344,6 +348,7 @@ class SchemaPayment {
|
||||
]],
|
||||
]],
|
||||
['key' => 'affiliate_settlement_days', 'label' => '结算观察期(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 90, 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true], 'help' => '订单完成后经过观察期,佣金才会结算到可提现余额'],
|
||||
['key' => 'affiliate_disabled_code_stops_commission', 'label' => '推广码禁用后停止返佣', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true], 'help' => '开启后,推广者的推广码被禁用(或缺失)时,其名下已绑定用户的新订单不再产生佣金;关闭则绑定关系继续生效'],
|
||||
]],
|
||||
['key' => 'text', 'title' => '返佣文案', 'fields' => [
|
||||
['key' => 'pay_rebate_text_desc', 'label' => '返佣文案(一句话简介)', 'type' => 'input', 'default' => '加入分享计划,获得高额奖励', 'dependsOn' => ['key' => 'pay_rebate_s', 'value' => true]],
|
||||
|
||||
@@ -691,11 +691,24 @@ trait ScriptsPart1b {
|
||||
|
||||
// profile 页:加载 credits.js(签到 + 邀请 + 提现交互)
|
||||
if ($is_profile_page) {
|
||||
$affiliate_deps = [];
|
||||
if ($affiliate_on) {
|
||||
// 推广二维码生成库(本地打包,无外部 CDN)。
|
||||
$qrcode_js_path = get_template_directory() . '/assets/js/vendor/qrcode.min.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-qrcode',
|
||||
$base_uri . '/assets/js/vendor/qrcode.min.js',
|
||||
[],
|
||||
file_exists($qrcode_js_path) ? (string) filemtime($qrcode_js_path) : $version,
|
||||
true
|
||||
);
|
||||
$affiliate_deps[] = 'aksrt-qrcode';
|
||||
}
|
||||
$credits_js_path = get_template_directory() . '/assets/js/credits.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-credits',
|
||||
$base_uri . '/assets/js/credits.js',
|
||||
['loader_js', 'aksrt-vip-coordinator'],
|
||||
array_merge(['loader_js', 'aksrt-vip-coordinator'], $affiliate_deps),
|
||||
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
||||
true
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -339,9 +339,16 @@ class PaymentRuntime {
|
||||
],
|
||||
'order_types' => $types_by_level[0],
|
||||
'order_types_by_level' => $types_by_level,
|
||||
'disabled_code_stops_commission' => !empty($settings['affiliate_disabled_code_stops_commission']),
|
||||
'window_days' => (int) CreditsRuntime::get_referral_settings()['window_days'],
|
||||
'settlement_days' => max(0, min(90, (int) ($settings['affiliate_settlement_days'] ?? 7))),
|
||||
'promotion_text' => sanitize_textarea_field((string) ($settings['pay_rebate_text_details'] ?? '')),
|
||||
'poster' => [
|
||||
'enabled' => !isset($settings['pay_rebate_poster_s']) || !empty($settings['pay_rebate_poster_s']),
|
||||
'image' => esc_url_raw((string) ($settings['pay_rebate_poster_img'] ?? '')),
|
||||
'title' => sanitize_text_field((string) ($settings['pay_rebate_poster_title'] ?? '')),
|
||||
'description' => sanitize_text_field((string) ($settings['pay_rebate_poster_desc'] ?? '')),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user