perf(revenue): server-render the promotion link and stop settling on reads #37
@@ -115,6 +115,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Forward-only affiliate revenue center =====
|
// ===== Forward-only affiliate revenue center =====
|
||||||
|
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
|
||||||
|
* 海报配置与佣金比例,取失败也不影响复制。 */
|
||||||
function initAffiliateRevenue() {
|
function initAffiliateRevenue() {
|
||||||
var root = document.querySelector('[data-aksrt-revenue-page]');
|
var root = document.querySelector('[data-aksrt-revenue-page]');
|
||||||
if (!root || root.dataset.affiliateBound) return;
|
if (!root || root.dataset.affiliateBound) return;
|
||||||
@@ -123,16 +125,14 @@
|
|||||||
var copy = root.querySelector('[data-affiliate-copy]');
|
var copy = root.querySelector('[data-affiliate-copy]');
|
||||||
var feedback = root.querySelector('[data-affiliate-feedback]');
|
var feedback = root.querySelector('[data-affiliate-feedback]');
|
||||||
function setFeedback(text, error) { if (feedback) { feedback.textContent = text; feedback.classList.toggle('is-error', !!error); } }
|
function setFeedback(text, error) { if (feedback) { feedback.textContent = text; feedback.classList.toggle('is-error', !!error); } }
|
||||||
|
var url = input ? input.value : '';
|
||||||
|
if (copy) copy.disabled = !url;
|
||||||
api('/revenue/me').then(function (r) {
|
api('/revenue/me').then(function (r) {
|
||||||
if (!r.ok) { setFeedback((r.json && r.json.message) || '推广数据加载失败。', true); return; }
|
if (!r.ok) return;
|
||||||
var j = r.json || {};
|
var j = r.json || {};
|
||||||
if (input) {
|
|
||||||
input.value = j.url || '';
|
|
||||||
}
|
|
||||||
if (copy) copy.disabled = !j.url;
|
|
||||||
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
|
||||||
initAffiliatePoster(root, j.url || '', j.poster || {}, j.site_name || document.title);
|
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title);
|
||||||
}).catch(function () { setFeedback('推广数据加载失败。', true); });
|
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
|
||||||
if (copy) copy.addEventListener('click', function () {
|
if (copy) copy.addEventListener('click', function () {
|
||||||
if (!input || !input.value) return;
|
if (!input || !input.value) return;
|
||||||
var done = function () { setFeedback('推广链接已复制。'); };
|
var done = function () { setFeedback('推广链接已复制。'); };
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ $details_title = trim((string) ($affiliate['details_title'] ?? ''));
|
|||||||
if ($details_title === '') {
|
if ($details_title === '') {
|
||||||
$details_title = __('返佣详解', 'aksrt');
|
$details_title = __('返佣详解', 'aksrt');
|
||||||
}
|
}
|
||||||
|
/* 推广链接服务端直出:避免为了一个本地拼接的 URL 多绕一次 REST 往返。 */
|
||||||
|
$affiliate_link_url = '';
|
||||||
|
if (class_exists('\AKSRT\Payment\ReferralAttribution')) {
|
||||||
|
$affiliate_link_url = (string) (\AKSRT\Payment\ReferralAttribution::get_instance()->create_link($user_id)['url'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
$revenue_request = is_array($context['revenue_request'] ?? null) ? $context['revenue_request'] : [];
|
$revenue_request = is_array($context['revenue_request'] ?? null) ? $context['revenue_request'] : [];
|
||||||
$active_reward_tab = sanitize_key((string) ($revenue_request['reward_tab'] ?? ($_GET['reward_tab'] ?? 'detail')));
|
$active_reward_tab = sanitize_key((string) ($revenue_request['reward_tab'] ?? ($_GET['reward_tab'] ?? 'detail')));
|
||||||
@@ -144,7 +149,7 @@ $status_labels = [
|
|||||||
<div class="uc-revenue-summary-list">
|
<div class="uc-revenue-summary-list">
|
||||||
<div class="uc-revenue-summary-row uc-revenue-summary-row--link">
|
<div class="uc-revenue-summary-row uc-revenue-summary-row--link">
|
||||||
<span><?php esc_html_e('推广链接', 'aksrt'); ?></span>
|
<span><?php esc_html_e('推广链接', 'aksrt'); ?></span>
|
||||||
<input type="text" readonly data-affiliate-link aria-label="<?php esc_attr_e('推广链接', 'aksrt'); ?>">
|
<input type="text" readonly data-affiliate-link value="<?php echo esc_attr($affiliate_link_url); ?>" aria-label="<?php esc_attr_e('推广链接', 'aksrt'); ?>">
|
||||||
<button type="button" class="btn-primary btn-sm" data-affiliate-copy><?php esc_html_e('复制链接', 'aksrt'); ?></button>
|
<button type="button" class="btn-primary btn-sm" data-affiliate-copy><?php esc_html_e('复制链接', 'aksrt'); ?></button>
|
||||||
<?php if (!empty($affiliate['poster']['enabled'])) : ?><button type="button" class="btn-outline btn-sm" data-affiliate-poster hidden><?php esc_html_e('推广海报', 'aksrt'); ?></button><?php endif; ?>
|
<?php if (!empty($affiliate['poster']['enabled'])) : ?><button type="button" class="btn-outline btn-sm" data-affiliate-poster hidden><?php esc_html_e('推广海报', 'aksrt'); ?></button><?php endif; ?>
|
||||||
<small data-affiliate-feedback role="status" aria-live="polite"></small>
|
<small data-affiliate-feedback role="status" aria-live="polite"></small>
|
||||||
|
|||||||
@@ -103,14 +103,6 @@ final class CommissionLedger {
|
|||||||
foreach ((array) $rows as $row) if ($this->settle_one((int)$row['id'])) $count++;
|
foreach ((array) $rows as $row) if ($this->settle_one((int)$row['id'])) $count++;
|
||||||
return $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 = '') {
|
private function settle_one(int $id, string $operation_key = '') {
|
||||||
global $wpdb; $table = $this->table('commission_ledger');
|
global $wpdb; $table = $this->table('commission_ledger');
|
||||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id=%d", $id), ARRAY_A);
|
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id=%d", $id), ARRAY_A);
|
||||||
@@ -241,7 +233,6 @@ final class CommissionLedger {
|
|||||||
return $negative ? -$minor : $minor;
|
return $negative ? -$minor : $minor;
|
||||||
}
|
}
|
||||||
public function overview(int $user_id): array {
|
public function overview(int $user_id): array {
|
||||||
$this->lazy_settle_due();
|
|
||||||
global $wpdb; $table=$this->table('commission_ledger');
|
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) ?: [];
|
$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';
|
$withdrawn = '0.00';
|
||||||
|
|||||||
@@ -291,24 +291,6 @@ final class ReferralAttribution {
|
|||||||
return $bound > 0 && $bound !== $user_id ? $bound : 0;
|
return $bound > 0 && $bound !== $user_id ? $bound : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stats(int $promoter_id): array {
|
|
||||||
global $wpdb;
|
|
||||||
$table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
|
|
||||||
if ($promoter_id <= 0 || $table === '') {
|
|
||||||
return ['promoters' => 0, 'effective_payers' => 0];
|
|
||||||
}
|
|
||||||
$promoters = (int) $wpdb->get_var($wpdb->prepare(
|
|
||||||
"SELECT COUNT(*) FROM {$table} WHERE promoter_id = %d AND status = 'bound'",
|
|
||||||
$promoter_id
|
|
||||||
));
|
|
||||||
$ledger = \AKSRT\Core\DBMigrator::table('commission_ledger');
|
|
||||||
$payers = $ledger !== '' ? (int) $wpdb->get_var($wpdb->prepare(
|
|
||||||
"SELECT COUNT(DISTINCT source_user_id) FROM {$ledger} WHERE promoter_id = %d AND original_amount > 0",
|
|
||||||
$promoter_id
|
|
||||||
)) : 0;
|
|
||||||
return ['promoters' => $promoters, 'effective_payers' => $payers];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int,max_pages:int} */
|
/** @return array{items:array<int,array<string,mixed>>,total:int,page:int,per_page:int,max_pages:int} */
|
||||||
public function list_referred_users(int $promoter_id, int $page = 1, int $per_page = 10): array {
|
public function list_referred_users(int $promoter_id, int $page = 1, int $per_page = 10): array {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
@@ -361,10 +343,8 @@ final class ReferralAttribution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function rest_me(): \WP_REST_Response {
|
public function rest_me(): \WP_REST_Response {
|
||||||
\AKSRT\Payment\CommissionLedger::get_instance()->lazy_settle_due();
|
|
||||||
$user_id = get_current_user_id();
|
$user_id = get_current_user_id();
|
||||||
$link = $this->create_link($user_id);
|
$link = $this->create_link($user_id);
|
||||||
$stats = $this->stats($user_id);
|
|
||||||
$affiliate = \AKSRT\Settings\Settings::get_affiliate_settings();
|
$affiliate = \AKSRT\Settings\Settings::get_affiliate_settings();
|
||||||
$registration = $this->registration_settings();
|
$registration = $this->registration_settings();
|
||||||
$reward = $this->reward_settings();
|
$reward = $this->reward_settings();
|
||||||
@@ -386,7 +366,6 @@ final class ReferralAttribution {
|
|||||||
'window_days' => (int) ($this->shared_settings()['window_days'] ?? 30),
|
'window_days' => (int) ($this->shared_settings()['window_days'] ?? 30),
|
||||||
'settlement_days' => (int) ($affiliate['settlement_days'] ?? 7),
|
'settlement_days' => (int) ($affiliate['settlement_days'] ?? 7),
|
||||||
],
|
],
|
||||||
'stats' => $stats,
|
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,19 +61,19 @@ test('commission base is the discounted payable amount, not the list price', ()
|
|||||||
assert.match(ledger, /AssetAmount::balance\(is_string\(\$order\['amount'\] \?\? null\) \? \$order\['amount'\] : ''\)/)
|
assert.match(ledger, /AssetAmount::balance\(is_string\(\$order\['amount'\] \?\? null\) \? \$order\['amount'\] : ''\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('lazy settlement backstop guards the earnings entry points', () => {
|
test('settlement stays off the read path and runs on the cron schedule', () => {
|
||||||
const ledger = read('src', 'Payment', 'CommissionLedger.php')
|
const ledger = read('src', 'Payment', 'CommissionLedger.php')
|
||||||
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
|
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
|
||||||
|
|
||||||
assert.match(ledger, /private static bool \$lazy_settled = false/)
|
// 懒结算兜底已移除:读路径不得顺带写库(收益页打开慢的主因)。
|
||||||
assert.match(ledger, /public function lazy_settle_due\(\): void/)
|
assert.doesNotMatch(ledger, /lazy_settle_due|\$lazy_settled/)
|
||||||
assert.match(ledger, /if \(!\\AKSRT\\Settings\\Settings::affiliate_enabled\(\)\) return;/)
|
// overview() 与 rest_me() 都不再触发结算。
|
||||||
assert.ok(
|
assert.doesNotMatch(ledger, /\$this->lazy_settle_due\(\)/)
|
||||||
ledger.indexOf('lazy_settle_due()') < ledger.indexOf('global \$wpdb; \$table=\$this->table\(\'commission_ledger\'\); \$row=\$wpdb->get_row'),
|
assert.doesNotMatch(attribution, /lazy_settle_due/)
|
||||||
'overview() must trigger the backstop before aggregating balances'
|
assert.doesNotMatch(attribution, /->stats\(/)
|
||||||
)
|
// 结算只由 hourly 的 wp-cron 事件驱动。
|
||||||
// 收益总览与我的推广两个入口都接入了兜底。
|
assert.match(ledger, /add_action\('aksrt_affiliate_settle_due', \[\$this, 'settle_due'\]\)/)
|
||||||
assert.match(attribution, /CommissionLedger::get_instance\(\)->lazy_settle_due\(\)/)
|
assert.match(ledger, /wp_schedule_event\(time\(\) \+ HOUR_IN_SECONDS, 'hourly', 'aksrt_affiliate_settle_due'\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('referral codes are self-serve: no admin ops page, granted at registration', () => {
|
test('referral codes are self-serve: no admin ops page, granted at registration', () => {
|
||||||
|
|||||||
@@ -51,6 +51,21 @@ test('revenue detail card is a four-row info list with no standalone QR block',
|
|||||||
assert.match(script, /data-affiliate-poster-download/)
|
assert.match(script, /data-affiliate-poster-download/)
|
||||||
assert.match(script, /openModal\(modal\)/)
|
assert.match(script, /openModal\(modal\)/)
|
||||||
assert.match(script, /link\.download = 'referral-poster\.png'/)
|
assert.match(script, /link\.download = 'referral-poster\.png'/)
|
||||||
|
// 链接由 PHP 直出:复制按钮立即可用,REST 只负责海报配置与佣金比例。
|
||||||
|
assert.match(script, /var url = input \? input\.value : '';/)
|
||||||
|
assert.match(script, /if \(copy\) copy\.disabled = !url;/)
|
||||||
|
assert.doesNotMatch(script, /input\.value = j\.url/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('promotion link is server-rendered so it needs no round trip', () => {
|
||||||
|
const template = read('inc', 'helpers', 'user-center-renderers', 'profile-revenue.php')
|
||||||
|
// input 带服务端 value,不依赖 JS 回填。
|
||||||
|
assert.match(template, /data-affiliate-link value="<\?php echo esc_attr\(\$affiliate_link_url\); \?>"/)
|
||||||
|
assert.match(template, /create_link\(\$user_id\)\['url'\]/)
|
||||||
|
const attribution = read('src', 'Payment', 'ReferralAttribution.php')
|
||||||
|
// rest_me 不再做 COUNT 聚合,也不触发结算。
|
||||||
|
assert.doesNotMatch(attribution, /\$this->stats\(\$user_id\)/)
|
||||||
|
assert.doesNotMatch(attribution, /lazy_settle_due\(\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('poster canvas is built from admin-configured copy and gated on the poster switch', () => {
|
test('poster canvas is built from admin-configured copy and gated on the poster switch', () => {
|
||||||
@@ -67,7 +82,7 @@ test('poster canvas is built from admin-configured copy and gated on the poster
|
|||||||
|
|
||||||
const script = read('assets', 'js', 'credits.js')
|
const script = read('assets', 'js', 'credits.js')
|
||||||
// 海报仅在开关开启时暴露按钮;绘制含顶部图、文案换行、二维码与站点名。
|
// 海报仅在开关开启时暴露按钮;绘制含顶部图、文案换行、二维码与站点名。
|
||||||
assert.match(script, /initAffiliatePoster\(root, j\.url \|\| '', j\.poster \|\| \{\}, j\.site_name \|\| document\.title\)/)
|
assert.match(script, /initAffiliatePoster\(root, url \|\| j\.url \|\| '', j\.poster \|\| \{\}, j\.site_name \|\| document\.title\)/)
|
||||||
assert.match(script, /poster\.enabled !== true\) return/)
|
assert.match(script, /poster\.enabled !== true\) return/)
|
||||||
assert.match(script, /function buildPosterCanvas/)
|
assert.match(script, /function buildPosterCanvas/)
|
||||||
assert.match(script, /function wrapText\(ctx, text, maxWidth\)/)
|
assert.match(script, /function wrapText\(ctx, text, maxWidth\)/)
|
||||||
|
|||||||
Reference in New Issue
Block a user