feat: adjust affiliate commissions and withdrawal strategy

This commit is contained in:
kate522 committed 2026-08-16 23:11:01 +08:00
1 parent 8fc6fe5312
commit 929603b9e2
13 files changed
+93 -111

No files matched your search

+6 -13
View File
@@ -143,23 +143,15 @@ class SchemaPayment {
'groups' => [
['key' => 'policy', 'title' => '返佣策略', 'fields' => [
['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_ratio_user', 'label' => '普通会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_ratio_vip_1', 'label' => '一级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_ratio_vip_2', 'label' => '二级会员佣金比例(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
['key' => 'affiliate_order_types', 'label' => '适用订单类型', 'type' => 'checkbox', 'options' => [['value' => 'membership', 'label' => '会员订单'], ['value' => 'content', 'label' => '付费内容'], ['value' => 'credits', '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' => 'withdrawal', 'title' => '提现规则', 'fields' => [
['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'],
]],
['key' => 'text', 'title' => '文案设置', 'fields' => [
['key' => 'affiliate_promotion_text', 'label' => '推广说明', 'type' => 'textarea', 'default' => ''],
['key' => 'affiliate_payout_channels', 'label' => '可用收款渠道', 'type' => 'checkbox', 'options' => [['value' => 'alipay', 'label' => '支付宝收款码'], ['value' => 'wechat', 'label' => '微信收款码']], 'default' => ['alipay', 'wechat']],
]],
],
],
@@ -317,7 +309,7 @@ class SchemaPayment {
],
'withdrawal' => [
'key' => 'withdrawal',
'title' => '提现管理',
'title' => '提现策略',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
@@ -327,6 +319,7 @@ class SchemaPayment {
['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。'],
['key' => 'affiliate_payout_channels', 'label' => '可用收款渠道', 'type' => 'checkbox', 'options' => [['value' => 'alipay', 'label' => '支付宝收款码'], ['value' => 'wechat', 'label' => '微信收款码']], 'default' => ['alipay', 'wechat']],
]],
],
],
+1 -14
View File
@@ -383,20 +383,7 @@ trait PayHelpers {
return $empty;
}
$ratio = (float) ($aff['ratio'] ?? 0);
$user_ratio = is_array($aff['user_ratio'] ?? null) ? $aff['user_ratio'] : [];
$ref_vip = 0;
if (function_exists('aksrt_get_user_vip_level')) {
$ref_vip = (int) aksrt_get_user_vip_level($referrer_id);
}
if ($ref_vip >= 2 && isset($user_ratio['vip2']) && (float) $user_ratio['vip2'] > 0) {
$ratio = (float) $user_ratio['vip2'];
} elseif ($ref_vip >= 1 && isset($user_ratio['vip1']) && (float) $user_ratio['vip1'] > 0) {
$ratio = (float) $user_ratio['vip1'];
} elseif (isset($user_ratio['user'])) {
$ratio = (float) $user_ratio['user'];
}
$ratio = max(0.0, min(100.0, $ratio));
$ratio = (float) \AKSRT\Settings\Settings::affiliate_ratio_for_user($referrer_id);
if ($ratio <= 0) {
return $empty;
}
+6 -1
View File
@@ -338,6 +338,7 @@ trait BalanceServiceRest {
if (is_wp_error($refund)) return new \WP_Error('aksrt_exchange_recovery_failed', __('积分入账失败,余额退回失败,请联系管理员', 'aksrt'), ['status' => 500]);
return $add;
}
do_action('aksrt_points_exchange_completed', $user_id, (float) $amount, $points, (string) $exchange_ref);
return rest_ensure_response(['success' => true, 'data' => [
'message' => sprintf(__('兑换成功,获得 %d 积分', 'aksrt'), $points),
'wallet_balance' => Balance::balance($user_id),
@@ -387,6 +388,7 @@ trait BalanceServiceRest {
}
$amount = (int) $raw_amount;
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
$method = $method !== '' ? $method : 'alipay';
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
$withdraw_settings = class_exists('\\AKSRT\\Settings\\Settings')
? \AKSRT\Settings\Settings::get_withdrawal_settings()
@@ -398,7 +400,10 @@ trait BalanceServiceRest {
if ($account === '') {
return new \WP_Error('aksrt_withdraw_account', __('请填写收款账号信息', 'aksrt'), ['status' => 400]);
}
$result = \AKSRT\Payment\Withdrawal::get_instance()->create_balance_request($user_id, $amount, $method !== '' ? $method : 'alipay', $account, $withdraw_settings);
if (!in_array($method, (array) ($withdraw_settings['payout_channels'] ?? []), true)) {
return new \WP_Error('aksrt_withdraw_channel', __('当前收款渠道不可用', 'aksrt'), ['status' => 400]);
}
$result = \AKSRT\Payment\Withdrawal::get_instance()->create_balance_request($user_id, $amount, $method, $account, $withdraw_settings);
if (is_wp_error($result)) {
return $result;
}
+19 -6
View File
@@ -12,6 +12,7 @@ final class CommissionLedger {
public function init(): void {
add_action('aksrt_order_paid', [$this, 'on_order_paid'], 20, 2);
add_action('aksrt_order_status_changed', [$this, 'on_order_status_changed'], 20, 3);
add_action('aksrt_points_exchange_completed', [$this, 'on_points_exchange_completed'], 20, 4);
add_action('aksrt_affiliate_settle_due', [$this, 'settle_due']);
add_action('rest_api_init', [$this, 'register_routes']);
if (!wp_next_scheduled('aksrt_affiliate_settle_due')) {
@@ -23,6 +24,16 @@ final class CommissionLedger {
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 on_points_exchange_completed(int $user_id, float $amount, int $points, string $exchange_ref): void {
$this->record_paid_order([
'order_number' => 'points-exchange-' . sanitize_key($exchange_ref),
'product_type' => 'credits',
'user_id' => $user_id,
'amount' => $amount,
'exchange_ref' => $exchange_ref,
'points' => $points,
]);
}
public function record_paid_order(array $order): int {
if (!\AKSRT\Settings\Settings::affiliate_enabled()) return 0;
$type = sanitize_key((string) ($order['product_type'] ?? ''));
@@ -35,14 +46,16 @@ final class CommissionLedger {
$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'];
if ($promoter <= 0) return 0;
$ratio = (float) \AKSRT\Settings\PaymentRuntime::affiliate_ratio_for_user($promoter);
$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);
$identity = $order_type === 'credits' && !empty($order['exchange_ref'])
? 'credits:' . sanitize_key((string) $order['exchange_ref'])
: $order_no . ':' . $order_type;
$key = hash('sha256', 'affiliate:' . $identity);
$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']);
@@ -123,9 +136,9 @@ final class CommissionLedger {
$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 ($amount < (int)ceil((float)$settings['min_amount'])) 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]);
if (!in_array($method,(array)$settings['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()]; }
+2 -1
View File
@@ -197,7 +197,8 @@ final class ReferralAttribution {
return new \WP_REST_Response([
'code' => $link['code'],
'url' => $link['url'],
'ratio' => (float) ($settings['ratio'] ?? 5),
'current_ratio' => (float) \AKSRT\Settings\Settings::affiliate_ratio_for_user($uid),
'ratios' => (array) ($settings['ratios'] ?? []),
'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,
+3 -2
View File
@@ -176,14 +176,15 @@ trait CreditsPaymentGroups {
}
/**
* @return array{enabled:bool,ratio:int,window_days:int,require_paid_order:bool,note:string}
* @return array<string,mixed>
*/
public static function get_payment_affiliate_settings(): array {
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 affiliate_ratio_for_user(int $user_id = 0): float { return PaymentRuntime::affiliate_ratio_for_user($user_id > 0 ? $user_id : get_current_user_id()); }
public static function affiliate_ratio_for_level(int $level): float { return PaymentRuntime::affiliate_ratio_for_level($level); }
public static function get_affiliate_settings(): array { return PaymentRuntime::get_affiliate_settings(); }
public static function get_settlement_settings(): array { return PaymentRuntime::get_settlement_settings(); }
+22 -16
View File
@@ -282,30 +282,34 @@ class PaymentRuntime {
*/
public static function get_payment_affiliate_settings(): array {
$settings = Settings::get_all();
$ratio = max(0, min(100, (float) ($settings['affiliate_ratio'] ?? 5)));
$fee = max(0, min(100, (float) ($settings['affiliate_withdrawal_fee'] ?? 5)));
$clamp_ratio = static fn ($value): float => max(0, min(100, (float) $value));
$types = (array) ($settings['affiliate_order_types'] ?? ['membership', 'content']);
$types = array_values(array_intersect(['membership', 'content'], array_map('sanitize_key', $types)));
$types = array_values(array_intersect(['membership', 'content', 'credits'], 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['affiliate_enabled']),
'ratio' => $ratio,
'ratios' => [
'user' => $clamp_ratio($settings['affiliate_ratio_user'] ?? 5),
'vip_1' => $clamp_ratio($settings['affiliate_ratio_vip_1'] ?? 5),
'vip_2' => $clamp_ratio($settings['affiliate_ratio_vip_2'] ?? 5),
],
'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'] ?? '')),
];
}
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 affiliate_ratio_for_user(int $user_id): float {
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
return self::affiliate_ratio_for_level($level);
}
public static function affiliate_ratio_for_level(int $level): float {
$settings = self::get_payment_affiliate_settings();
$key = $level >= 2 ? 'vip_2' : ($level >= 1 ? 'vip_1' : 'user');
return (float) ($settings['ratios'][$key] ?? 0);
}
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();
@@ -436,18 +440,20 @@ class PaymentRuntime {
];
}
/** @return array{min_amount:float,service_charge_rate:int,daily_count:int,daily_limit:float,reset_time:string} */
/** @return array{min_amount:float,service_charge_rate:int,daily_count:int,daily_limit:float,reset_time:string,payout_channels:string[]} */
public static function get_withdrawal_settings(): array {
$s = Settings::get_all();
$affiliate = self::get_payment_affiliate_settings();
$channels = array_values(array_intersect(['alipay', 'wechat'], array_map('sanitize_key', (array) ($s['affiliate_payout_channels'] ?? ['alipay', 'wechat']))));
if (!$channels) { $channels = ['alipay', 'wechat']; }
return [
'min_amount' => (float) $affiliate['min_withdrawal'],
'service_charge_rate' => (int) round((float) $affiliate['withdrawal_fee']),
'min_amount' => max(0, min(999999, (float) ($s['affiliate_min_withdrawal'] ?? 50))),
'service_charge_rate' => (int) round(max(0, min(100, (float) ($s['affiliate_withdrawal_fee'] ?? 5)))),
'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',
'payout_channels' => $channels,
];
}