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

This commit is contained in:
kate522 committed 2026-08-07 09:15:42 +08:00
1 parent 9504463403
commit 450b9f44b9
7 files changed
+217 -301

No files matched your search

+160 -132
View File
@@ -12,7 +12,9 @@ class Revenue {
private static $instance = null;
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;
}
@@ -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;
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(), [
'user_id' => $user_id,
'order_number' => sanitize_text_field($order_number),
'amount' => $amount,
'type' => sanitize_key($type),
'status' => 'pending',
'created_at' => current_time('mysql'),
], ['%d', '%s', '%f', '%s', '%s', '%s']);
$meta = $this->decode_order_meta($order);
if (empty($meta['content_pay'])) {
return 0;
}
$id = (int) $wpdb->insert_id;
$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,
'type' => 'order',
'status' => 'pending',
'created_at' => current_time('mysql'),
], ['%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;
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,
'type' => $type,
'currency' => $currency,
'revenue_id' => $id,
]);
}
@@ -49,159 +114,122 @@ class Revenue {
}
/**
* 结算收益
* Legacy generic insertion retained as the native income page entry point.
*/
public function settle(int $revenue_id): bool {
public function add(int $user_id, float $amount, string $order_number = '', string $type = 'income'): int {
global $wpdb;
if (!class_exists('\AKSRT\Credits\Balance')) {
return false;
}
$wpdb->query('START TRANSACTION');
try {
$revenue = $wpdb->get_row($wpdb->prepare(
"SELECT id, user_id, amount, order_number FROM {$this->table()} WHERE id = %d AND status = %s FOR UPDATE",
$revenue_id,
'pending'
), ARRAY_A);
if (!is_array($revenue)) {
$wpdb->query('ROLLBACK');
return false;
}
$credited = \AKSRT\Credits\Balance::add(
(int) $revenue['user_id'],
(float) $revenue['amount'],
'creator_income',
['type' => 'revenue', 'id' => $revenue_id],
sprintf(__('创作收益结算(订单 %s)', 'aksrt'), (string) $revenue['order_number'])
);
if (is_wp_error($credited)) {
$wpdb->query('ROLLBACK');
return false;
}
$settled = $wpdb->update(
$this->table(),
['status' => 'settled', 'settled_at' => current_time('mysql')],
['id' => $revenue_id, 'status' => 'pending'],
['%s', '%s'],
['%d', '%s']
);
if ($settled !== 1) {
$wpdb->query('ROLLBACK');
return false;
}
$wpdb->query('COMMIT');
return true;
} catch (\Throwable $e) {
$wpdb->query('ROLLBACK');
return false;
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 {
global $wpdb;
$revenue = $wpdb->get_row($wpdb->prepare(
"SELECT id, author_id, amount, currency, order_number FROM {$this->table()} WHERE id = %d AND status = %s",
$revenue_id,
'pending'
), ARRAY_A);
if (!is_array($revenue)) {
return false;
}
if ((string) $revenue['currency'] === 'points' && class_exists('\\AKSRT\\Credits\\Credits')) {
$credited = \AKSRT\Credits\Credits::add((int) $revenue['author_id'], (int) $revenue['amount'], 'creator_income', ['type' => 'revenue', 'id' => $revenue_id]);
} elseif (class_exists('\\AKSRT\\Credits\\Balance')) {
$credited = \AKSRT\Credits\Balance::add((int) $revenue['author_id'], (float) $revenue['amount'], 'creator_income', ['type' => 'revenue', 'id' => $revenue_id]);
} else {
return false;
}
if (is_wp_error($credited)) {
return false;
}
$updated = $wpdb->query($wpdb->prepare(
"UPDATE {$this->table()} SET status = %s, settled_at = %s WHERE id = %d AND status = %s",
'settled', current_time('mysql'), $revenue_id, 'pending'
));
return $updated === 1;
}
public function get_user_revenues(int $user_id, array $args = []): array {
global $wpdb;
$where = ['user_id = %d'];
$where = ['author_id = %d'];
$params = [$user_id];
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$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));
$sql = "SELECT * FROM {$this->table()} WHERE " . implode(' AND ', $where) . " ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
return (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return (array) $wpdb->get_results($wpdb->prepare(
"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 {
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));
$status = sanitize_key((string) ($args['status'] ?? ''));
$user_id = (int) ($args['user_id'] ?? 0);
$where = [];
$where = [];
$params = [];
if ($status !== '') {
$where[] = 'status = %s';
if (($status = sanitize_key((string) ($args['status'] ?? ''))) !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
if ($user_id > 0) {
$where[] = 'user_id = %d';
if (($user_id = (int) ($args['user_id'] ?? 0)) > 0) {
$where[] = 'author_id = %d';
$params[] = $user_id;
}
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
$total = $params
? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params))
: (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),
];
$total = (int) ($params ? $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$this->table()} {$where_sql}", $params)) : $wpdb->get_var("SELECT COUNT(*) FROM {$this->table()}"));
$items_params = array_merge($params, [$limit, $offset]);
$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);
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 {
global $wpdb;
$where = $user_id > 0 ? 'WHERE author_id = %d' : '';
$params = $user_id > 0 ? [$user_id] : [];
$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);
return ['total' => (float) ($row['total'] ?? 0), 'pending' => (float) ($row['pending'] ?? 0), 'settled' => (float) ($row['settled'] ?? 0), 'count' => (int) ($row['count'] ?? 0)];
}
$where = '';
$params = [];
if ($user_id > 0) {
$where = 'WHERE user_id = %d';
$params[] = $user_id;
private function resolve_share_ratio(int $author_level): float {
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
$ratio = $author_level >= 2 ? 80.0 : ($author_level === 1 ? 70.0 : 60.0);
$key = $author_level >= 2 ? 'ratio_vip2' : ($author_level === 1 ? 'ratio_vip1' : 'share_ratio');
if (isset($settings[$key]) && is_numeric($settings[$key])) {
$ratio = (float) $settings[$key];
}
return max(0, min(100, $ratio));
}
$sql = "SELECT
COALESCE(SUM(amount), 0) as total,
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) as pending,
COALESCE(SUM(CASE WHEN status = 'settled' THEN amount ELSE 0 END), 0) as settled,
COUNT(*) as count
FROM {$this->table()} {$where}";
if (!empty($params)) {
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
} else {
$row = $wpdb->get_row($sql, ARRAY_A);
private function decode_order_meta(array $order): array {
if (is_array($order['meta_payload'] ?? null)) {
return $order['meta_payload'];
}
return [
'total' => (float) ($row['total'] ?? 0),
'pending' => (float) ($row['pending'] ?? 0),
'settled' => (float) ($row['settled'] ?? 0),
'count' => (int) ($row['count'] ?? 0),
];
$decoded = json_decode((string) ($order['note'] ?? ''), true);
return is_array($decoded) ? $decoded : [];
}
}