feat: add creator income order tracking\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 : [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'] ?? '')),
|
||||||
|
|||||||
Reference in New Issue
Block a user