perf(revenue): server-render the promotion link and stop settling on reads
Opening the 推广收益 tab was slow because every read path ran settlement and two COUNT aggregates before the browser could show the link. - profile-revenue.php: the link input now carries a server-rendered value from ReferralAttribution::create_link(), so 复制链接 works on first paint instead of after a REST round trip - CommissionLedger::overview() no longer calls lazy_settle_due(); the lazy-settlement hook and its static flag are gone. Settlement runs only on the existing hourly aksrt_affiliate_settle_due cron, which uses a larger batch (100 vs 20) and would otherwise re-run up to 20 locked SELECT..FOR UPDATE + write cycles inside a GET request - ReferralAttribution::rest_me() drops the lazy_settle_due() call and the stats() result; stats() itself is deleted — its two COUNTs (one of them COUNT(DISTINCT source_user_id)) had no consumer left once the 推广人数 row was removed - credits.js: reads the link from the already-rendered input and only fetches /revenue/me for poster config and commission ratio. A failed fetch no longer blocks copying — it silently skips the poster enhancement - tests: the lazy-settlement contract is inverted to assert the read path stays read-only and cron stays wired; new assertions pin the server-rendered value and the no-COUNT REST payload Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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