diff --git a/assets/css/routes/user-center/promotion.css b/assets/css/routes/user-center/promotion.css
index 7b1306ad..7af47243 100644
--- a/assets/css/routes/user-center/promotion.css
+++ b/assets/css/routes/user-center/promotion.css
@@ -72,3 +72,24 @@
.uc-content .uc-promotion-copy { width: 100%; }
.uc-content .uc-promotion-record { align-items: flex-start; }
}
+
+/* New affiliate contract cards. */
+.uc-content .aksrt-affiliate-panel { display:grid; gap:18px; padding:22px; border:1px solid var(--border,rgba(0,0,0,.08)); border-radius:var(--card-radius,16px); background:var(--surface, #fff); box-shadow:0 8px 24px rgba(15,23,42,.06); }
+.uc-content .uc-panel-heading h2 { margin:0 0 4px; color:var(--text,#111827); }
+.uc-content .uc-panel-heading p,.uc-content .uc-affiliate-note { margin:0; color:var(--muted,#6b7280); line-height:1.6; }
+.uc-content .uc-affiliate-link-card { display:flex; align-items:end; gap:10px; padding:14px; border-radius:12px; background:var(--surface-muted,#f8fafc); }
+.uc-content .uc-affiliate-link-card>div { flex:1; min-width:0; display:grid; gap:6px; }
+.uc-content .uc-affiliate-link-card input { width:100%; min-width:0; height:40px; padding:0 10px; border:1px solid var(--border,#e5e7eb); border-radius:8px; background:var(--surface,#fff); color:var(--text,#111827); }
+.uc-content .uc-affiliate-link-card button { white-space:nowrap; }
+.uc-content .uc-affiliate-stats { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; }
+.uc-content .uc-affiliate-stats article { display:grid; gap:5px; padding:14px; border:1px solid var(--border,#e5e7eb); border-radius:12px; }
+.uc-content .uc-affiliate-stats article:first-child strong { color:var(--danger,#dc2626); }
+.uc-content .uc-affiliate-stats article strong { color:var(--primary,#2563eb); font-size:24px; font-variant-numeric:tabular-nums; }
+.uc-content .uc-affiliate-link-card [data-affiliate-feedback] { grid-column:1/-1; margin:0; min-height:18px; color:var(--success,#16a34a); font-size:12px; }
+.uc-content .uc-affiliate-link-card [data-affiliate-feedback].is-error { color:var(--danger,#dc2626); }
+.uc-content .uc-affiliate-accounts { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; padding-top:4px; }
+.uc-content .uc-affiliate-accounts label { display:grid; gap:5px; color:var(--muted,#6b7280); font-size:12px; }
+.uc-content .uc-affiliate-accounts input { height:38px; padding:0 10px; border:1px solid var(--border,#e5e7eb); border-radius:8px; background:var(--surface,#fff); color:var(--text,#111827); }
+.uc-content .uc-affiliate-accounts button { justify-self:start; }
+.uc-content .uc-affiliate-accounts p { grid-column:1/-1; margin:0; color:var(--success,#16a34a); font-size:12px; }
+@media (max-width:640px) { .uc-content .aksrt-affiliate-panel { padding:16px; } .uc-content .uc-affiliate-link-card { align-items:stretch; flex-direction:column; } .uc-content .uc-affiliate-link-card button { width:100%; } .uc-content .uc-affiliate-stats,.uc-content .uc-affiliate-accounts { grid-template-columns:1fr; } }
diff --git a/assets/js/credits.js b/assets/js/credits.js
index 866aea27..1b848a40 100644
--- a/assets/js/credits.js
+++ b/assets/js/credits.js
@@ -383,6 +383,39 @@
});
}
+ // ===== Forward-only affiliate promotion center =====
+ function initAffiliatePromotion() {
+ var root = document.querySelector('[data-aksrt-promotion-page]');
+ if (!root || root.dataset.affiliateBound) return;
+ root.dataset.affiliateBound = '1';
+ var input = root.querySelector('[data-affiliate-link]');
+ var copy = root.querySelector('[data-affiliate-copy]');
+ var feedback = root.querySelector('[data-affiliate-feedback]');
+ function setFeedback(text, error) { if (feedback) { feedback.textContent = text; feedback.classList.toggle('is-error', !!error); } }
+ api('/promotion/me').then(function (r) {
+ if (!r.ok) { setFeedback((r.json && r.json.message) || '推广数据加载失败。', true); return; }
+ var j = r.json || {}, stats = j.stats || {};
+ 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.ratio || 0) + '%';
+ var promoters = root.querySelector('[data-affiliate-promoters]'); if (promoters) promoters.textContent = String(stats.promoters || 0);
+ var payers = root.querySelector('[data-affiliate-payers]'); if (payers) payers.textContent = String(stats.effective_payers || 0);
+ }).catch(function () { setFeedback('推广数据加载失败。', true); });
+ if (copy) copy.addEventListener('click', function () {
+ if (!input || !input.value) return;
+ var done = function () { setFeedback('推广链接已复制。'); };
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(function () { input.focus(); input.select(); setFeedback('复制失败,请手动复制。', true); });
+ else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { setFeedback('复制失败,请手动复制。', true); } }
+ });
+ var accountForm = root.querySelector('[data-affiliate-accounts]');
+ if (accountForm) accountForm.addEventListener('submit', function (event) {
+ event.preventDefault();
+ var feedback = accountForm.querySelector('[data-affiliate-account-feedback]');
+ var accounts = {}; new FormData(accountForm).forEach(function (value, key) { if (String(value).trim()) accounts[key] = String(value).trim(); });
+ api('/promotion/payout-accounts', { method: 'PUT', body: JSON.stringify({ accounts: accounts }) }).then(function (r) { if (feedback) feedback.textContent = r.ok ? '收款方式已保存。' : ((r.json && r.json.message) || '保存失败。'); }).catch(function () { if (feedback) feedback.textContent = '网络错误,请稍后重试。'; });
+ });
+ }
+
// ===== VIP 积分兑换 =====
@@ -917,9 +950,9 @@
}
busy = true;
setTip('正在提交…');
- api('/pay/balance/withdraw', {
+ api('/earnings/withdrawals', {
method: 'POST',
- body: JSON.stringify({ amount: amount, method: method, account_info: account })
+ body: JSON.stringify({ amount: amount, method: method, account: account })
}).then(function (r) {
busy = false;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
@@ -996,7 +1029,8 @@
function bootAssets() {
initSignin();
initSigninGoto();
- initInvite();
+ initInvite();
+ initAffiliatePromotion();
initVipShop();
initVipPointsExchange();
initCardPass();
diff --git a/assets/js/revenue.js b/assets/js/revenue.js
index 1795d62b..f6eb9170 100644
--- a/assets/js/revenue.js
+++ b/assets/js/revenue.js
@@ -31,7 +31,7 @@
}
function request(payload) {
- return fetch(rest + '/pay/balance/withdraw', {
+ return fetch(rest + '/earnings/withdrawals', {
method: 'POST',
credentials: 'same-origin',
headers: {
@@ -53,7 +53,7 @@
var values = new FormData(form);
var amount = Number(values.get('amount') || 0);
var method = String(values.get('method') || 'alipay');
- var account = String(values.get('account_info') || '').trim();
+ var account = String(values.get('account') || values.get('account_info') || '').trim();
var minimum = Number(page.getAttribute('data-min-amount') || 50);
if (amount < minimum) {
setMessage('最低提现金额为 ' + minimum, true);
@@ -69,7 +69,7 @@
button.disabled = true;
button.textContent = '提交中…';
}
- request({ amount: amount, method: method, account_info: account }).then(function (result) {
+ request({ amount: amount, method: method, account: account }).then(function (result) {
if (button) {
button.disabled = false;
button.textContent = '提交提现申请';
diff --git a/functions.php b/functions.php
index 53de23c1..f66d67bb 100644
--- a/functions.php
+++ b/functions.php
@@ -202,6 +202,12 @@ if (class_exists('\AKSRT\Credits\Invite')) {
if (class_exists('AKSRT\\Payment\\CreatorRevenue')) {
\AKSRT\Payment\CreatorRevenue::get_instance()->init();
}
+if (class_exists('AKSRT\\Payment\\ReferralAttribution')) {
+ \AKSRT\Payment\ReferralAttribution::get_instance()->init();
+}
+if (class_exists('AKSRT\\Payment\\CommissionLedger')) {
+ \AKSRT\Payment\CommissionLedger::get_instance()->init();
+}
if (class_exists('\AKSRT\Content\Download')) {
\AKSRT\Content\Download::get_instance()->init();
}
@@ -409,9 +415,6 @@ if (is_admin() && class_exists('\AKSRT\Admin\Native\PayWithdrawals')) {
if (is_admin() && class_exists('\AKSRT\Admin\Native\PayIncome')) {
\AKSRT\Admin\Native\PayIncome::get_instance()->init();
}
-if (is_admin() && class_exists('\AKSRT\Admin\Native\PayRebate')) {
- \AKSRT\Admin\Native\PayRebate::get_instance()->init();
-}
// WordPress 原生用户菜单:消息/邀请码/认证/禁封(zibll users.php 子菜单对齐)
if (is_admin() && class_exists('\AKSRT\Admin\Native\UsersMenus')) {
diff --git a/inc/helpers/aksrt-user-center.php b/inc/helpers/aksrt-user-center.php
index 2bb65e36..d4379a6f 100644
--- a/inc/helpers/aksrt-user-center.php
+++ b/inc/helpers/aksrt-user-center.php
@@ -133,8 +133,8 @@ if (!function_exists('aksrt_get_user_center_tab_url')) {
*/
function aksrt_get_user_center_tab_url(string $tab): string {
$tab = sanitize_key($tab);
- if ('invite' === $tab || 'promotion' === $tab) {
- $tab = 'revenue';
+ if ('invite' === $tab) {
+ $tab = 'promotion';
}
return aksrt_get_user_center_url($tab);
}
@@ -180,6 +180,7 @@ if (!function_exists('aksrt_capture_user_center_view')) {
'vip' => 'vip',
'orders' => 'orders',
'auth' => 'auth',
+ 'promotion' => 'promotion',
'revenue' => 'revenue',
'settings' => 'settings',
'header' => 'header',
@@ -205,7 +206,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) {
* Capture a built-in or registered user-center panel body.
*/
function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string {
- $built_in_tabs = ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'settings'];
+ $built_in_tabs = ['profile', 'level', 'creator', 'posts', 'follow', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'promotion', 'revenue', 'settings'];
if (in_array($tab_key, $built_in_tabs, true)) {
return aksrt_capture_user_center_view($tab_key, $context);
}
@@ -314,8 +315,8 @@ if (!function_exists('aksrt_user_center_resolve_tabs')) {
}
if ('signin' === $active || 'assets' === $active) {
$active = 'credits';
- } elseif ('invite' === $active || 'promotion' === $active) {
- $active = 'revenue';
+ } elseif ('invite' === $active) {
+ $active = 'promotion';
} elseif ('vip' === $active) {
// Prefer dedicated vip tab when registered; else credits (VIP shop).
$active = in_array('vip', $keys, true) ? 'vip' : 'credits';
@@ -354,8 +355,7 @@ if (!function_exists('aksrt_user_center_runtime_config')) {
'activeTab' => $active_tab,
'tabAliases' => [
'signin' => 'credits',
- 'invite' => 'revenue',
- 'promotion' => 'revenue',
+ 'invite' => 'promotion',
'assets' => 'credits',
// vip → vip when panel exists; SPA may still map missing keys via server resolve.
'vip' => 'vip',
diff --git a/inc/helpers/user-center-renderers/profile-promotion.php b/inc/helpers/user-center-renderers/profile-promotion.php
index 1dcbf053..14342f66 100644
--- a/inc/helpers/user-center-renderers/profile-promotion.php
+++ b/inc/helpers/user-center-renderers/profile-promotion.php
@@ -2,9 +2,7 @@
/**
* User center promotion panel (invite / 推广中心).
*
- * Standalone dashboard tab that hosts the invite card. Reuses the existing
- * invite REST contract and credits.js `initInvite()` binding via the
- * `[data-aksrt-invite]` card in profile-invite.php.
+ * Standalone dashboard tab for the forward-only promotion contract.
*
* @package AKSRT
*/
@@ -18,8 +16,12 @@ if ($user_id <= 0) {
return;
}
-$currency_name = (string) ($context['currency_name'] ?? __('积分', 'aksrt'));
+$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_affiliate_settings() : [];
?>
-
-
-
+
diff --git a/inc/helpers/user-center-renderers/profile-revenue.php b/inc/helpers/user-center-renderers/profile-revenue.php
index 2c502e01..96057d5d 100644
--- a/inc/helpers/user-center-renderers/profile-revenue.php
+++ b/inc/helpers/user-center-renderers/profile-revenue.php
@@ -20,42 +20,33 @@ $affiliate = class_exists('\AKSRT\Settings\Settings')
$withdrawal = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_withdrawal_settings()
: [];
-$all_settings = class_exists('\AKSRT\Settings\Settings')
- ? \AKSRT\Settings\Settings::get_all()
- : [];
-$withdraw_on = !empty($all_settings['pay_balance_withdraw_s'])
+$withdraw_on = !empty($affiliate['enabled'])
&& class_exists('\AKSRT\Credits\Balance')
&& \AKSRT\Credits\Balance::enabled();
if ($withdraw_on && class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('balance_withdraw', $user_id)) {
$withdraw_on = false;
}
-$withdraw_min = max(1, (float) ($all_settings['pay_balance_withdraw_lowest_money'] ?? $all_settings['pay_rebate_withdraw_lowest_money'] ?? $withdrawal['min_amount'] ?? 50));
+$withdraw_min = max(1, (float) ($affiliate['min_withdrawal'] ?? $withdrawal['min_amount'] ?? 50));
$wallet_fmt = class_exists('\AKSRT\Credits\Balance')
? \AKSRT\Credits\Balance::format((float) \AKSRT\Credits\Balance::balance($user_id))
: '0.00';
-$commission_stats = class_exists('\AKSRT\Payment\Rebate')
- ? \AKSRT\Payment\Rebate::get_instance()->get_stats($user_id)
- : ['total' => 0, 'pending' => 0, 'pending_amount' => 0, 'settled' => 0, 'total_amount' => 0, 'settled_amount' => 0];
+$commission_stats = class_exists('\AKSRT\Payment\CommissionLedger')
+ ? \AKSRT\Payment\CommissionLedger::get_instance()->overview($user_id)
+ : ['total' => 0, 'pending' => 0, 'settled' => 0, 'wallet' => 0];
$available_commission = class_exists('\AKSRT\Credits\Balance')
? (float) \AKSRT\Credits\Balance::balance($user_id)
- : (float) ($commission_stats['settled_amount'] ?? 0);
-$vip_ratios = is_array($affiliate['user_ratio'] ?? null) ? $affiliate['user_ratio'] : [];
-$commission_ratio = (int) ($vip_ratios['user'] ?? $affiliate['ratio'] ?? 0);
-$current_vip = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
-if ($current_vip >= 2 && !empty($vip_ratios['vip2'])) {
- $commission_ratio = (int) $vip_ratios['vip2'];
-} elseif ($current_vip >= 1 && !empty($vip_ratios['vip1'])) {
- $commission_ratio = (int) $vip_ratios['vip1'];
-}
-$details_text = trim((string) ($affiliate['text']['details'] ?? ''));
+ : (float) ($commission_stats['settled'] ?? 0);
+$vip_ratios = [];
+$commission_ratio = (float) ($affiliate['ratio'] ?? 0);
+$details_text = trim((string) ($affiliate['earnings_text'] ?? ''));
if ($details_text === '') {
- $details_text = trim((string) ($affiliate['note'] ?? '')) ?: __('好友通过推广链接完成符合条件的订单后,系统会按当前佣金比例记录返佣,结算后进入可提现余额。', 'aksrt');
+ $details_text = __('好友通过推广链接完成符合条件的订单后,系统会按当前佣金比例记录返佣,结算后进入可提现余额。', 'aksrt');
}
$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')));
-if (!in_array($active_reward_tab, ['detail', 'orders', 'users'], true)) {
+if (!in_array($active_reward_tab, ['detail', 'orders'], true)) {
$active_reward_tab = 'detail';
}
$base_url = function_exists('aksrt_get_user_center_tab_url')
@@ -69,22 +60,20 @@ $format_amount = static function ($amount): string {
};
$commission_page = max(1, (int) ($revenue_request['commission_page'] ?? ($_GET['commission_page'] ?? 1)));
-$commission_view = class_exists('\AKSRT\Payment\Rebate')
- ? \AKSRT\Payment\Rebate::get_instance()->get_list([
- 'user_id' => $user_id,
- 'limit' => 10,
- 'offset' => ($commission_page - 1) * 10,
+$commission_view = class_exists('\AKSRT\Payment\CommissionLedger')
+ ? \AKSRT\Payment\CommissionLedger::get_instance()->list_entries($user_id, [
+ 'per_page' => 10,
+ 'page' => $commission_page,
])
: ['items' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1];
$commission_items = (array) ($commission_view['items'] ?? []);
$commission_total_pages = max(1, (int) ($commission_view['max_pages'] ?? ceil(max(1, (int) ($commission_view['total'] ?? 0)) / 10)));
if ($commission_page > $commission_total_pages) {
$commission_page = $commission_total_pages;
- if (class_exists('\AKSRT\Payment\Rebate')) {
- $commission_view = \AKSRT\Payment\Rebate::get_instance()->get_list([
- 'user_id' => $user_id,
- 'limit' => 10,
- 'offset' => ($commission_page - 1) * 10,
+ if (class_exists('\AKSRT\Payment\CommissionLedger')) {
+ $commission_view = \AKSRT\Payment\CommissionLedger::get_instance()->list_entries($user_id, [
+ 'per_page' => 10,
+ 'page' => $commission_page,
]);
}
}
@@ -110,7 +99,7 @@ $status_labels = [
-
+
@@ -120,35 +109,15 @@ $status_labels = [
-
-
+
+
-
- class="" href="">
+
class="" href="">
- class="" href="">
-
-
-
-
-
%
-
VIP 1 %
-
VIP 2 %
-
-
@@ -164,17 +133,14 @@ $status_labels = [
$source_user = !empty($item['source_user_id']) ? get_userdata((int) $item['source_user_id']) : null;
$source_name = $source_user instanceof WP_User ? $source_user->display_name : __('推广用户', 'aksrt');
?>
-
+
+
1) : ?>
-
+
diff --git a/inc/helpers/user-center-renderers/withdraw-modal.php b/inc/helpers/user-center-renderers/withdraw-modal.php
index 9247135d..d1418c9e 100644
--- a/inc/helpers/user-center-renderers/withdraw-modal.php
+++ b/inc/helpers/user-center-renderers/withdraw-modal.php
@@ -23,7 +23,6 @@ $withdraw_fee_rate = (int) ($withdraw_settings['service_charge_rate'] ?? 5);
-
diff --git a/inc/modules/widget-common.php b/inc/modules/widget-common.php
new file mode 100644
index 00000000..867c2115
--- /dev/null
+++ b/inc/modules/widget-common.php
@@ -0,0 +1,29 @@
+'','title_link'=>'','display_pc'=>1,'display_mobile'=>1,'sticky'=>0,'animation'=>'none','background'=>'surface','dark_background'=>'surface'];
+}
+function aksrt_widget_common_normalize(array $instance): array {
+ if (class_exists('\\AKSRT\\Widgets\\CommonWidgetFields')) return \AKSRT\Widgets\CommonWidgetFields::normalize($instance);
+ $d = aksrt_widget_common_defaults();
+ $out = $instance;
+ $out['subtitle'] = sanitize_text_field((string)($instance['subtitle'] ?? $d['subtitle']));
+ $out['title_link'] = esc_url_raw((string)($instance['title_link'] ?? ''));
+ foreach (['display_pc','display_mobile','sticky'] as $key) $out[$key] = !empty($instance[$key]) ? 1 : 0;
+ $out['animation'] = in_array(($a = sanitize_key((string)($instance['animation'] ?? 'none'))), ['none','fade','slide'], true) ? $a : 'none';
+ $out['background'] = in_array(($b = sanitize_key((string)($instance['background'] ?? 'surface'))), ['surface','transparent','primary'], true) ? $b : 'surface';
+ $out['dark_background'] = in_array(($db = sanitize_key((string)($instance['dark_background'] ?? 'surface'))), ['surface','transparent','primary'], true) ? $db : 'surface';
+ return $out;
+}
+function aksrt_widget_common_form(WP_Widget $widget, array $instance): void {
+ $v = aksrt_widget_common_normalize($instance);
+ $field = static function(string $key) use ($widget): string { return esc_attr($widget->get_field_name($key)); };
+ echo '' . esc_html__('副标题', 'aksrt') . '
';
+ echo '' . esc_html__('标题链接', 'aksrt') . '
';
+ echo ' ' . esc_html__('桌面端显示', 'aksrt') . ' ' . esc_html__('移动端显示', 'aksrt') . ' ' . esc_html__('侧栏吸顶', 'aksrt') . '
';
+ echo '' . esc_html__('入场动画', 'aksrt') . 'None Fade Slide
';
+}
+add_filter('widget_update_callback', static function($instance, $new_instance, $old_instance, $widget) { return is_array($instance) ? aksrt_widget_common_normalize($instance) : $instance; }, 10, 4);
+add_action('in_widget_form', static function(WP_Widget $widget, &$return, array $instance): void { aksrt_widget_common_form($widget, $instance); }, 20, 3);
diff --git a/inc/modules/widgets-register.php b/inc/modules/widgets-register.php
index 3c454227..5e88eb42 100644
--- a/inc/modules/widgets-register.php
+++ b/inc/modules/widgets-register.php
@@ -8,6 +8,7 @@
defined('ABSPATH') || exit;
require_once AKSRT_DIR . '/inc/modules/widget-areas.php';
+require_once AKSRT_DIR . '/inc/modules/widget-common.php';
/**
* Register custom widgets.
@@ -68,6 +69,12 @@ function aksrt_register_widgets() {
register_widget('\AKSRT\Widgets\WidgetMainPosts');
register_widget('\AKSRT\Widgets\WidgetTermAggregation');
register_widget('\AKSRT\Widgets\WidgetIconCoverCards');
+ register_widget('\AKSRT\Widgets\PricingPlansWidget');
+ register_widget('\AKSRT\Widgets\TestimonialsWidget');
+ register_widget('\AKSRT\Widgets\FaqWidget');
+ register_widget('\AKSRT\Widgets\StatsWidget');
+ register_widget('\AKSRT\Widgets\HighlightsWidget');
+ register_widget('\AKSRT\Widgets\PartnersWidget');
}
add_action('widgets_init', 'aksrt_register_widgets');
@@ -102,6 +109,9 @@ add_filter('block_type_metadata', function ($metadata) {
$aksrt_widgets = [
'aksrt_categories',
'aksrt_tag_cloud',
+ 'aksrt_hot_posts','aksrt_tab_posts','aksrt_category_block','aksrt_category_tabs','aksrt_category_recommend',
+ 'aksrt_search','aksrt_notice','aksrt_recent_comments','aksrt_icon_cards','aksrt_buttons','aksrt_announcement','aksrt_user_card','aksrt_cover_cards','aksrt_video_player','aksrt_yiyan','aksrt_slider','aksrt_iframe','aksrt_combo_info','aksrt_text_title','aksrt_term_cards','aksrt_post_pay_box','aksrt_author_card','aksrt_oneline_posts','aksrt_main_posts','aksrt_term_aggregation','aksrt_icon_cover_cards',
+ 'aksrt_pricing_plans','aksrt_testimonials','aksrt_faq','aksrt_stats','aksrt_highlights','aksrt_partners',
];
if (!empty($metadata['name']) && in_array($metadata['name'], $aksrt_widgets, true)) {
@@ -123,6 +133,8 @@ add_action('in_widget_form', function (WP_Widget $widget): void {
'aksrt_category_block' => 'hybrid',
'aksrt_category_tabs' => 'hybrid',
'aksrt_category_recommend' => 'homepage',
+ 'aksrt_search'=>'sidebar','aksrt_notice'=>'sidebar','aksrt_recent_comments'=>'sidebar','aksrt_icon_cards'=>'hybrid','aksrt_buttons'=>'hybrid','aksrt_announcement'=>'homepage','aksrt_user_card'=>'sidebar','aksrt_cover_cards'=>'homepage','aksrt_video_player'=>'content','aksrt_yiyan'=>'sidebar','aksrt_slider'=>'homepage','aksrt_iframe'=>'content','aksrt_combo_info'=>'homepage','aksrt_text_title'=>'hybrid','aksrt_term_cards'=>'homepage','aksrt_post_pay_box'=>'content','aksrt_author_card'=>'content','aksrt_oneline_posts'=>'sidebar','aksrt_main_posts'=>'homepage','aksrt_term_aggregation'=>'homepage','aksrt_icon_cover_cards'=>'homepage',
+ 'aksrt_pricing_plans'=>'homepage','aksrt_testimonials'=>'homepage','aksrt_faq'=>'content','aksrt_stats'=>'homepage','aksrt_highlights'=>'homepage','aksrt_partners'=>'homepage',
];
$scope = $map[$widget->id_base] ?? '';
diff --git a/src/Admin/PaymentController.php b/src/Admin/PaymentController.php
index 1252a2a8..f8a2e548 100644
--- a/src/Admin/PaymentController.php
+++ b/src/Admin/PaymentController.php
@@ -97,33 +97,6 @@ class PaymentController extends ControllerBase {
'permission_callback' => [$this, 'check_permission'],
]);
- // ── Rebate ──
-
- // Rebate list
- register_rest_route(self::API_NAMESPACE, '/payment/rebate/log', [
- 'methods' => 'GET',
- 'callback' => [$this, 'get_rebate_list'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
- register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)', [
- 'methods' => 'GET',
- 'callback' => [$this, 'get_rebate_detail'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
-
- // Rebate settle
- register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/settle', [
- 'methods' => 'POST',
- 'callback' => [$this, 'settle_rebate'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
-
- // Rebate cancel
- register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/cancel', [
- 'methods' => 'POST',
- 'callback' => [$this, 'cancel_rebate'],
- 'permission_callback' => [$this, 'check_permission'],
- ]);
}
public function rest_payment_overview(\WP_REST_Request $request): \WP_REST_Response {
diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php
index 541fb537..ce5257e2 100644
--- a/src/Admin/Schema/SchemaPayment.php
+++ b/src/Admin/Schema/SchemaPayment.php
@@ -142,28 +142,19 @@ class SchemaPayment {
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'policy', 'title' => '返佣策略', 'fields' => [
- ['key' => 'pay_rebate_s', 'label' => '启用推广返佣', 'type' => 'switch', 'default' => false],
- ['key' => 'pay_rebate_ratio', 'label' => '返佣比例(%)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 100],
- ['key' => 'payment_affiliate_window_days', 'label' => '归因窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
- ['key' => 'payment_affiliate_require_paid_order', 'label' => '仅对已支付订单返佣', 'type' => 'switch', 'default' => true],
- ['key' => 'pay_rebate_self_s', 'label' => '自购返佣', 'type' => 'switch', 'default' => true],
- ['key' => 'pay_rebate_show_users', 'label' => '显示推广用户', 'type' => 'switch', 'default' => true],
- ]],
- ['key' => 'user-ratio', 'title' => '用户返佣比例', 'fields' => [
- ['key' => 'payment_affiliate_user_ratio', 'label' => '普通用户比例(%)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 100],
- ['key' => 'pay_rebate_ratio_vip_1', 'label' => 'VIP1比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
- ['key' => 'pay_rebate_ratio_vip_2', 'label' => 'VIP2比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
- ]],
- ['key' => 'poster', 'title' => '推广海报', 'fields' => [
- ['key' => 'pay_rebate_poster_s', 'label' => '启用推广海报', 'type' => 'switch', 'default' => true],
- ['key' => 'pay_rebate_poster_img', 'label' => '海报背景图', 'type' => 'media', 'default' => ''],
- ['key' => 'pay_rebate_poster_title', 'label' => '海报标题', 'type' => 'input', 'default' => ''],
- ['key' => 'pay_rebate_poster_desc', 'label' => '海报描述', 'type' => 'input', 'default' => ''],
+ ['key' => 'affiliate_enabled', 'label' => '启用推广返佣', 'type' => 'switch', 'default' => false],
+ ['key' => 'affiliate_ratio', 'label' => '全站佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
+ ['key' => 'affiliate_order_types', 'label' => '适用订单类型', 'type' => 'checkbox', 'options' => [['value' => 'membership', 'label' => '会员订单'], ['value' => 'content', 'label' => '付费内容']], 'default' => ['membership', 'content']],
+ ['key' => 'affiliate_touch_window_days', 'label' => '首次触达窗口(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
+ ['key' => 'affiliate_settlement_days', 'label' => '结算观察期(天)', 'type' => 'number', 'default' => 7, 'min' => 0, 'max' => 90],
+ ['key' => 'affiliate_allow_self_purchase', 'label' => '允许自购返佣', 'type' => 'switch', 'default' => false],
+ ['key' => 'affiliate_show_stats', 'label' => '展示推广统计', 'type' => 'switch', 'default' => true],
]],
['key' => 'text', 'title' => '文案设置', 'fields' => [
- ['key' => 'pay_rebate_text_desc', 'label' => '返佣说明', 'type' => 'textarea', 'default' => ''],
- ['key' => 'pay_rebate_text_details', 'label' => '明细说明', 'type' => 'textarea', 'default' => ''],
- ['key' => 'pay_rebate_text_discount', 'label' => '折扣文案', 'type' => 'input', 'default' => ''],
+ ['key' => 'affiliate_promotion_text', 'label' => '推广说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'affiliate_earnings_text', 'label' => '收益说明', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'affiliate_withdrawal_agreement', 'label' => '提现协议', 'type' => 'textarea', 'default' => ''],
+ ['key' => 'affiliate_payout_channels', 'label' => '可用收款渠道', 'type' => 'checkbox', 'options' => [['value' => 'alipay', 'label' => '支付宝收款码'], ['value' => 'wechat', 'label' => '微信收款码']], 'default' => ['alipay', 'wechat']],
]],
],
],
@@ -326,12 +317,11 @@ class SchemaPayment {
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '提现设置', 'fields' => [
- ['key' => 'pay_balance_withdraw_s', 'label' => '启用余额提现', 'type' => 'switch', 'default' => false],
- ['key' => 'pay_rebate_withdraw_lowest_money', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
- ['key' => 'withdraw_service_charge', 'label' => '提现手续费(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 50, 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
- ['key' => 'withdraw_daily_count', 'label' => '每日可提现次数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 99999, 'sanitize_type' => 'int', 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
- ['key' => 'withdraw_daily_limit', 'label' => '每日提现上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 999999, 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
- ['key' => 'withdraw_reset_time', 'label' => '重置时间', 'type' => 'input', 'default' => '00:00', 'help' => '按站点 WordPress 时区重置,格式 HH:MM。', 'dependsOn' => ['key' => 'pay_balance_withdraw_s', 'value' => true]],
+ ['key' => 'affiliate_min_withdrawal', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 99999],
+ ['key' => 'affiliate_withdrawal_fee', 'label' => '提现手续费(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
+ ['key' => 'affiliate_daily_withdrawal_count', 'label' => '每日可提现次数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 99999],
+ ['key' => 'affiliate_daily_withdrawal_limit', 'label' => '每日提现上限', 'type' => 'number', 'default' => 500, 'min' => 0, 'max' => 999999],
+ ['key' => 'affiliate_withdrawal_reset_time', 'label' => '重置时间', 'type' => 'input', 'default' => '00:00', 'help' => '按站点 WordPress 时区重置,格式 HH:MM。'],
]],
],
],
diff --git a/src/Admin/SchemaProvider.php b/src/Admin/SchemaProvider.php
index 75105dcf..e4307b54 100644
--- a/src/Admin/SchemaProvider.php
+++ b/src/Admin/SchemaProvider.php
@@ -210,10 +210,10 @@ class SchemaProvider {
if ($domain === 'payment' && $section === 'withdrawal') {
$stored = get_option('aksrt_settings', []);
$merged = is_array($stored) ? array_merge($stored, $data) : $data;
- $count = $merged['withdraw_daily_count'] ?? 3;
- $min = (float) ($merged['pay_rebate_withdraw_lowest_money'] ?? 50);
- $limit = (float) ($merged['withdraw_daily_limit'] ?? 50);
- $time = (string) ($merged['withdraw_reset_time'] ?? '00:00');
+ $count = $merged['affiliate_daily_withdrawal_count'] ?? 3;
+ $min = (float) ($merged['affiliate_min_withdrawal'] ?? 50);
+ $limit = (float) ($merged['affiliate_daily_withdrawal_limit'] ?? 500);
+ $time = (string) ($merged['affiliate_withdrawal_reset_time'] ?? '00:00');
if (filter_var($count, FILTER_VALIDATE_INT) === false || (int) $count < 0) {
$errors[] = __('每日可提现次数必须是非负整数', 'aksrt');
}
@@ -336,10 +336,6 @@ class SchemaProvider {
$stored[$key] = $value;
}
- if (($schema['key'] ?? '') === 'withdrawal') {
- unset($stored['pay_rebate_withdraw_text_details'], $stored['pay_balance_withdraw_lowest_money']);
- }
-
// Captcha stores a single positive scene list.
if ($option_key === 'aksrt_captcha_config' && function_exists('aksrt_captcha_scenes')) {
$stored['enabled_scenarios'] = function_exists('aksrt_captcha_sanitize_scene_list')
diff --git a/src/Content/PayOrders.php b/src/Content/PayOrders.php
index b36b4ea1..06ed8f2d 100644
--- a/src/Content/PayOrders.php
+++ b/src/Content/PayOrders.php
@@ -92,32 +92,8 @@ trait PayOrders {
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
}
- // Affiliate commission (does not reduce buyer pay).
- $referrer_id = (int) ($meta['referrer_id'] ?? 0);
- $rebate_price = round((float) ($meta['rebate_price'] ?? 0), 2);
- $pay_amount = round((float) ($order['amount'] ?? $meta['payable'] ?? 0), 2);
- if ($referrer_id <= 0 || $rebate_price <= 0 || $pay_amount <= 0) {
- return;
- }
- if (!class_exists('\AKSRT\Payment\Rebate')) {
- return;
- }
- if ($this->rebate_log_exists($order_number, $referrer_id)) {
- return;
- }
- \AKSRT\Payment\Rebate::get_instance()->create([
- 'user_id' => $referrer_id,
- 'order_number' => $order_number,
- 'source_user_id' => $buyer_id,
- 'amount' => $rebate_price,
- 'type' => \AKSRT\Payment\Rebate::TYPE_ORDER,
- 'note' => sprintf(
- /* translators: 1: order number 2: post id */
- __('内容推广返佣 订单 %1$s 内容 #%2$d', 'aksrt'),
- $order_number,
- (int) ($order['product_id'] ?? $meta['post_id'] ?? 0)
- ),
- ]);
+ // Affiliate commission is recorded exactly once by CommissionLedger on
+ // the canonical aksrt_order_paid event. No order-side legacy writes.
}
@@ -140,22 +116,6 @@ trait PayOrders {
- private function rebate_log_exists(string $order_number, int $referrer_id): bool {
- global $wpdb;
- $table = \AKSRT\Core\DBMigrator::table('rebate_log');
- if ($table === '' || $order_number === '') {
- return false;
- }
- $found = $wpdb->get_var($wpdb->prepare(
- "SELECT id FROM {$table} WHERE order_number = %s AND user_id = %d LIMIT 1",
- $order_number,
- $referrer_id
- ));
- return !empty($found);
- }
-
-
-
/**
* Points one-shot purchase → permanent resource order.
*
diff --git a/src/Core/DBMigrator.php b/src/Core/DBMigrator.php
index 14ab076c..826a520b 100644
--- a/src/Core/DBMigrator.php
+++ b/src/Core/DBMigrator.php
@@ -14,7 +14,7 @@ class DBMigrator {
use DBMigratorVip;
/** Current schema version; changing it re-runs dbDelta. */
- const SCHEMA_VERSION = '1.22.0';
+ const SCHEMA_VERSION = '1.23.0';
const OPT_KEY = 'aksrt_db_schema_version';
diff --git a/src/Core/DBMigratorPart2.php b/src/Core/DBMigratorPart2.php
index 4fc41bda..4f356712 100644
--- a/src/Core/DBMigratorPart2.php
+++ b/src/Core/DBMigratorPart2.php
@@ -377,6 +377,84 @@ trait DBMigratorPart2 {
) {$charset_collate};";
dbDelta($sql_rebate_log);
+ // ── affiliate attribution (new contract; no legacy invite reads) ──
+ $sql_affiliate_relation = "CREATE TABLE {$prefix}aksrt_affiliate_relation (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ referral_code VARCHAR(32) NOT NULL,
+ promoter_id BIGINT UNSIGNED NOT NULL,
+ invitee_id BIGINT UNSIGNED DEFAULT NULL,
+ first_touch_at DATETIME NOT NULL,
+ bound_at DATETIME DEFAULT NULL,
+ status VARCHAR(20) NOT NULL DEFAULT 'captured',
+ created_at DATETIME NOT NULL,
+ PRIMARY KEY (id),
+ KEY idx_referral_code (referral_code),
+ UNIQUE KEY uniq_invitee (invitee_id),
+ KEY idx_promoter (promoter_id),
+ KEY idx_status_touch (status, first_touch_at)
+ ) {$charset_collate};";
+ dbDelta($sql_affiliate_relation);
+
+ // ── immutable commission ledger ──
+ $sql_commission_ledger = "CREATE TABLE {$prefix}aksrt_commission_ledger (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ promoter_id BIGINT UNSIGNED NOT NULL,
+ source_user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
+ order_number VARCHAR(64) NOT NULL,
+ order_type VARCHAR(32) NOT NULL,
+ eligible_base DECIMAL(18,2) NOT NULL DEFAULT 0,
+ ratio DECIMAL(8,4) NOT NULL DEFAULT 0,
+ original_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
+ adjustment_total DECIMAL(18,2) NOT NULL DEFAULT 0,
+ status VARCHAR(20) NOT NULL DEFAULT 'pending',
+ idempotency_key VARCHAR(128) NOT NULL,
+ available_at DATETIME NOT NULL,
+ settled_at DATETIME DEFAULT NULL,
+ created_at DATETIME NOT NULL,
+ note TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY uniq_commission_idempotency (idempotency_key),
+ KEY idx_promoter_status (promoter_id, status),
+ KEY idx_order_type (order_number, order_type),
+ KEY idx_available (status, available_at),
+ KEY idx_created (created_at)
+ ) {$charset_collate};";
+ dbDelta($sql_commission_ledger);
+
+ // ── immutable commission adjustments (refund/debt/manual) ──
+ $sql_commission_adjustment = "CREATE TABLE {$prefix}aksrt_commission_adjustment (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ ledger_id BIGINT UNSIGNED NOT NULL,
+ promoter_id BIGINT UNSIGNED NOT NULL,
+ amount DECIMAL(18,2) NOT NULL DEFAULT 0,
+ reason VARCHAR(32) NOT NULL,
+ source_key VARCHAR(128) NOT NULL,
+ operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
+ created_at DATETIME NOT NULL,
+ note TEXT DEFAULT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY uniq_adjustment_source (source_key),
+ KEY idx_ledger (ledger_id),
+ KEY idx_promoter (promoter_id)
+ ) {$charset_collate};";
+ dbDelta($sql_commission_adjustment);
+
+ // ── payout account snapshots ──
+ $sql_payout_account = "CREATE TABLE {$prefix}aksrt_payout_account (
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT UNSIGNED NOT NULL,
+ channel VARCHAR(20) NOT NULL,
+ attachment_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
+ account_hash CHAR(64) NOT NULL DEFAULT '',
+ status VARCHAR(20) NOT NULL DEFAULT 'active',
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME NOT NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY uniq_user_channel (user_id, channel),
+ KEY idx_user_status (user_id, status)
+ ) {$charset_collate};";
+ dbDelta($sql_payout_account);
+
// ── order_items (§6.5 多商品多商家) ──
$sql_order_items = "CREATE TABLE {$prefix}aksrt_order_item (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
diff --git a/src/Core/DBMigratorPart3.php b/src/Core/DBMigratorPart3.php
index 10ad66e8..6d015147 100644
--- a/src/Core/DBMigratorPart3.php
+++ b/src/Core/DBMigratorPart3.php
@@ -33,6 +33,10 @@ trait DBMigratorPart3 {
'coupon' => 'aksrt_coupon',
'charge_card' => 'aksrt_charge_card',
'rebate_log' => 'aksrt_rebate_log',
+ 'affiliate_relation' => 'aksrt_affiliate_relation',
+ 'commission_ledger' => 'aksrt_commission_ledger',
+ 'commission_adjustment' => 'aksrt_commission_adjustment',
+ 'payout_account' => 'aksrt_payout_account',
'coupon_usage' => 'aksrt_coupon_usage',
'user_restriction' => 'aksrt_user_restriction',
'identity_application' => 'aksrt_identity_application',
diff --git a/src/Core/WidgetAjax.php b/src/Core/WidgetAjax.php
index eaf61eea..23788f96 100644
--- a/src/Core/WidgetAjax.php
+++ b/src/Core/WidgetAjax.php
@@ -337,12 +337,13 @@ class WidgetAjax {
}
private function fail(): \WP_REST_Response {
- return new \WP_REST_Response(['success' => false, 'message' => 'unavailable'], 500);
+ return new \WP_REST_Response(['success' => false, 'code' => 'widget_unavailable', 'message' => __('小工具服务暂不可用。', 'aksrt'), 'data' => ['items' => [], 'page' => 1, 'max_pages' => 1, 'total' => 0]], 500);
}
private function payload(array $payload, int $page): \WP_REST_Response {
return new \WP_REST_Response([
'success' => true,
+ 'code' => 'ok',
'html' => (string) ($payload['html'] ?? ''),
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
diff --git a/src/Payment/CommissionLedger.php b/src/Payment/CommissionLedger.php
new file mode 100644
index 00000000..755bb111
--- /dev/null
+++ b/src/Payment/CommissionLedger.php
@@ -0,0 +1,125 @@
+record_paid_order(array_merge($order, ['order_number' => $order_number])); }
+ public function on_order_status_changed(string $order_number, string $status, array $order = []): void {
+ if (in_array($status, ['refunding', 'refunded'], true)) { $this->reverse($order_number, (float) ($order['amount'] ?? 0), 'refund'); }
+ }
+ public function record_paid_order(array $order): int {
+ if (!\AKSRT\Settings\Settings::affiliate_enabled()) return 0;
+ $type = sanitize_key((string) ($order['product_type'] ?? ''));
+ $map = ['vip' => 'membership', 'resource' => 'content'];
+ $order_type = $map[$type] ?? $type;
+ $settings = \AKSRT\Settings\Settings::get_affiliate_settings();
+ if (!in_array($order_type, (array) $settings['order_types'], true)) return 0;
+ $order_no = sanitize_text_field((string) ($order['order_number'] ?? ''));
+ $buyer = (int) ($order['user_id'] ?? 0);
+ $amount = round((float) ($order['amount'] ?? 0), 2);
+ if ($order_no === '' || $buyer <= 0 || $amount <= 0) return 0;
+ $promoter = ReferralAttribution::get_instance()->resolve($buyer);
+ if ($promoter <= 0 || $promoter === $buyer) return 0;
+ if (!$settings['allow_self_purchase'] && $promoter === $buyer) return 0;
+ $ratio = (float) $settings['ratio'];
+ $commission = round($amount * $ratio / 100, 2);
+ if ($commission <= 0) return 0;
+ global $wpdb;
+ $table = $this->table('commission_ledger');
+ $key = hash('sha256', 'affiliate:' . $order_no . ':' . $order_type);
+ $now = current_time('mysql', true);
+ $available = gmdate('Y-m-d H:i:s', time() + ((int) $settings['settlement_days'] * DAY_IN_SECONDS));
+ $ok = $wpdb->insert($table, ['promoter_id'=>$promoter,'source_user_id'=>$buyer,'order_number'=>$order_no,'order_type'=>$order_type,'eligible_base'=>$amount,'ratio'=>$ratio,'original_amount'=>$commission,'adjustment_total'=>0,'status'=>'pending','idempotency_key'=>$key,'available_at'=>$available,'created_at'=>$now], ['%d','%d','%s','%s','%f','%f','%f','%f','%s','%s','%s','%s']);
+ return $ok ? (int) $wpdb->insert_id : (int) $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE idempotency_key=%s", $key));
+ }
+ public function settle_due(int $limit = 100): int {
+ global $wpdb; $table = $this->table('commission_ledger');
+ $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE status='pending' AND available_at <= %s ORDER BY id ASC LIMIT %d", current_time('mysql', true), max(1,min(500,$limit))), ARRAY_A);
+ $count = 0;
+ foreach ((array) $rows as $row) if ($this->settle_one((int)$row['id'])) $count++;
+ return $count;
+ }
+ private function settle_one(int $id): bool {
+ global $wpdb; $table = $this->table('commission_ledger');
+ $wpdb->query('START TRANSACTION');
+ try {
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id=%d AND status='pending' FOR UPDATE", $id), ARRAY_A);
+ if (!$row) { $wpdb->query('ROLLBACK'); return false; }
+ $amount = round((float)$row['original_amount'] + (float)$row['adjustment_total'], 2);
+ if ($amount > 0 && class_exists('\\AKSRT\\Credits\\Balance')) {
+ $credited = \AKSRT\Credits\Balance::add((int)$row['promoter_id'], $amount, 'affiliate_commission', ['type'=>'commission','id'=>$id], sprintf(__('推广佣金结算(订单 %s)','aksrt'), $row['order_number']));
+ if (is_wp_error($credited)) { $wpdb->query('ROLLBACK'); return false; }
+ }
+ $updated = $wpdb->query($wpdb->prepare("UPDATE {$table} SET status='settled', settled_at=%s WHERE id=%d AND status='pending'", current_time('mysql', true), $id)) === 1;
+ if (!$updated) { $wpdb->query('ROLLBACK'); return false; }
+ $wpdb->query('COMMIT'); return true;
+ } catch (\Throwable $e) { $wpdb->query('ROLLBACK'); return false; }
+ }
+ public function reverse(string $order_number, float $refund_amount, string $reason = 'refund'): int {
+ global $wpdb; $table = $this->table('commission_ledger'); $adjust = $this->table('commission_adjustment');
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE order_number=%s LIMIT 1", $order_number), ARRAY_A);
+ if (!$row || $refund_amount <= 0) return 0;
+ $base = max(0.01, (float)$row['eligible_base']); $negative = -round((float)$row['original_amount'] * min(1, $refund_amount / $base), 2);
+ $source = hash('sha256', 'refund:' . $order_number . ':' . number_format($refund_amount,2,'.',''));
+ $ok = $wpdb->insert($adjust, ['ledger_id'=>$row['id'],'promoter_id'=>$row['promoter_id'],'amount'=>$negative,'reason'=>sanitize_key($reason),'source_key'=>$source,'operator_id'=>0,'created_at'=>current_time('mysql', true)], ['%d','%d','%f','%s','%s','%d','%s']);
+ if (!$ok) return 0;
+ $wpdb->query($wpdb->prepare("UPDATE {$table} SET adjustment_total=adjustment_total+%f, status=IF(status='pending','cancelled',status) WHERE id=%d", $negative, $row['id']));
+ if ($row['status'] === 'settled' && class_exists('\\AKSRT\\Credits\\Balance')) {
+ $debit = min((float)$row['original_amount'], abs($negative));
+ $result = \AKSRT\Credits\Balance::deduct((int)$row['promoter_id'], $debit, 'affiliate_refund', ['type'=>'commission','id'=>$row['id']], __('推广佣金退款冲正','aksrt'));
+ if (is_wp_error($result)) update_user_meta((int)$row['promoter_id'], self::DEBT_META, round((float)get_user_meta((int)$row['promoter_id'], self::DEBT_META, true) + $debit, 2));
+ }
+ return (int)$wpdb->insert_id;
+ }
+ public function overview(int $user_id): array {
+ 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) ?: [];
+ return ['total'=>round((float)($row['total']??0),2),'pending'=>round((float)($row['pending']??0),2),'settled'=>round((float)($row['settled']??0),2),'wallet'=>class_exists('\\AKSRT\\Credits\\Balance')?\AKSRT\Credits\Balance::balance($user_id):0.0,'debt'=>round((float)get_user_meta($user_id,self::DEBT_META,true),2)];
+ }
+ public function list_entries(int $user_id, array $args=[]): array {
+ global $wpdb; $table=$this->table('commission_ledger'); $limit=max(1,min(100,(int)($args['per_page']??20))); $page=max(1,(int)($args['page']??1)); $offset=($page-1)*$limit; $where='promoter_id=%d'; $params=[$user_id]; $status=sanitize_key((string)($args['status']??'')); if(in_array($status,['pending','settled','cancelled'],true)){ $where.=' AND status=%s'; $params[]=$status; } $total=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}",$params)); $rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d",array_merge($params,[$limit,$offset])),ARRAY_A); return ['items'=>$rows?:[],'total'=>$total,'page'=>$page,'per_page'=>$limit,'max_pages'=>max(1,(int)ceil($total/$limit))];
+ }
+ public function register_routes(): void {
+ register_rest_route('aksrt/v1','/earnings/overview',['methods'=>\WP_REST_Server::READABLE,'callback'=>fn()=>rest_ensure_response($this->overview(get_current_user_id())),'permission_callback'=>fn()=>is_user_logged_in()]);
+ register_rest_route('aksrt/v1','/earnings/commissions',['methods'=>\WP_REST_Server::READABLE,'callback'=>fn(\WP_REST_Request $r)=>rest_ensure_response($this->list_entries(get_current_user_id(),$r->get_params())),'permission_callback'=>fn()=>is_user_logged_in()]);
+ register_rest_route('aksrt/v1','/earnings/withdrawals',['methods'=>\WP_REST_Server::READABLE,'callback'=>fn(\WP_REST_Request $r)=>rest_ensure_response($this->withdrawals(get_current_user_id(),$r->get_params())),'permission_callback'=>fn()=>is_user_logged_in()]);
+ register_rest_route('aksrt/v1','/earnings/withdrawals',['methods'=>\WP_REST_Server::CREATABLE,'callback'=>[$this,'create_withdrawal'],'permission_callback'=>fn()=>is_user_logged_in()]);
+ register_rest_route('aksrt/v1','/payment/affiliate/ledger/(?P\d+)/settle',['methods'=>\WP_REST_Server::CREATABLE,'callback'=>fn(\WP_REST_Request $r)=>$this->admin_settle((int)$r['id']),'permission_callback'=>fn()=>current_user_can('manage_options')]);
+ register_rest_route('aksrt/v1','/payment/affiliate/ledger/(?P\d+)/cancel',['methods'=>\WP_REST_Server::CREATABLE,'callback'=>fn(\WP_REST_Request $r)=>$this->admin_cancel((int)$r['id']),'permission_callback'=>fn()=>current_user_can('manage_options')]);
+ register_rest_route('aksrt/v1','/payment/affiliate/overview',['methods'=>\WP_REST_Server::READABLE,'callback'=>fn()=>rest_ensure_response($this->admin_overview()),'permission_callback'=>fn()=>current_user_can('manage_options')]);
+ register_rest_route('aksrt/v1','/payment/affiliate/ledger',['methods'=>\WP_REST_Server::READABLE,'callback'=>fn(\WP_REST_Request $r)=>rest_ensure_response($this->admin_ledger($r->get_params())),'permission_callback'=>fn()=>current_user_can('manage_options')]);
+ register_rest_route('aksrt/v1','/payment/withdrawals/(?P\d+)/transition',['methods'=>\WP_REST_Server::CREATABLE,'callback'=>fn(\WP_REST_Request $r)=>$this->transition_withdrawal((int)$r['id'],(string)$r->get_param('status'),(string)$r->get_param('note')),'permission_callback'=>fn()=>current_user_can('manage_options')]);
+ }
+ public function admin_settle(int $id): \WP_REST_Response { $ok=$this->settle_one($id); return new \WP_REST_Response(['success'=>$ok],$ok?200:409); }
+ public function admin_cancel(int $id): \WP_REST_Response { global $wpdb; $ok=$wpdb->query($wpdb->prepare("UPDATE {$this->table('commission_ledger')} SET status='cancelled' WHERE id=%d AND status='pending'",$id))===1; return new \WP_REST_Response(['success'=>$ok],$ok?200:409); }
+ public function admin_overview(): array { global $wpdb; $table=$this->table('commission_ledger'); $row=$wpdb->get_row("SELECT COUNT(*) total, SUM(status='pending') pending, SUM(status='settled') settled, COALESCE(SUM(original_amount+adjustment_total),0) amount FROM {$table}",ARRAY_A) ?: []; return ['total'=>(int)($row['total']??0),'pending'=>(int)($row['pending']??0),'settled'=>(int)($row['settled']??0),'amount'=>(float)($row['amount']??0)]; }
+ public function admin_ledger(array $args): array { global $wpdb; $table=$this->table('commission_ledger'); $limit=max(1,min(100,(int)($args['per_page']??20))); $page=max(1,(int)($args['page']??1)); $offset=($page-1)*$limit; $status=sanitize_key((string)($args['status']??'')); $where='1=1'; $params=[]; if(in_array($status,['pending','settled','cancelled'],true)){ $where.=' AND status=%s'; $params[]=$status; } $total=(int)($wpdb->get_var($params?$wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}",$params):"SELECT COUNT(*) FROM {$table}")); $rows=$wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE {$where} ORDER BY id DESC LIMIT %d OFFSET %d",array_merge($params,[$limit,$offset])),ARRAY_A); return ['items'=>$rows?:[],'total'=>$total,'page'=>$page,'per_page'=>$limit,'max_pages'=>max(1,(int)ceil($total/$limit))]; }
+ public function withdrawals(int $user_id, array $args): array { return \AKSRT\Payment\Withdrawal::get_instance()->get_user_withdrawals($user_id, ['status'=>sanitize_key((string)($args['status']??'')),'limit'=>max(1,min(100,(int)($args['per_page']??20))),'offset'=>max(0,((int)($args['page']??1)-1)*max(1,(int)($args['per_page']??20)))]); }
+ public function create_withdrawal(\WP_REST_Request $request): \WP_REST_Response|\WP_Error {
+ $user_id=get_current_user_id(); $amount=(int)round((float)$request->get_param('amount')); $method=sanitize_key((string)$request->get_param('method')); $account=sanitize_text_field((string)$request->get_param('account')); $settings=\AKSRT\Settings\Settings::get_withdrawal_settings(); $affiliate=\AKSRT\Settings\Settings::get_affiliate_settings();
+ if (empty($affiliate['enabled'])) return new \WP_Error('affiliate_disabled',__('推广返佣未启用。','aksrt'),['status'=>403]);
+ if (\AKSRT\Payment\Withdrawal::get_instance()->has_open_withdrawal($user_id)) return new \WP_Error('affiliate_withdraw_pending',__('已有待处理提现申请。','aksrt'),['status'=>409]);
+ if ($amount < (int)ceil((float)$affiliate['min_withdrawal'])) return new \WP_Error('affiliate_withdraw_min',__('未达到最低提现金额。','aksrt'),['status'=>400]);
+ if ((float)get_user_meta($user_id,self::DEBT_META,true)>0) return new \WP_Error('affiliate_withdraw_debt',__('存在待偿还的佣金欠款,暂不可提现。','aksrt'),['status'=>409]);
+ if (!in_array($method,(array)$affiliate['payout_channels'],true) || $account==='') return new \WP_Error('affiliate_withdraw_account',__('请提供有效的收款渠道和收款码。','aksrt'),['status'=>400]);
+ $result=\AKSRT\Payment\Withdrawal::get_instance()->create_balance_request($user_id,$amount,$method,$account,$settings); if(is_wp_error($result)) return $result; return new \WP_REST_Response(['success'=>true,'data'=>$result],201);
+ }
+ public function admin_withdrawals(array $args): array { $service=\AKSRT\Payment\Withdrawal::get_instance(); return ['items'=>$service->get_all(['status'=>sanitize_key((string)($args['status']??'')),'limit'=>max(1,min(100,(int)($args['per_page']??20))),'offset'=>max(0,((int)($args['page']??1)-1)*max(1,(int)($args['per_page']??20)))]),'stats'=>$service->get_stats()]; }
+ public function transition_withdrawal(int $id,string $status,string $note): \WP_REST_Response|\WP_Error { $status = ['approved'=>'processing','paid'=>'completed'][$status] ?? $status; $result=\AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id,$status,$note); if(is_wp_error($result)) return $result; return new \WP_REST_Response(['success'=>true,'status'=>$status],200); }
+}
diff --git a/src/Payment/ReferralAttribution.php b/src/Payment/ReferralAttribution.php
new file mode 100644
index 00000000..dc5139f6
--- /dev/null
+++ b/src/Payment/ReferralAttribution.php
@@ -0,0 +1,203 @@
+resolve_promoter($raw) <= 0 || !empty($_COOKIE[self::COOKIE])) {
+ return;
+ }
+ if (!headers_sent()) {
+ $ttl_days = class_exists('\\AKSRT\\Settings\\Settings') ? (int) (\AKSRT\Settings\Settings::get_affiliate_settings()['window_days'] ?? 30) : 30;
+ setcookie(self::COOKIE, $raw, [
+ 'expires' => time() + DAY_IN_SECONDS * max(1, min(365, $ttl_days)),
+ 'path' => COOKIEPATH ?: '/',
+ 'domain' => COOKIE_DOMAIN,
+ 'secure' => is_ssl(),
+ 'httponly' => true,
+ 'samesite' => 'Lax',
+ ]);
+ $_COOKIE[self::COOKIE] = $raw;
+ }
+ }
+
+ public function create_link(int $user_id, string $url = ''): array {
+ $code = $this->get_or_create_code($user_id);
+ $target = $url !== '' ? $url : home_url('/');
+ return ['code' => $code, 'url' => add_query_arg('ref', $code, $target)];
+ }
+
+ public function get_or_create_code(int $user_id): string {
+ if ($user_id <= 0) {
+ return '';
+ }
+ $existing = sanitize_key((string) get_user_meta($user_id, self::META_CODE, true));
+ if ($existing !== '' && strlen($existing) >= 10) {
+ return $existing;
+ }
+ $alphabet = 'abcdefghjkmnpqrstuvwxyz23456789';
+ for ($attempt = 0; $attempt < 10; $attempt++) {
+ $code = '';
+ for ($i = 0; $i < 12; $i++) {
+ $code .= $alphabet[random_int(0, strlen($alphabet) - 1)];
+ }
+ if ($this->resolve_promoter($code) <= 0) {
+ update_user_meta($user_id, self::META_CODE, $code);
+ return $code;
+ }
+ }
+ return '';
+ }
+
+ public function bind_registered_user(int $invitee_id): void {
+ if ($invitee_id <= 0 || !class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::affiliate_enabled()) {
+ return;
+ }
+ global $wpdb;
+ $code = sanitize_key((string) ($_COOKIE[self::COOKIE] ?? ''));
+ $promoter_id = $this->resolve_promoter($code);
+ if ($code === '' || $promoter_id <= 0 || $promoter_id === $invitee_id) {
+ return;
+ }
+ $table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
+ if ($table === '') {
+ return;
+ }
+ $existing = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE invitee_id = %d LIMIT 1", $invitee_id));
+ if ($existing) {
+ return;
+ }
+ $now = current_time('mysql', true);
+ $wpdb->insert($table, [
+ 'referral_code' => $code,
+ 'promoter_id' => $promoter_id,
+ 'invitee_id' => $invitee_id,
+ 'first_touch_at'=> $now,
+ 'bound_at' => $now,
+ 'status' => 'bound',
+ 'created_at' => $now,
+ ], ['%s', '%d', '%d', '%s', '%s', '%s', '%s']);
+ if (!headers_sent()) {
+ setcookie(self::COOKIE, '', time() - HOUR_IN_SECONDS, COOKIEPATH ?: '/', COOKIE_DOMAIN);
+ }
+ }
+
+ public function resolve(int $user_id = 0): int {
+ $user_id = $user_id > 0 ? $user_id : get_current_user_id();
+ if ($user_id <= 0) {
+ return 0;
+ }
+ global $wpdb;
+ $table = \AKSRT\Core\DBMigrator::table('affiliate_relation');
+ $bound = $table !== '' ? (int) $wpdb->get_var($wpdb->prepare("SELECT promoter_id FROM {$table} WHERE invitee_id = %d AND status = 'bound' LIMIT 1", $user_id)) : 0;
+ if ($bound > 0) {
+ return $bound === $user_id ? 0 : $bound;
+ }
+ // Orders may only use a permanently bound relation. A browsing cookie
+ // never becomes an implicit post-login override.
+ return 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];
+ }
+
+ public function register_routes(): void {
+ register_rest_route('aksrt/v1', '/promotion/me', [
+ 'methods' => \WP_REST_Server::READABLE,
+ 'callback' => [$this, 'rest_me'],
+ 'permission_callback' => static fn() => is_user_logged_in(),
+ ]);
+ register_rest_route('aksrt/v1', '/promotion/payout-accounts', [
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => [$this, 'rest_save_payout_accounts'],
+ 'permission_callback' => static fn() => is_user_logged_in(),
+ 'args' => ['accounts' => ['required' => true, 'type' => 'array']],
+ ]);
+ }
+
+ public function rest_me(): \WP_REST_Response {
+ $uid = get_current_user_id();
+ $link = $this->create_link($uid);
+ $stats = $this->stats($uid);
+ $settings = \AKSRT\Settings\Settings::get_affiliate_settings();
+ return new \WP_REST_Response([
+ 'code' => $link['code'],
+ 'url' => $link['url'],
+ 'ratio' => (float) ($settings['ratio'] ?? 5),
+ 'order_types' => (array) ($settings['order_types'] ?? ['membership', 'content']),
+ 'rules' => ['window_days' => (int) ($settings['window_days'] ?? 30), 'settlement_days' => (int) ($settings['settlement_days'] ?? 7)],
+ 'stats' => $stats,
+ ], 200);
+ }
+
+ public function rest_save_payout_accounts(\WP_REST_Request $request): \WP_REST_Response|\WP_Error {
+ $accounts = $request->get_param('accounts');
+ if (!is_array($accounts)) {
+ return new \WP_Error('aksrt_payout_accounts', __('收款账户格式无效。', 'aksrt'), ['status' => 400]);
+ }
+ global $wpdb;
+ $table = \AKSRT\Core\DBMigrator::table('payout_account');
+ if ($table === '') {
+ return new \WP_Error('aksrt_payout_unavailable', __('收款账户服务不可用。', 'aksrt'), ['status' => 500]);
+ }
+ $uid = get_current_user_id();
+ $now = current_time('mysql', true);
+ foreach (['alipay', 'wechat'] as $channel) {
+ $value = isset($accounts[$channel]) ? sanitize_text_field((string) $accounts[$channel]) : '';
+ if ($value === '') {
+ $wpdb->update($table, ['status' => 'inactive', 'updated_at' => $now], ['user_id' => $uid, 'channel' => $channel], ['%s', '%s'], ['%d', '%s']);
+ continue;
+ }
+ $wpdb->replace($table, ['user_id' => $uid, 'channel' => $channel, 'attachment_id' => 0, 'account_hash' => hash('sha256', $value), 'status' => 'active', 'created_at' => $now, 'updated_at' => $now], ['%d', '%s', '%d', '%s', '%s', '%s', '%s']);
+ }
+ return new \WP_REST_Response(['success' => true], 200);
+ }
+
+ private function resolve_promoter(string $code): int {
+ $code = sanitize_key($code);
+ if ($code === '') {
+ return 0;
+ }
+ $users = get_users(['meta_key' => self::META_CODE, 'meta_value' => $code, 'number' => 1, 'fields' => 'ID']);
+ return !empty($users) ? (int) $users[0] : 0;
+ }
+}
diff --git a/src/Payment/Withdrawal.php b/src/Payment/Withdrawal.php
index a1d06669..a366efef 100644
--- a/src/Payment/Withdrawal.php
+++ b/src/Payment/Withdrawal.php
@@ -110,6 +110,11 @@ class Withdrawal {
}
$wpdb->query('START TRANSACTION');
try {
+ $open = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} WHERE user_id = %d AND status IN ('pending','processing') FOR UPDATE", $user_id));
+ if ($open > 0) {
+ $wpdb->query('ROLLBACK');
+ return new \WP_Error('withdraw_pending', __('已有待处理提现申请', 'aksrt'), ['status' => 409]);
+ }
$meta_id = $wpdb->get_var($wpdb->prepare("SELECT umeta_id FROM {$meta_table} WHERE user_id = %d AND meta_key = %s LIMIT 1 FOR UPDATE", $user_id, $meta_key));
if (!$meta_id) {
throw new \RuntimeException('wallet row unavailable');
@@ -222,6 +227,15 @@ class Withdrawal {
if ($old_status === $new_status && $note === '') {
return true;
}
+ $transitions = [
+ self::STATUS_PENDING => [self::STATUS_PROCESSING, self::STATUS_REJECTED],
+ self::STATUS_PROCESSING => [self::STATUS_COMPLETED, self::STATUS_REJECTED],
+ self::STATUS_COMPLETED => [],
+ self::STATUS_REJECTED => [],
+ ];
+ if (!in_array($new_status, $transitions[$old_status] ?? [], true)) {
+ return new \WP_Error('aksrt_withdraw_transition', __('提现状态转换不合法', 'aksrt'), ['status' => 409]);
+ }
$meta = $this->parse_account_meta((string) ($row['account_info'] ?? ''));
$user_id = (int) ($row['user_id'] ?? 0);
@@ -459,7 +473,8 @@ class Withdrawal {
$row['meta'] = $meta;
$row['gross_amount'] = $gross;
$row['fee_rate'] = (int) ($meta['fee_rate'] ?? 0);
- $row['display_account'] = (string) ($meta['account'] ?? $row['account_info'] ?? '');
+ $display = (string) ($meta['account'] ?? $row['account_info'] ?? '');
+ $row['display_account'] = strlen($display) > 4 ? substr($display, 0, 2) . str_repeat('*', max(2, strlen($display) - 4)) . substr($display, -2) : '****';
$row['source'] = (string) ($meta['source'] ?? 'balance');
return $row;
}
diff --git a/src/Settings/CreditsPaymentGroups.php b/src/Settings/CreditsPaymentGroups.php
index ef6a86ac..676145bd 100644
--- a/src/Settings/CreditsPaymentGroups.php
+++ b/src/Settings/CreditsPaymentGroups.php
@@ -182,6 +182,11 @@ trait CreditsPaymentGroups {
return PaymentRuntime::get_payment_affiliate_settings();
}
+ public static function affiliate_enabled(): bool { return PaymentRuntime::affiliate_enabled(); }
+ public static function affiliate_ratio(): float { return PaymentRuntime::affiliate_ratio(); }
+ public static function get_affiliate_settings(): array { return PaymentRuntime::get_affiliate_settings(); }
+ public static function get_settlement_settings(): array { return PaymentRuntime::get_settlement_settings(); }
+
/**
* @return array{mode:string,test_enabled:bool,manual_review:bool,callback_secret:string,help_text:string}
*/
diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php
index 05b65a6f..daf62497 100644
--- a/src/Settings/PaymentRuntime.php
+++ b/src/Settings/PaymentRuntime.php
@@ -282,35 +282,38 @@ class PaymentRuntime {
*/
public static function get_payment_affiliate_settings(): array {
$settings = Settings::get_all();
- $note = trim((string) ($settings['pay_rebate_text_desc'] ?? ''));
-
+ $ratio = max(0, min(100, (float) ($settings['affiliate_ratio'] ?? 5)));
+ $fee = max(0, min(100, (float) ($settings['affiliate_withdrawal_fee'] ?? 5)));
+ $types = (array) ($settings['affiliate_order_types'] ?? ['membership', 'content']);
+ $types = array_values(array_intersect(['membership', 'content'], array_map('sanitize_key', $types)));
+ if (!$types) { $types = ['membership', 'content']; }
+ $channels = array_values(array_intersect(['alipay', 'wechat'], array_map('sanitize_key', (array) ($settings['affiliate_payout_channels'] ?? ['alipay', 'wechat']))));
+ if (!$channels) { $channels = ['alipay', 'wechat']; }
return [
- 'enabled' => !empty($settings['pay_rebate_s']),
- 'ratio' => max(0, min(100, (int) ($settings['pay_rebate_ratio'] ?? 10))),
- 'window_days' => max(1, min(365, (int) ($settings['payment_affiliate_window_days'] ?? 30))),
- 'require_paid_order' => !isset($settings['payment_affiliate_require_paid_order']) || !empty($settings['payment_affiliate_require_paid_order']),
- 'self_s' => !isset($settings['pay_rebate_self_s']) || !empty($settings['pay_rebate_self_s']),
- 'show_users' => !isset($settings['pay_rebate_show_users']) || !empty($settings['pay_rebate_show_users']),
- 'note' => $note !== '' ? $note : __('返佣链路复用邀请体系,订单归因和结算自动衔接当前策略字段。', 'aksrt'),
- 'user_ratio' => [
- 'user' => max(0, min(100, (int) ($settings['payment_affiliate_user_ratio'] ?? $settings['pay_rebate_ratio'] ?? 10))),
- 'vip1' => max(0, min(100, (int) ($settings['pay_rebate_ratio_vip_1'] ?? 0))),
- 'vip2' => max(0, min(100, (int) ($settings['pay_rebate_ratio_vip_2'] ?? 0))),
- ],
- 'poster' => [
- 'enabled' => !isset($settings['pay_rebate_poster_s']) || !empty($settings['pay_rebate_poster_s']),
- 'img' => esc_url_raw((string) ($settings['pay_rebate_poster_img'] ?? '')),
- 'title' => sanitize_text_field((string) ($settings['pay_rebate_poster_title'] ?? '')),
- 'desc' => sanitize_text_field((string) ($settings['pay_rebate_poster_desc'] ?? '')),
- ],
- 'text' => [
- 'desc' => sanitize_textarea_field((string) ($settings['pay_rebate_text_desc'] ?? '')),
- 'details' => sanitize_textarea_field((string) ($settings['pay_rebate_text_details'] ?? '')),
- 'discount' => sanitize_text_field((string) ($settings['pay_rebate_text_discount'] ?? '')),
- ],
+ 'enabled' => !empty($settings['affiliate_enabled']),
+ 'ratio' => $ratio,
+ 'order_types' => $types,
+ 'window_days' => max(1, min(365, (int) ($settings['affiliate_touch_window_days'] ?? 30))),
+ 'settlement_days' => max(0, min(90, (int) ($settings['affiliate_settlement_days'] ?? 7))),
+ 'min_withdrawal' => max(0, min(999999, (float) ($settings['affiliate_min_withdrawal'] ?? 50))),
+ 'withdrawal_fee' => $fee,
+ 'payout_channels' => $channels,
+ 'allow_self_purchase' => !empty($settings['affiliate_allow_self_purchase']),
+ 'show_stats' => !isset($settings['affiliate_show_stats']) || !empty($settings['affiliate_show_stats']),
+ 'promotion_text' => sanitize_textarea_field((string) ($settings['affiliate_promotion_text'] ?? '')),
+ 'earnings_text' => sanitize_textarea_field((string) ($settings['affiliate_earnings_text'] ?? '')),
+ 'withdrawal_agreement' => sanitize_textarea_field((string) ($settings['affiliate_withdrawal_agreement'] ?? '')),
];
}
+ public static function affiliate_enabled(): bool { return !empty(self::get_payment_affiliate_settings()['enabled']); }
+ public static function affiliate_ratio(): float { return (float) self::get_payment_affiliate_settings()['ratio']; }
+ public static function get_affiliate_settings(): array { return self::get_payment_affiliate_settings(); }
+ public static function get_settlement_settings(): array {
+ $s = self::get_payment_affiliate_settings();
+ return ['observation_days' => (int) $s['settlement_days'], 'retry_limit' => 100];
+ }
+
/**
* @return array
*/
@@ -438,14 +441,14 @@ class PaymentRuntime {
/** @return array{min_amount:float,service_charge_rate:int,daily_count:int,daily_limit:float,reset_time:string} */
public static function get_withdrawal_settings(): array {
$s = Settings::get_all();
-
+ $affiliate = self::get_payment_affiliate_settings();
return [
- 'min_amount' => max(0, min(99999, (float) ($s['pay_rebate_withdraw_lowest_money'] ?? 50))),
- 'service_charge_rate' => max(0, min(50, (int) ($s['withdraw_service_charge'] ?? 5))),
- 'daily_count' => max(0, min(99999, (int) ($s['withdraw_daily_count'] ?? 3))),
- 'daily_limit' => max(0, min(999999, (float) ($s['withdraw_daily_limit'] ?? 50))),
- 'reset_time' => preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', (string) ($s['withdraw_reset_time'] ?? '00:00'))
- ? (string) $s['withdraw_reset_time']
+ 'min_amount' => (float) $affiliate['min_withdrawal'],
+ 'service_charge_rate' => (int) round((float) $affiliate['withdrawal_fee']),
+ 'daily_count' => max(0, min(99999, (int) ($s['affiliate_daily_withdrawal_count'] ?? 3))),
+ 'daily_limit' => max(0, min(999999, (float) ($s['affiliate_daily_withdrawal_limit'] ?? 500))),
+ 'reset_time' => preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', (string) ($s['affiliate_withdrawal_reset_time'] ?? '00:00'))
+ ? (string) $s['affiliate_withdrawal_reset_time']
: '00:00',
];
}
diff --git a/src/Settings/UserRuntime.php b/src/Settings/UserRuntime.php
index 7463de34..9a389405 100644
--- a/src/Settings/UserRuntime.php
+++ b/src/Settings/UserRuntime.php
@@ -84,14 +84,14 @@ class UserRuntime {
*/
public static function get_user_center_settings(): array {
$settings = Settings::get_all();
- $raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,comments,credits,revenue,favorites,auth';
+ $raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,comments,credits,promotion,revenue,favorites,auth';
$tabs = is_array($raw_tabs)
? $raw_tabs
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
// Built-in switchable panels. VIP is always auto-injected when enabled
// by AKSRT\User\Center — keep it allowed for explicit config too.
- $allowed_tabs = ['profile', 'level', 'comments', 'favorites', 'credits', 'revenue', 'vip', 'orders', 'auth', 'settings'];
+ $allowed_tabs = ['profile', 'level', 'comments', 'favorites', 'credits', 'promotion', 'revenue', 'vip', 'orders', 'auth', 'settings'];
$normalized_tabs = ['profile'];
foreach ((array) $tabs as $tab) {
diff --git a/src/User/Center.php b/src/User/Center.php
index ab78601c..443fc90e 100644
--- a/src/User/Center.php
+++ b/src/User/Center.php
@@ -78,7 +78,7 @@ class Center {
$revenue_request = [];
if ($tab === 'revenue') {
$reward_tab = sanitize_key((string) $request->get_param('reward_tab'));
- if (!in_array($reward_tab, ['detail', 'orders', 'users'], true)) {
+ if (!in_array($reward_tab, ['detail', 'orders'], true)) {
$reward_tab = 'detail';
}
$revenue_request = [
@@ -188,6 +188,12 @@ class Center {
'priority' => 55,
'type' => 'panel',
],
+ 'promotion' => [
+ 'label' => __('推广中心', 'aksrt'),
+ 'icon' => 'share-alt',
+ 'priority' => 54,
+ 'type' => 'panel',
+ ],
'notif' => [
'label' => __('消息通知', 'aksrt'),
'icon' => 'bell',
@@ -218,9 +224,10 @@ class Center {
&& !empty(\AKSRT\User\Level::settings()['enabled']);
$credits_on = class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::credits_enabled();
$balance_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
- $revenue_tab_on = class_exists('\AKSRT\Settings\Settings')
- && !empty(\AKSRT\Settings\Settings::get_payment_affiliate_settings()['enabled'])
- && \AKSRT\Settings\Settings::invite_registration_enabled();
+ $promotion_tab_on = class_exists('\AKSRT\Settings\Settings')
+ && !empty(\AKSRT\Settings\Settings::get_affiliate_settings()['enabled']);
+ $revenue_tab_on = $promotion_tab_on || (class_exists('\AKSRT\Settings\Settings')
+ && !empty(\AKSRT\Settings\Settings::get_payment_revenue_settings()['enabled']));
$creator_tab_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_revenue_settings()['enabled']);
$vip_on = class_exists('\AKSRT\Settings\Settings')
@@ -299,6 +306,12 @@ class Center {
}
array_splice($order, $insert_at, 0, ['revenue']);
}
+ if ($promotion_tab_on && !in_array('promotion', $order, true)) {
+ $insert_at = count($order);
+ $idx = array_search('revenue', $order, true);
+ if ($idx !== false) { $insert_at = (int) $idx; }
+ array_splice($order, $insert_at, 0, ['promotion']);
+ }
$auth_on = class_exists('\AKSRT\User\AuthApply')
&& !empty(\AKSRT\User\AuthApply::settings()['enabled']);
if ($auth_on && !in_array('auth', $order, true)) {
@@ -321,6 +334,9 @@ class Center {
if ($key === 'revenue' && !$revenue_tab_on) {
continue;
}
+ if ($key === 'promotion' && !$promotion_tab_on) {
+ continue;
+ }
if ($key === 'vip' && !$vip_on) {
continue;
diff --git a/src/Widgets/CommonWidgetFields.php b/src/Widgets/CommonWidgetFields.php
new file mode 100644
index 00000000..cbb4323d
--- /dev/null
+++ b/src/Widgets/CommonWidgetFields.php
@@ -0,0 +1,11 @@
+'','title_link'=>'','display_pc'=>1,'display_mobile'=>1,'sticky'=>0,'animation'=>'none','background'=>'surface','dark_background'=>'surface']; }
+ public static function normalize(array $instance): array {
+ $d=self::defaults(); $out=$instance; $out['subtitle']=sanitize_text_field((string)($instance['subtitle']??$d['subtitle'])); $out['title_link']=esc_url_raw((string)($instance['title_link']??'')); foreach(['display_pc','display_mobile','sticky'] as $k)$out[$k]=!empty($instance[$k])?1:0; $a=sanitize_key((string)($instance['animation']??'none')); $out['animation']=in_array($a,['none','fade','slide'],true)?$a:'none'; $b=sanitize_key((string)($instance['background']??'surface')); $out['background']=in_array($b,['surface','transparent','primary'],true)?$b:'surface'; $db=sanitize_key((string)($instance['dark_background']??'surface')); $out['dark_background']=in_array($db,['surface','transparent','primary'],true)?$db:'surface'; return $out;
+ }
+}
diff --git a/src/Widgets/FaqWidget.php b/src/Widgets/FaqWidget.php
new file mode 100644
index 00000000..185d66f0
--- /dev/null
+++ b/src/Widgets/FaqWidget.php
@@ -0,0 +1,3 @@
+mode=$mode; parent::__construct($id, esc_html__('AKSRT '.$name,'aksrt'), ['description'=>esc_html__('结构化营销展示模块','aksrt')]); }
+ public function form($instance): void { $title=sanitize_text_field((string)($instance['title']??'')); $items=(array)($instance['items']??[]); if(!$items && !empty($instance['items_raw'])) $items=array_filter(array_map('trim',preg_split('/\r?\n/',(string)$instance['items_raw']))); $raw=implode("\n",array_map(static fn($row)=>is_array($row)?implode('|',array_map('strval',$row)):(string)$row,$items)); echo ''.esc_html__('标题','aksrt').'
'.esc_html__('条目(每行:标题|描述|链接)','aksrt').'
'; }
+ public function update($new_instance,$old_instance): array { $rows=[]; foreach(preg_split('/\r?\n/',(string)($new_instance['items_raw']??'')) as $line){$parts=array_map('trim',explode('|',$line,3)); if(trim(implode('',$parts))==='')continue; $rows[]=['title'=>sanitize_text_field($parts[0]??''),'desc'=>sanitize_text_field($parts[1]??''),'url'=>esc_url_raw($parts[2]??'')];} return ['title'=>sanitize_text_field($new_instance['title']??''),'items'=>$rows]; }
+ public function widget($args,$instance): void { echo $args['before_widget']; $title=apply_filters('widget_title',$instance['title']??'',$instance,$this->id_base); if($title) echo $args['before_title'].esc_html($title).$args['after_title']; $items=(array)($instance['items']??[]); echo ''.$args['after_widget']; }
+}
+final class PricingPlansWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_pricing_plans','定价计划','pricing');} }
+final class TestimonialsWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_testimonials','评价轮播','testimonials');} }
+final class FaqWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_faq','FAQ 常见问题','faq');} }
+final class StatsWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_stats','数据统计','stats');} }
+final class HighlightsWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_highlights','亮点卡片','highlights');} }
+final class PartnersWidget extends MarketingWidgetBase { public function __construct(){parent::__construct('aksrt_partners','合作伙伴滚动','partners');} }
diff --git a/src/Widgets/PartnersWidget.php b/src/Widgets/PartnersWidget.php
new file mode 100644
index 00000000..185d66f0
--- /dev/null
+++ b/src/Widgets/PartnersWidget.php
@@ -0,0 +1,3 @@
+