feat(payment): separate creator income settlement
This commit is contained in:
1 parent
cd93ea1656
commit
e20418e909
17 files changed
+451
-445
No files matched your search
@@ -51,12 +51,16 @@ class PayIncome {
|
||||
$svc = \AKSRT\Payment\Revenue::get_instance();
|
||||
$stats = $user_id > 0 ? $svc->get_stats($user_id) : $svc->get_stats();
|
||||
|
||||
$cash_stats = (array) ($stats['cash'] ?? []);
|
||||
$points_stats = (array) ($stats['points'] ?? []);
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: settled */
|
||||
__('合计 %1$s · 待结算 %2$s · 已结算 %3$s', 'aksrt'),
|
||||
number_format((float) ($stats['total'] ?? 0), 2),
|
||||
number_format((float) ($stats['pending'] ?? 0), 2),
|
||||
number_format((float) ($stats['settled'] ?? 0), 2)
|
||||
__('现金:合计 ¥%1$s · 待结算 ¥%2$s · 已结算 ¥%3$s;积分:合计 %4$s · 待结算 %5$s · 已结算 %6$s', 'aksrt'),
|
||||
number_format((float) ($cash_stats['total'] ?? 0), 2),
|
||||
number_format((float) ($cash_stats['pending'] ?? 0), 2),
|
||||
number_format((float) ($cash_stats['settled'] ?? 0), 2),
|
||||
number_format((float) ($points_stats['total'] ?? 0), 0),
|
||||
number_format((float) ($points_stats['pending'] ?? 0), 0),
|
||||
number_format((float) ($points_stats['settled'] ?? 0), 0)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0;">';
|
||||
@@ -68,7 +72,7 @@ class PayIncome {
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_income" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), 'reversed' => __('已冲正', 'aksrt'), 'pending_recovery' => __('待追缴', 'aksrt')] as $key => $label) {
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
@@ -116,7 +120,10 @@ class PayIncome {
|
||||
echo '<td>' . ($st === 'pending' ? '<input type="checkbox" form="aksrt-pay-batch-settle-income" name="revenue_ids[]" value="' . esc_attr((string) $id) . '" />' : '') . '</td>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td><td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . '</td>';
|
||||
$is_points = (string) ($row['currency'] ?? 'CNY') === 'points';
|
||||
$row_currency = $is_points ? __('积分', 'aksrt') : '¥';
|
||||
$row_decimals = $is_points ? 0 : 2;
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), $row_decimals) . ' ' . $row_currency) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
|
||||
@@ -48,11 +48,6 @@ class PaymentController extends ControllerBase {
|
||||
'callback' => [$this, 'settle_creator_income'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/creator-income/order/(?P<order_id>[a-zA-Z0-9_-]+)/reverse', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'reverse_creator_income'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [
|
||||
'methods' => 'GET',
|
||||
@@ -160,7 +155,10 @@ class PaymentController extends ControllerBase {
|
||||
* GET /payment/revenue — revenue stats with optional time-series
|
||||
*/
|
||||
public function get_revenue(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0];
|
||||
$revenue_stats = [
|
||||
'cash' => ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0],
|
||||
'points' => ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0],
|
||||
];
|
||||
if (class_exists('\AKSRT\Payment\Revenue')) {
|
||||
$revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats();
|
||||
}
|
||||
@@ -194,12 +192,14 @@ class PaymentController extends ControllerBase {
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
$granularity = sanitize_text_field((string) $request->get_param('granularity'));
|
||||
|
||||
// Default: last 30 days
|
||||
// Default: last 30 days in the WordPress site timezone.
|
||||
$timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
|
||||
$now = new \DateTimeImmutable('now', $timezone);
|
||||
if (!$date_from) {
|
||||
$date_from = gmdate('Y-m-d', strtotime('-30 days'));
|
||||
$date_from = $now->modify('-30 days')->format('Y-m-d');
|
||||
}
|
||||
if (!$date_to) {
|
||||
$date_to = gmdate('Y-m-d');
|
||||
$date_to = $now->format('Y-m-d');
|
||||
}
|
||||
|
||||
// Granularity: day, week, month
|
||||
@@ -212,27 +212,27 @@ class PaymentController extends ControllerBase {
|
||||
|
||||
$table = $prefix . 'aksrt_revenue';
|
||||
$results = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT DATE_FORMAT(created_at, %s) AS period,
|
||||
"SELECT currency, DATE_FORMAT(created_at, %s) AS period,
|
||||
SUM(amount) AS total_amount,
|
||||
COUNT(*) AS order_count
|
||||
FROM {$table}
|
||||
WHERE created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
|
||||
GROUP BY period
|
||||
WHERE status IN ('pending', 'settled')
|
||||
AND created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
|
||||
GROUP BY currency, period
|
||||
ORDER BY period ASC",
|
||||
$date_format,
|
||||
$date_from,
|
||||
$date_to
|
||||
));
|
||||
|
||||
$series = [];
|
||||
if ($results) {
|
||||
foreach ($results as $row) {
|
||||
$series[] = [
|
||||
'date' => (string) $row->period,
|
||||
'amount' => (float) $row->total_amount,
|
||||
'count' => (int) $row->order_count,
|
||||
];
|
||||
}
|
||||
$series = ['cash' => [], 'points' => []];
|
||||
foreach ((array) $results as $row) {
|
||||
$key = (string) ($row->currency ?? '') === 'points' ? 'points' : 'cash';
|
||||
$series[$key][] = [
|
||||
'date' => (string) $row->period,
|
||||
'amount' => (float) $row->total_amount,
|
||||
'count' => (int) $row->order_count,
|
||||
];
|
||||
}
|
||||
|
||||
return $series;
|
||||
@@ -277,22 +277,6 @@ class PaymentController extends ControllerBase {
|
||||
return $this->success(['message' => __('创作分成已结算', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/creator-income/order/{order_id}/reverse — cancel or reverse order income.
|
||||
*/
|
||||
public function reverse_creator_income(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Revenue')) {
|
||||
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
|
||||
}
|
||||
$order_id = sanitize_text_field((string) $request->get_param('order_id'));
|
||||
$params = $this->get_json_params($request);
|
||||
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
|
||||
if ($order_id === '' || !\AKSRT\Payment\Revenue::get_instance()->cancel_or_reverse($order_id, $reason)) {
|
||||
return $this->error(__('冲正失败', 'aksrt'));
|
||||
}
|
||||
return $this->success(['message' => __('创作分成已处理退款/冲正', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function get_withdrawals(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1, 'stats' => []]]);
|
||||
|
||||
@@ -164,24 +164,23 @@ class SchemaPayment {
|
||||
'revenue' => [
|
||||
'key' => 'revenue',
|
||||
'title' => '创作分成',
|
||||
'version' => '1.0.0',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'policy', 'title' => '分成策略', 'fields' => [
|
||||
['key' => 'basic', 'title' => '创作分成设置', 'fields' => [
|
||||
['key' => 'pay_income_s', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_revenue_share_ratio', 'label' => '作者分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 1, 'max' => 100],
|
||||
['key' => 'ratio_vip_1', 'label' => 'VIP1分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'ratio_vip_2', 'label' => 'VIP2分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'payment_revenue_settlement_threshold', 'label' => '最小结算金额', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 99999],
|
||||
['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
|
||||
['key' => 'payment_revenue_settlement_mode', 'label' => '结算模式', 'type' => 'select', 'default' => 'automatic', 'options' => [['value' => 'automatic', 'label' => '自动结算'], ['value' => 'manual', 'label' => '手动结算']]],
|
||||
['key' => 'payment_revenue_settlement_delay_hours', 'label' => '结算延迟(小时)', 'type' => 'number', 'default' => 24, 'min' => 0, 'max' => 8760],
|
||||
['key' => 'payment_revenue_points_rule', 'label' => '积分规则', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_revenue_note', 'label' => '结算备注', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
['key' => 'payment_revenue_note', 'label' => '结算备注文案', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>', 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'pay_income_desc_details', 'label' => '收益明细文案', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'pay_income_desc_page', 'label' => '收益说明页', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'cash', 'title' => '现金收益分成', 'fields' => [
|
||||
['key' => 'payment_revenue_cash_ratio_user', 'label' => '普通用户分成比例(%)', 'type' => 'number', 'default' => 60, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
['key' => 'payment_revenue_cash_ratio_vip_1', 'label' => '一级会员分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
['key' => 'payment_revenue_cash_ratio_vip_2', 'label' => '二级会员分成比例(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'points', 'title' => '积分收益分成', 'fields' => [
|
||||
['key' => 'payment_revenue_points_ratio_user', 'label' => '普通用户分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
['key' => 'payment_revenue_points_ratio_vip_1', 'label' => '一级会员分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
['key' => 'payment_revenue_points_ratio_vip_2', 'label' => '二级会员分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
|
||||
]],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -20,7 +20,6 @@ class CreatorRevenue {
|
||||
|
||||
public function init(): void {
|
||||
add_action('aksrt_creator_income_settlement', [$this, 'settle_due']);
|
||||
add_action('aksrt_order_status_changed', [$this, 'handle_order_status_changed'], 10, 3);
|
||||
if (!wp_next_scheduled('aksrt_creator_income_settlement')) {
|
||||
wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'aksrt_creator_income_settlement');
|
||||
}
|
||||
@@ -32,12 +31,6 @@ class CreatorRevenue {
|
||||
}
|
||||
}
|
||||
|
||||
public function handle_order_status_changed(string $order_number, string $new_status, array $order = []): void {
|
||||
if (in_array($new_status, ['cancelled', 'refunding', 'refunded'], true) && class_exists('AKSRT\\Payment\\Revenue')) {
|
||||
Revenue::get_instance()->cancel_or_reverse($order_number, sprintf(__('订单状态变更为%s', 'aksrt'), $new_status));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create creator income after content entitlement has been delivered.
|
||||
*
|
||||
|
||||
+58
-120
@@ -66,7 +66,7 @@ class Revenue {
|
||||
$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);
|
||||
$ratio = $this->resolve_share_ratio($currency, $author_level);
|
||||
if ($ratio <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ class Revenue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle all due pending records in automatic mode.
|
||||
* Settle all pending records whose rolling earning cycle has ended.
|
||||
*
|
||||
* @return int Number of records settled.
|
||||
*/
|
||||
@@ -168,15 +168,15 @@ class Revenue {
|
||||
return 0;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
|
||||
if (($settings['settlement_mode'] ?? 'automatic') !== 'automatic') {
|
||||
return 0;
|
||||
}
|
||||
$cycle_days = max(1, (int) ($settings['cycle_days'] ?? 30));
|
||||
$timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
|
||||
$cutoff = (new \DateTimeImmutable('now', $timezone))
|
||||
->modify('-' . $cycle_days . ' days')
|
||||
->format('Y-m-d H:i:s');
|
||||
|
||||
$limit = max(1, min(500, $limit));
|
||||
$delay_hours = max(0, (int) ($settings['settlement_delay_hours'] ?? 24));
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($delay_hours * HOUR_IN_SECONDS));
|
||||
$ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at <= %s ORDER BY id ASC LIMIT %d",
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at < %s ORDER BY id ASC LIMIT %d",
|
||||
'pending',
|
||||
$cutoff,
|
||||
$limit
|
||||
@@ -191,100 +191,6 @@ class Revenue {
|
||||
return $settled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending record or reverse a settled record for a refunded order.
|
||||
*
|
||||
* @param string|int $order_id Order number or local order id.
|
||||
*/
|
||||
public function cancel_or_reverse($order_id, string $reason = ''): bool {
|
||||
global $wpdb;
|
||||
|
||||
$order_number = sanitize_text_field((string) $order_id);
|
||||
if ($order_number === '') {
|
||||
return false;
|
||||
}
|
||||
$reason = sanitize_textarea_field($reason);
|
||||
$wpdb->query('START TRANSACTION');
|
||||
try {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE order_number = %s FOR UPDATE",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
if (!is_array($rows) || !$rows) {
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if (in_array($status, ['cancelled', 'reversed'], true)) {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'pending') {
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'cancelled',
|
||||
$reason,
|
||||
$this->audit_json('cancelled', $reason),
|
||||
$id,
|
||||
'pending'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_cancel_failed');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!in_array($status, ['settled', 'pending_recovery'], true)) {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'pending_recovery') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$amount = round((float) ($row['amount'] ?? 0), 2);
|
||||
$user_id = (int) ($row['author_id'] ?? 0);
|
||||
$ref = ['type' => 'creator_income_reversal', 'id' => $id];
|
||||
$debited = (string) ($row['currency'] ?? '') === 'points'
|
||||
? (class_exists('\\AKSRT\\Credits\\Credits') ? \AKSRT\Credits\Credits::deduct($user_id, (int) ceil($amount), 'creator_income_reversal', $ref, $reason) : new \WP_Error('credits_unavailable'))
|
||||
: (class_exists('\\AKSRT\\Credits\\Balance') ? \AKSRT\Credits\Balance::deduct($user_id, $amount, 'creator_income_reversal', $ref, $reason) : new \WP_Error('balance_unavailable'));
|
||||
if (is_wp_error($debited)) {
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, reversal_amount = %f, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'pending_recovery',
|
||||
$amount,
|
||||
sanitize_textarea_field($debited->get_error_message()),
|
||||
$this->audit_json('pending_recovery', $debited->get_error_message()),
|
||||
$id,
|
||||
'settled'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_recovery_failed');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, reversal_amount = %f, reversed_at = %s, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'reversed',
|
||||
$amount,
|
||||
current_time('mysql'),
|
||||
$reason,
|
||||
$this->audit_json('reversed', $reason),
|
||||
$id,
|
||||
'settled'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_reverse_failed');
|
||||
}
|
||||
}
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function settle_one(int $revenue_id, int $actor_id): bool {
|
||||
global $wpdb;
|
||||
$wpdb->query('START TRANSACTION');
|
||||
@@ -370,10 +276,10 @@ class Revenue {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$where = ['author_id = %d'];
|
||||
$where = ["author_id = %d", "status IN ('pending', 'settled')"];
|
||||
$params = [$user_id];
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
if (in_array($status, ['pending', 'settled'], true)) {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
@@ -416,15 +322,17 @@ class Revenue {
|
||||
$where .= ' AND currency = %s';
|
||||
$params[] = $currency;
|
||||
}
|
||||
$today = current_time('Y-m-d');
|
||||
$stats_params = array_merge($params, [$today]);
|
||||
$stats_row = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT currency,
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') THEN amount ELSE 0 END), 0) total_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) pending_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'settled' THEN amount ELSE 0 END), 0) settled_amount,
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) today_amount
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') AND DATE(created_at) = %s THEN amount ELSE 0 END), 0) today_amount
|
||||
FROM {$this->table()} WHERE {$where}
|
||||
GROUP BY currency",
|
||||
$params
|
||||
$stats_params
|
||||
), ARRAY_A);
|
||||
foreach ((array) $stats_row as $row) {
|
||||
$key = (string) ($row['currency'] ?? '');
|
||||
@@ -438,11 +346,14 @@ class Revenue {
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if (!in_array($status, ['pending', 'settled'], true)) {
|
||||
$status = '';
|
||||
}
|
||||
$list_args = ['currency' => $currency, 'status' => $status, 'limit' => 20, 'offset' => 0];
|
||||
$page = max(1, min(100, (int) ($args['page'] ?? 1)));
|
||||
$list_args['offset'] = ($page - 1) * 20;
|
||||
$items = $this->get_user_revenues($user_id, $list_args);
|
||||
$count_where = $where . ($status !== '' ? ' AND status = %s' : '');
|
||||
$count_where = $where . " AND status IN ('pending', 'settled')" . ($status !== '' ? ' AND status = %s' : '');
|
||||
$count_params = $params;
|
||||
if ($status !== '') {
|
||||
$count_params[] = $status;
|
||||
@@ -462,9 +373,10 @@ class Revenue {
|
||||
global $wpdb;
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
$where = [];
|
||||
$where = ["status IN ('pending', 'settled')"];
|
||||
$params = [];
|
||||
if (($status = sanitize_key((string) ($args['status'] ?? ''))) !== '') {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if (in_array($status, ['pending', 'settled'], true)) {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
@@ -500,22 +412,48 @@ class Revenue {
|
||||
return ['items' => $items ?: [], 'total' => $total, 'page' => (int) floor($offset / $limit) + 1, 'max_pages' => (int) ceil(max(1, $total) / $limit)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return separate cash and points totals; amounts must never be added across units.
|
||||
*
|
||||
* @return array{cash:array{total:float,pending:float,settled:float,count:int},points:array{total:float,pending:float,settled:float,count:int}}
|
||||
*/
|
||||
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)];
|
||||
$empty = [
|
||||
'cash' => ['total' => 0.0, 'pending' => 0.0, 'settled' => 0.0, 'count' => 0],
|
||||
'points' => ['total' => 0.0, 'pending' => 0.0, 'settled' => 0.0, 'count' => 0],
|
||||
];
|
||||
$where = ["status IN ('pending', 'settled')"];
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'author_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
$stats_sql = "SELECT currency,
|
||||
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 " . implode(' AND ', $where) . " GROUP BY currency";
|
||||
$rows = $wpdb->get_results($params ? $wpdb->prepare($stats_sql, $params) : $stats_sql, ARRAY_A);
|
||||
foreach ((array) $rows as $row) {
|
||||
$key = (string) ($row['currency'] ?? '') === 'points' ? 'points' : 'cash';
|
||||
$empty[$key] = [
|
||||
'total' => (float) ($row['total'] ?? 0),
|
||||
'pending' => (float) ($row['pending'] ?? 0),
|
||||
'settled' => (float) ($row['settled'] ?? 0),
|
||||
'count' => (int) ($row['count'] ?? 0),
|
||||
];
|
||||
}
|
||||
return $empty;
|
||||
}
|
||||
|
||||
private function resolve_share_ratio(int $author_level): float {
|
||||
private function resolve_share_ratio(string $currency, 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));
|
||||
$bucket = $currency === 'points' ? 'points' : 'cash';
|
||||
$level_key = $author_level >= 2 ? 'vip2' : ($author_level === 1 ? 'vip1' : 'ordinary');
|
||||
$ratio = $settings['ratios'][$bucket][$level_key] ?? 0;
|
||||
return max(0, min(100, (float) $ratio));
|
||||
}
|
||||
|
||||
private function decode_order_meta(array $order): array {
|
||||
|
||||
@@ -171,7 +171,7 @@ trait CreditsPaymentGroups {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,share_ratio:int,settlement_threshold:int,cycle_days:int,note:string}
|
||||
* @return array{enabled:bool,cycle_days:int,ratios:array<string,array<string,int>>,note:string}
|
||||
*/
|
||||
public static function get_payment_revenue_settings(): array {
|
||||
return PaymentRuntime::get_payment_revenue_settings();
|
||||
|
||||
@@ -237,31 +237,31 @@ class PaymentRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
* Creator-income policy. Cash and points intentionally use separate ratio sets.
|
||||
*
|
||||
* @return array{enabled:bool,cycle_days:int,ratios:array<string,array<string,int>>,note:string}
|
||||
*/
|
||||
public static function get_payment_revenue_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
$note = trim((string) ($settings['payment_revenue_note'] ?? ''));
|
||||
$mode = sanitize_key((string) ($settings['payment_revenue_settlement_mode'] ?? 'automatic'));
|
||||
if (!in_array($mode, ['automatic', 'manual'], true)) {
|
||||
$mode = 'automatic';
|
||||
}
|
||||
$clamp_ratio = static fn ($value): int => max(0, min(100, (int) $value));
|
||||
|
||||
return [
|
||||
'enabled' => !empty($settings['pay_income_s']),
|
||||
'share_ratio' => max(0, min(100, (int) ($settings['payment_revenue_share_ratio'] ?? 60))),
|
||||
'ratio_vip1' => max(0, min(100, (int) ($settings['ratio_vip_1'] ?? 70))),
|
||||
'ratio_vip2' => max(0, min(100, (int) ($settings['ratio_vip_2'] ?? 80))),
|
||||
'settlement_threshold' => max(0, (int) ($settings['payment_revenue_settlement_threshold'] ?? 100)),
|
||||
'cycle_days' => max(1, min(365, (int) ($settings['payment_revenue_cycle_days'] ?? 30))),
|
||||
'settlement_mode' => $mode,
|
||||
'settlement_delay_hours' => max(0, min(8760, (int) ($settings['payment_revenue_settlement_delay_hours'] ?? 24))),
|
||||
'points_rule' => sanitize_textarea_field((string) ($settings['payment_revenue_points_rule'] ?? '')),
|
||||
'note' => $note !== '' ? $note : __('分成策略和台账结构已就绪,订单入账后自动按比例结算。', 'aksrt'),
|
||||
'display' => [
|
||||
'desc_details' => sanitize_textarea_field((string) ($settings['pay_income_desc_details'] ?? '')),
|
||||
'desc_page' => esc_url_raw((string) ($settings['pay_income_desc_page'] ?? '')),
|
||||
'enabled' => !empty($settings['pay_income_s']),
|
||||
'cycle_days' => max(1, min(365, (int) ($settings['payment_revenue_cycle_days'] ?? 30))),
|
||||
'ratios' => [
|
||||
'cash' => [
|
||||
'ordinary' => $clamp_ratio($settings['payment_revenue_cash_ratio_user'] ?? 60),
|
||||
'vip1' => $clamp_ratio($settings['payment_revenue_cash_ratio_vip_1'] ?? 70),
|
||||
'vip2' => $clamp_ratio($settings['payment_revenue_cash_ratio_vip_2'] ?? 80),
|
||||
],
|
||||
'points' => [
|
||||
'ordinary' => $clamp_ratio($settings['payment_revenue_points_ratio_user'] ?? 0),
|
||||
'vip1' => $clamp_ratio($settings['payment_revenue_points_ratio_vip_1'] ?? 0),
|
||||
'vip2' => $clamp_ratio($settings['payment_revenue_points_ratio_vip_2'] ?? 0),
|
||||
],
|
||||
],
|
||||
'note' => $note !== '' ? wp_kses_post($note) : __('<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+1
-5
@@ -70,12 +70,8 @@ class Center {
|
||||
|
||||
$income_request = [];
|
||||
if ($tab === 'creator') {
|
||||
$currency = sanitize_key((string) $request->get_param('income_currency'));
|
||||
$status = sanitize_key((string) $request->get_param('income_status'));
|
||||
$income_request = [
|
||||
'currency' => in_array($currency, ['', 'CNY', 'points'], true) ? $currency : '',
|
||||
'status' => in_array($status, ['', 'pending', 'settled', 'cancelled', 'reversed', 'pending_recovery'], true) ? $status : '',
|
||||
'page' => max(1, min(100, (int) $request->get_param('income_page'))),
|
||||
'page' => max(1, min(100, (int) $request->get_param('income_page'))),
|
||||
];
|
||||
$context['creator_income_request'] = $income_request;
|
||||
}
|
||||
|
||||
@@ -72,15 +72,25 @@ class CreditsAdmin {
|
||||
}
|
||||
if (class_exists('\AKSRT\Payment\Revenue')) {
|
||||
$stats = \AKSRT\Payment\Revenue::get_instance()->get_stats($user_id);
|
||||
$pending = (float) ($stats['pending'] ?? 0);
|
||||
$total = (float) ($stats['total'] ?? 0);
|
||||
if ($total > 0 || $pending > 0) {
|
||||
$cash = (array) ($stats['cash'] ?? []);
|
||||
$points = (array) ($stats['points'] ?? []);
|
||||
if ((float) ($cash['total'] ?? 0) > 0 || (float) ($cash['pending'] ?? 0) > 0) {
|
||||
$bits[] = sprintf(
|
||||
/* translators: 1 total 2 pending */
|
||||
esc_html__('分成 ¥%1$s(待结 ¥%2$s)', 'aksrt'),
|
||||
number_format($total, 2),
|
||||
number_format($pending, 2)
|
||||
esc_html__('现金分成 ¥%1$s(待结 ¥%2$s)', 'aksrt'),
|
||||
number_format((float) ($cash['total'] ?? 0), 2),
|
||||
number_format((float) ($cash['pending'] ?? 0), 2)
|
||||
);
|
||||
}
|
||||
if ((float) ($points['total'] ?? 0) > 0 || (float) ($points['pending'] ?? 0) > 0) {
|
||||
$bits[] = sprintf(
|
||||
/* translators: 1 total 2 pending */
|
||||
esc_html__('积分分成 %1$s(待结 %2$s)', 'aksrt'),
|
||||
number_format((float) ($points['total'] ?? 0), 0),
|
||||
number_format((float) ($points['pending'] ?? 0), 0)
|
||||
);
|
||||
}
|
||||
if ($bits) {
|
||||
$bits[] = '<a href="' . esc_url(admin_url('admin.php?page=aksrt_income&user_id=' . $user_id)) . '">' . esc_html__('分成明细', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user