feat: add creator income order tracking\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This commit is contained in:
2026-08-07 09:15:42 +08:00
parent 9504463403
commit 450b9f44b9
7 changed files with 217 additions and 301 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ class PayIncome {
} }
foreach ($items as $row) { foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0); $id = (int) ($row['id'] ?? 0);
$user_id = (int) ($row['user_id'] ?? 0); $user_id = (int) ($row['author_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null; $user = $user_id > 0 ? get_userdata($user_id) : null;
$st = (string) ($row['status'] ?? ''); $st = (string) ($row['status'] ?? '');
echo '<tr>'; echo '<tr>';
+8 -1
View File
@@ -55,11 +55,18 @@ trait PayOrders {
// Consume coupon + write affiliate commission (cash content only; idempotent). // Consume coupon + write affiliate commission (cash content only; idempotent).
$this->finalize_content_order_side_effects($order_number, $paid_order); $this->finalize_content_order_side_effects($order_number, $paid_order);
if (class_exists('\AKSRT\Payment\OrderRepository')) { if (class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status( $completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number, $order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED, \AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('内容权益已发放。', 'aksrt')] ['note' => __('内容权益已发放。', 'aksrt')]
); );
if ($completed && class_exists('\AKSRT\Payment\CreatorRevenue')) {
$completed_order = array_merge($paid_order, [
'order_number' => $order_number,
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
]);
\AKSRT\Payment\CreatorRevenue::get_instance()->create_from_paid_order($completed_order);
}
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@ class DBMigrator {
use DBMigratorIdentity; use DBMigratorIdentity;
/** Current schema version; changing it re-runs dbDelta. */ /** Current schema version; changing it re-runs dbDelta. */
const SCHEMA_VERSION = '1.18.0'; const SCHEMA_VERSION = '1.19.0';
const OPT_KEY = 'aksrt_db_schema_version'; const OPT_KEY = 'aksrt_db_schema_version';
+15 -5
View File
@@ -187,19 +187,29 @@ trait DBMigratorPart2 {
) {$charset_collate};"; ) {$charset_collate};";
dbDelta($sql_order); dbDelta($sql_order);
// ---------- 9. 收益台账 ---------- // ---------- 9. 作者收益台账 ----------
$sql_revenue = "CREATE TABLE {$prefix}aksrt_revenue ( $sql_revenue = "CREATE TABLE {$prefix}aksrt_revenue (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL, author_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
order_number VARCHAR(32) NOT NULL DEFAULT '', order_number VARCHAR(32) NOT NULL DEFAULT '',
amount DECIMAL(10,2) NOT NULL DEFAULT 0, post_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
type VARCHAR(32) NOT NULL DEFAULT 'income', buyer_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
currency VARCHAR(8) NOT NULL DEFAULT 'CNY',
original_amount DECIMAL(18,2) NOT NULL DEFAULT 0,
share_ratio DECIMAL(5,2) NOT NULL DEFAULT 0,
author_level TINYINT UNSIGNED NOT NULL DEFAULT 0,
amount DECIMAL(18,2) NOT NULL DEFAULT 0,
type VARCHAR(32) NOT NULL DEFAULT 'order',
status VARCHAR(20) NOT NULL DEFAULT 'pending', status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL, created_at DATETIME NOT NULL,
settled_at DATETIME DEFAULT NULL, settled_at DATETIME DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
KEY idx_user (user_id), UNIQUE KEY uniq_creator_order (author_id, order_number),
KEY idx_author (author_id),
KEY idx_order (order_number), KEY idx_order (order_number),
KEY idx_post (post_id),
KEY idx_buyer (buyer_id),
KEY idx_currency (currency),
KEY idx_status (status), KEY idx_status (status),
KEY idx_created (created_at) KEY idx_created (created_at)
) {$charset_collate};"; ) {$charset_collate};";
+12 -141
View File
@@ -19,154 +19,25 @@ class CreatorRevenue {
} }
public function init(): void { public function init(): void {
add_action('aksrt_order_paid', [$this, 'handle_order_paid'], 20, 2); // Revenue is generated by the content fulfillment completion boundary.
} }
/** /**
* Whether creator revenue settlement is enabled. * Create creator income after content entitlement has been delivered.
*/
private function is_enabled(): bool {
if (!class_exists('AKSRT\\Payment\\Revenue') || !class_exists('\AKSRT\Settings\Settings')) {
return false;
}
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
return !empty($settings['enabled']);
}
/**
* Resolve share ratio (percentage 0-100) for a given product author.
*/
private function resolve_share_ratio(int $author_id): float {
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
$ratio = (float) ($settings['share_ratio'] ?? 0);
// VIP-tier author overrides, if configured (>0 wins).
if ($author_id > 0 && function_exists('aksrt_get_user_vip_level')) {
$level = (int) aksrt_get_user_vip_level($author_id);
if ($level === 1 && (float) ($settings['ratio_vip1'] ?? 0) > 0) {
$ratio = (float) $settings['ratio_vip1'];
} elseif ($level >= 2 && (float) ($settings['ratio_vip2'] ?? 0) > 0) {
$ratio = (float) $settings['ratio_vip2'];
}
}
return max(0.0, min(100.0, $ratio));
}
/**
* Guard: has this order already been settled into revenue?
*/
private function already_settled(string $order_number): bool {
if ($order_number === '' || !class_exists('\AKSRT\Core\DBMigrator')) {
return false;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('revenue');
if ($table === '') {
return false;
}
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE order_number = %s AND type = %s",
$order_number,
'order'
));
return $count > 0;
}
/**
* aksrt_order_paid listener: credit creators their share.
* *
* @param string $order_number Paid order number.
* @param array $order Order row (status=paid).
*/
public function handle_order_paid(string $order_number, array $order): void {
if (!$this->is_enabled() || $order_number === '') {
return;
}
if ($this->already_settled($order_number)) {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
$this->settle_non_shop_cash_order($order_number, $order, $buyer_id);
}
/**
* Content / topic cash orders (no shop line items).
*/
private function settle_non_shop_cash_order(string $order_number, array $order, int $buyer_id): void {
$meta = $this->decode_note($order);
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
$currency = sanitize_key((string) ($order['currency'] ?? ''));
if ($payment_method === 'points' || $currency === 'points') {
return;
}
$base_amount = round((float) ($order['amount'] ?? $meta['payable'] ?? 0), 2);
if ($base_amount <= 0) {
return;
}
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
$product_id = (int) ($order['product_id'] ?? 0);
if ($product_id <= 0) {
return;
}
$post = get_post($product_id);
if (!($post instanceof \WP_Post)) {
return;
}
$is_content = !empty($meta['content_pay'])
&& $post->post_type === 'post';
// Resource without content_pay and not a known cash kind: skip.
if (!$is_content) {
return;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $buyer_id) {
return;
}
$ratio = $this->resolve_share_ratio($author_id);
if ($ratio <= 0) {
return;
}
$amount = round($base_amount * $ratio / 100, 2);
if ($amount <= 0) {
return;
}
// Re-check after shop-less path; shop path already returned.
if ($this->already_settled($order_number)) {
return;
}
Revenue::get_instance()->add($author_id, $amount, $order_number, 'order');
}
/**
* @param array<string,mixed> $order * @param array<string,mixed> $order
* @return array<string,mixed>
*/ */
private function decode_note(array $order): array { public function create_from_paid_order(array $order): int {
if (isset($order['meta_payload']) && is_array($order['meta_payload'])) { if (!$this->is_enabled() || !class_exists('AKSRT\\Payment\\Revenue')) {
return $order['meta_payload']; return 0;
} }
$note = (string) ($order['note'] ?? ''); return Revenue::get_instance()->create_from_paid_order($order);
if ($note === '') {
return [];
} }
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : []; private function is_enabled(): bool {
if (!class_exists('AKSRT\\Settings\\Settings')) {
return false;
}
return !empty(\AKSRT\Settings\Settings::get_payment_revenue_settings()['enabled']);
} }
} }
+142 -114
View File
@@ -12,7 +12,9 @@ class Revenue {
private static $instance = null; private static $instance = null;
public static function get_instance(): self { public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self(); if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance; return self::$instance;
} }
@@ -21,27 +23,90 @@ class Revenue {
} }
/** /**
* 记录收益 * Create one pending creator-income record from a completed paid order.
*
* @param array<string,mixed> $order
*/ */
public function add(int $user_id, float $amount, string $order_number = '', string $type = 'income'): int { public function create_from_paid_order(array $order): int {
global $wpdb; global $wpdb;
if ($user_id <= 0 || $amount <= 0) return 0; $order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
if ($order_number === '' || (string) ($order['status'] ?? '') !== 'completed') {
return 0;
}
$wpdb->insert($this->table(), [ $meta = $this->decode_order_meta($order);
'user_id' => $user_id, if (empty($meta['content_pay'])) {
'order_number' => sanitize_text_field($order_number), return 0;
}
$post_id = (int) ($order['product_id'] ?? $meta['post_id'] ?? 0);
$buyer_id = (int) ($order['user_id'] ?? 0);
$post = $post_id > 0 ? get_post($post_id) : null;
if (!($post instanceof \WP_Post) || $post->post_type !== 'post') {
return 0;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $buyer_id) {
return 0;
}
$currency = sanitize_key((string) ($order['currency'] ?? 'CNY'));
$is_points = $currency === 'points' || ($meta['pay_modo'] ?? '') === 'points';
$currency = $is_points ? 'points' : 'CNY';
$original_amount = round((float) ($order['amount'] ?? $meta['amount'] ?? $meta['payable'] ?? 0), 2);
if ($is_points) {
$original_amount = (float) ceil($original_amount);
}
if ($original_amount <= 0) {
return 0;
}
$author_level = function_exists('aksrt_get_user_vip_level')
? max(0, (int) aksrt_get_user_vip_level($author_id))
: 0;
$ratio = $this->resolve_share_ratio($author_level);
if ($ratio <= 0) {
return 0;
}
$amount = $is_points
? (float) max(1, (int) floor($original_amount * $ratio / 100))
: round($original_amount * $ratio / 100, 2);
if ($amount <= 0) {
return 0;
}
$inserted = $wpdb->insert($this->table(), [
'author_id' => $author_id,
'order_number' => $order_number,
'post_id' => $post_id,
'buyer_id' => $buyer_id,
'currency' => $currency,
'original_amount' => $original_amount,
'share_ratio' => $ratio,
'author_level' => $author_level,
'amount' => $amount, 'amount' => $amount,
'type' => sanitize_key($type), 'type' => 'order',
'status' => 'pending', 'status' => 'pending',
'created_at' => current_time('mysql'), 'created_at' => current_time('mysql'),
], ['%d', '%s', '%f', '%s', '%s', '%s']); ], ['%d', '%s', '%d', '%d', '%s', '%f', '%f', '%d', '%f', '%s', '%s', '%s']);
if ($inserted === false) {
// Duplicate callbacks are a successful no-op by contract.
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$this->table()} WHERE author_id = %d AND order_number = %s LIMIT 1",
$author_id,
$order_number
));
return (int) $existing;
}
$id = (int) $wpdb->insert_id; $id = (int) $wpdb->insert_id;
if ($id > 0) { if ($id > 0) {
do_action('aksrt_creator_income_added', $user_id, $amount, [ do_action('aksrt_creator_income_added', $author_id, $amount, [
'order_number' => $order_number, 'order_number' => $order_number,
'type' => $type, 'currency' => $currency,
'revenue_id' => $id, 'revenue_id' => $id,
]); ]);
} }
@@ -49,159 +114,122 @@ class Revenue {
} }
/** /**
* 结算收益 * Legacy generic insertion retained as the native income page entry point.
*/
public function add(int $user_id, float $amount, string $order_number = '', string $type = 'income'): int {
global $wpdb;
if ($user_id <= 0 || $amount <= 0) {
return 0;
}
$order_number = sanitize_text_field($order_number);
$wpdb->insert($this->table(), [
'author_id' => $user_id,
'order_number' => $order_number,
'amount' => round($amount, 2),
'original_amount' => round($amount, 2),
'currency' => 'CNY',
'type' => sanitize_key($type),
'status' => 'pending',
'created_at' => current_time('mysql'),
], ['%d', '%s', '%f', '%f', '%s', '%s', '%s', '%s']);
return (int) $wpdb->insert_id;
}
/**
* Settle one pending record through its matching atomic balance service.
*/ */
public function settle(int $revenue_id): bool { public function settle(int $revenue_id): bool {
global $wpdb; global $wpdb;
if (!class_exists('\AKSRT\Credits\Balance')) {
return false;
}
$wpdb->query('START TRANSACTION');
try {
$revenue = $wpdb->get_row($wpdb->prepare( $revenue = $wpdb->get_row($wpdb->prepare(
"SELECT id, user_id, amount, order_number FROM {$this->table()} WHERE id = %d AND status = %s FOR UPDATE", "SELECT id, author_id, amount, currency, order_number FROM {$this->table()} WHERE id = %d AND status = %s",
$revenue_id, $revenue_id,
'pending' 'pending'
), ARRAY_A); ), ARRAY_A);
if (!is_array($revenue)) { if (!is_array($revenue)) {
$wpdb->query('ROLLBACK');
return false; return false;
} }
$credited = \AKSRT\Credits\Balance::add( if ((string) $revenue['currency'] === 'points' && class_exists('\\AKSRT\\Credits\\Credits')) {
(int) $revenue['user_id'], $credited = \AKSRT\Credits\Credits::add((int) $revenue['author_id'], (int) $revenue['amount'], 'creator_income', ['type' => 'revenue', 'id' => $revenue_id]);
(float) $revenue['amount'], } elseif (class_exists('\\AKSRT\\Credits\\Balance')) {
'creator_income', $credited = \AKSRT\Credits\Balance::add((int) $revenue['author_id'], (float) $revenue['amount'], 'creator_income', ['type' => 'revenue', 'id' => $revenue_id]);
['type' => 'revenue', 'id' => $revenue_id], } else {
sprintf(__('创作收益结算(订单 %s)', 'aksrt'), (string) $revenue['order_number']) return false;
); }
if (is_wp_error($credited)) { if (is_wp_error($credited)) {
$wpdb->query('ROLLBACK');
return false; return false;
} }
$settled = $wpdb->update( $updated = $wpdb->query($wpdb->prepare(
$this->table(), "UPDATE {$this->table()} SET status = %s, settled_at = %s WHERE id = %d AND status = %s",
['status' => 'settled', 'settled_at' => current_time('mysql')], 'settled', current_time('mysql'), $revenue_id, 'pending'
['id' => $revenue_id, 'status' => 'pending'], ));
['%s', '%s'], return $updated === 1;
['%d', '%s']
);
if ($settled !== 1) {
$wpdb->query('ROLLBACK');
return false;
} }
$wpdb->query('COMMIT');
return true;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return false;
}
}
/**
* 获取用户收益列表
*/
public function get_user_revenues(int $user_id, array $args = []): array { public function get_user_revenues(int $user_id, array $args = []): array {
global $wpdb; global $wpdb;
$where = ['author_id = %d'];
$where = ['user_id = %d'];
$params = [$user_id]; $params = [$user_id];
$status = sanitize_key((string) ($args['status'] ?? '')); $status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') { if ($status !== '') {
$where[] = 'status = %s'; $where[] = 'status = %s';
$params[] = $status; $params[] = $status;
} }
$limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0)); $offset = max(0, (int) ($args['offset'] ?? 0));
$sql = "SELECT * FROM {$this->table()} WHERE " . implode(' AND ', $where) . " ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit; $params[] = $limit;
$params[] = $offset; $params[] = $offset;
return (array) $wpdb->get_results($wpdb->prepare(
return (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); "SELECT * FROM {$this->table()} WHERE " . implode(' AND ', $where) . " ORDER BY created_at DESC LIMIT %d OFFSET %d",
$params
), ARRAY_A);
} }
/**
* Admin list (all users) with optional status / user filter.
*
* @param array{status?:string,user_id?:int,limit?:int,offset?:int} $args
* @return array{items:array<int,array>,total:int,page:int,max_pages:int}
*/
public function get_list(array $args = []): array { public function get_list(array $args = []): array {
global $wpdb; global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0)); $offset = max(0, (int) ($args['offset'] ?? 0));
$status = sanitize_key((string) ($args['status'] ?? ''));
$user_id = (int) ($args['user_id'] ?? 0);
$where = []; $where = [];
$params = []; $params = [];
if ($status !== '') { if (($status = sanitize_key((string) ($args['status'] ?? ''))) !== '') {
$where[] = 'status = %s'; $where[] = 'status = %s';
$params[] = $status; $params[] = $status;
} }
if ($user_id > 0) { if (($user_id = (int) ($args['user_id'] ?? 0)) > 0) {
$where[] = 'user_id = %d'; $where[] = 'author_id = %d';
$params[] = $user_id; $params[] = $user_id;
} }
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int) ($params ? $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} {$where_sql}", $params)) : $wpdb->get_var("SELECT COUNT(*) FROM {$this->table()}"));
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; $items_params = array_merge($params, [$limit, $offset]);
$total = $params $items = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d", $items_params), ARRAY_A);
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) return ['items' => $items ?: [], 'total' => $total, 'page' => (int) floor($offset / $limit) + 1, 'max_pages' => (int) ceil(max(1, $total) / $limit)];
: (int) $wpdb->get_var($count_sql);
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return [
'items' => $items ?: [],
'total' => $total,
'page' => (int) floor($offset / $limit) + 1,
'max_pages' => (int) ceil(max(1, $total) / $limit),
];
} }
/**
* 获取收益统计
*/
public function get_stats(int $user_id = 0): array { public function get_stats(int $user_id = 0): array {
global $wpdb; global $wpdb;
$where = $user_id > 0 ? 'WHERE author_id = %d' : '';
$where = ''; $params = $user_id > 0 ? [$user_id] : [];
$params = []; $row = $params ? $wpdb->get_row($wpdb->prepare("SELECT COALESCE(SUM(amount),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN amount ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN amount ELSE 0 END),0) settled, COUNT(*) count FROM {$this->table()} {$where}", $params), ARRAY_A) : $wpdb->get_row("SELECT COALESCE(SUM(amount),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN amount ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN amount ELSE 0 END),0) settled, COUNT(*) count FROM {$this->table()}", ARRAY_A);
if ($user_id > 0) { return ['total' => (float) ($row['total'] ?? 0), 'pending' => (float) ($row['pending'] ?? 0), 'settled' => (float) ($row['settled'] ?? 0), 'count' => (int) ($row['count'] ?? 0)];
$where = 'WHERE user_id = %d';
$params[] = $user_id;
} }
$sql = "SELECT private function resolve_share_ratio(int $author_level): float {
COALESCE(SUM(amount), 0) as total, $settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) as pending, $ratio = $author_level >= 2 ? 80.0 : ($author_level === 1 ? 70.0 : 60.0);
COALESCE(SUM(CASE WHEN status = 'settled' THEN amount ELSE 0 END), 0) as settled, $key = $author_level >= 2 ? 'ratio_vip2' : ($author_level === 1 ? 'ratio_vip1' : 'share_ratio');
COUNT(*) as count if (isset($settings[$key]) && is_numeric($settings[$key])) {
FROM {$this->table()} {$where}"; $ratio = (float) $settings[$key];
}
if (!empty($params)) { return max(0, min(100, $ratio));
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
} else {
$row = $wpdb->get_row($sql, ARRAY_A);
} }
return [ private function decode_order_meta(array $order): array {
'total' => (float) ($row['total'] ?? 0), if (is_array($order['meta_payload'] ?? null)) {
'pending' => (float) ($row['pending'] ?? 0), return $order['meta_payload'];
'settled' => (float) ($row['settled'] ?? 0), }
'count' => (int) ($row['count'] ?? 0), $decoded = json_decode((string) ($order['note'] ?? ''), true);
]; return is_array($decoded) ? $decoded : [];
} }
} }
+3 -3
View File
@@ -245,9 +245,9 @@ class PaymentRuntime {
return [ return [
'enabled' => !empty($settings['pay_income_s']), 'enabled' => !empty($settings['pay_income_s']),
'share_ratio' => max(0, min(100, (int) ($settings['payment_revenue_share_ratio'] ?? 70))), 'share_ratio' => max(0, min(100, (int) ($settings['payment_revenue_share_ratio'] ?? 60))),
'ratio_vip1' => max(0, min(100, (int) ($settings['ratio_vip_1'] ?? 0))), 'ratio_vip1' => max(0, min(100, (int) ($settings['ratio_vip_1'] ?? 70))),
'ratio_vip2' => max(0, min(100, (int) ($settings['ratio_vip_2'] ?? 0))), 'ratio_vip2' => max(0, min(100, (int) ($settings['ratio_vip_2'] ?? 80))),
'settlement_threshold' => max(0, (int) ($settings['payment_revenue_settlement_threshold'] ?? 100)), 'settlement_threshold' => max(0, (int) ($settings['payment_revenue_settlement_threshold'] ?? 100)),
'cycle_days' => max(1, min(365, (int) ($settings['payment_revenue_cycle_days'] ?? 30))), 'cycle_days' => max(1, min(365, (int) ($settings['payment_revenue_cycle_days'] ?? 30))),
'points_rule' => sanitize_textarea_field((string) ($settings['payment_revenue_points_rule'] ?? '')), 'points_rule' => sanitize_textarea_field((string) ($settings['payment_revenue_points_rule'] ?? '')),