feat(payment): complete creator income center

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 10:59:58 +08:00
co-authored by Claude Fable 5
parent 450b9f44b9
commit 9e6ae32fc1
16 changed files with 550 additions and 134 deletions
+52 -6
View File
@@ -26,6 +26,7 @@ class PayIncome {
return;
}
add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']);
add_action('admin_post_aksrt_pay_batch_settle_income', [$this, 'handle_batch_settle_income']);
}
public function render_income_page(): void {
@@ -40,6 +41,11 @@ class PayIncome {
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
$post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0;
$currency = isset($_GET['currency']) ? sanitize_key((string) wp_unslash($_GET['currency'])) : '';
$order_number = isset($_GET['order_number']) ? sanitize_text_field((string) wp_unslash($_GET['order_number'])) : '';
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
$per_page = 20;
$svc = \AKSRT\Payment\Revenue::get_instance();
@@ -53,14 +59,24 @@ class PayIncome {
number_format((float) ($stats['settled'] ?? 0), 2)
)) . '</p>';
echo '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0;">';
echo '<input type="hidden" name="action" value="aksrt_pay_batch_settle_income" />';
wp_nonce_field('aksrt_pay_batch_settle_income');
submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false);
echo '</form>';
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')] as $key => $label) {
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), 'reversed' => __('已冲正', 'aksrt'), 'pending_recovery' => __('待追缴', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('用户 ID', 'aksrt') . '" class="small-text" /> ';
echo '<input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" placeholder="' . esc_attr__('文章 ID', 'aksrt') . '" class="small-text" /> ';
echo '<select name="currency"><option value="">' . esc_html__('全部货币', 'aksrt') . '</option><option value="CNY"' . selected($currency, 'CNY', false) . '>CNY</option><option value="points"' . selected($currency, 'points', false) . '>' . esc_html__('积分', 'aksrt') . '</option></select> ';
echo '<input type="search" name="order_number" value="' . esc_attr($order_number) . '" placeholder="' . esc_attr__('订单号', 'aksrt') . '" /> ';
echo '<input type="date" name="date_from" value="' . esc_attr($date_from) . '" /> — <input type="date" name="date_to" value="' . esc_attr($date_to) . '" /> ';
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
if ($user_id > 0) {
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_income')) . '">' . esc_html__('清除用户过滤', 'aksrt') . '</a>';
@@ -71,6 +87,11 @@ class PayIncome {
'status' => $status,
'limit' => $per_page,
'offset' => ($page_num - 1) * $per_page,
'post_id' => $post_id,
'currency' => $currency,
'order_number' => $order_number,
'date_from' => $date_from,
'date_to' => $date_to,
];
if ($user_id > 0) {
$list_args['user_id'] = $user_id;
@@ -81,10 +102,10 @@ class PayIncome {
$pages = max(1, (int) ($result['max_pages'] ?? 1));
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '<th><input type="checkbox" form="aksrt-pay-batch-settle-income" aria-label="' . esc_attr__('全选待结算记录', 'aksrt') . '" /></th><th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="7">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
echo '<tr><td colspan="8">' . esc_html__('暂无分成记录', 'aksrt') . '</td></tr>';
}
foreach ($items as $row) {
$id = (int) ($row['id'] ?? 0);
@@ -92,8 +113,8 @@ class PayIncome {
$user = $user_id > 0 ? get_userdata($user_id) : null;
$st = (string) ($row['status'] ?? '');
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
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>';
echo '<td>' . esc_html($st) . '</td>';
@@ -122,13 +143,37 @@ class PayIncome {
}
$id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
check_admin_referer('aksrt_pay_settle_income_' . $id);
$ok = $id > 0 && class_exists('\AKSRT\Payment\Revenue') && \AKSRT\Payment\Revenue::get_instance()->settle($id);
$ok = $id > 0 && class_exists('\AKSRT\Payment\Revenue') && \AKSRT\Payment\Revenue::get_instance()->settle($id, get_current_user_id());
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']));
exit;
}
public function handle_batch_settle_income(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
check_admin_referer('aksrt_pay_batch_settle_income');
$raw_ids = isset($_POST['revenue_ids']) ? wp_unslash($_POST['revenue_ids']) : [];
$ids = [];
if (is_array($raw_ids)) {
foreach ($raw_ids as $raw_id) {
$id = (int) $raw_id;
if ($id > 0) {
$ids[] = $id;
}
}
}
$ids = array_values(array_unique($ids));
$ok = !empty($ids)
&& class_exists('\\AKSRT\\Payment\\Revenue')
&& \AKSRT\Payment\Revenue::get_instance()->settle($ids, get_current_user_id());
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $ok ? 'income_batch_settled' : 'income_failed']));
exit;
}
private function ensure_cap(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
@@ -152,6 +197,7 @@ class PayIncome {
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'income_settled' => __('分成已结算。', 'aksrt'),
'income_batch_settled' => __('选中分成已批量结算。', 'aksrt'),
'income_failed' => __('分成结算失败。', 'aksrt'),
];
if (!isset($map[$code])) {
+69 -2
View File
@@ -38,7 +38,22 @@ class PaymentController extends ControllerBase {
],
]);
// Withdrawal list — unified pagination: page + per_page
register_rest_route(self::API_NAMESPACE, '/payment/creator-income', [
'methods' => 'GET',
'callback' => [$this, 'get_creator_income'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/payment/creator-income/(?P<id>\d+)/settle', [
'methods' => 'POST',
'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',
'callback' => [$this, 'get_withdrawals'],
@@ -224,8 +239,60 @@ class PaymentController extends ControllerBase {
}
/**
* GET /payment/withdrawals — 提现列表,统一 page + per_page 分页
* GET /payment/creator-income — creator income operations list.
*/
public function get_creator_income(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Revenue')) {
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
$result = \AKSRT\Payment\Revenue::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => sanitize_key((string) $request->get_param('status')),
'user_id' => max(0, (int) $request->get_param('user_id')),
'post_id' => max(0, (int) $request->get_param('post_id')),
'currency' => sanitize_key((string) $request->get_param('currency')),
'order_number' => sanitize_text_field((string) $request->get_param('order_number')),
'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
]);
$result['page'] = $page;
$result['per_page'] = $per_page;
return $this->success(['data' => $result]);
}
/**
* POST /payment/creator-income/{id}/settle — settle one income record.
*/
public function settle_creator_income(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Revenue')) {
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
}
$id = (int) $request->get_param('id');
if ($id <= 0 || !\AKSRT\Payment\Revenue::get_instance()->settle($id, get_current_user_id())) {
return $this->error(__('结算失败', 'aksrt'));
}
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' => []]]);
+2
View File
@@ -174,6 +174,8 @@ class SchemaPayment {
['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' => ''],
]],