register_section_routes('payment', 'payment'); register_rest_route(self::API_NAMESPACE, '/payment/overview', [ 'methods' => 'GET', 'callback' => [$this, 'rest_payment_overview'], 'permission_callback' => [$this, 'check_permission'], ]); // Revenue stats (with optional time-series) register_rest_route(self::API_NAMESPACE, '/payment/revenue', [ 'methods' => 'GET', 'callback' => [$this, 'get_revenue'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'date_from' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'date_to' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'granularity' => ['default' => 'day', 'sanitize_callback' => 'sanitize_text_field'], 'time_series' => ['default' => false, 'sanitize_callback' => 'rest_sanitize_boolean'], ], ]); // Withdrawal list — unified pagination: page + per_page register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [ 'methods' => 'GET', 'callback' => [$this, 'get_withdrawals'], 'permission_callback' => [$this, 'check_permission'], ]); // Withdrawal status update register_rest_route(self::API_NAMESPACE, '/payment/withdrawals/(?P\d+)', [ 'methods' => 'POST', 'callback' => [$this, 'update_withdrawal_status'], 'permission_callback' => [$this, 'check_permission'], ]); // ── Charge Card ── // Charge card list register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [ 'methods' => 'GET', 'callback' => [$this, 'get_charge_cards'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'get_charge_card_detail'], 'permission_callback' => [$this, 'check_permission'], ]); // Charge card create register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [ 'methods' => 'POST', 'callback' => [$this, 'create_charge_card'], 'permission_callback' => [$this, 'check_permission'], ]); // Charge card batch create register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/batch', [ 'methods' => 'POST', 'callback' => [$this, 'batch_create_charge_cards'], 'permission_callback' => [$this, 'check_permission'], ]); // Charge card disable register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)/disable', [ 'methods' => 'POST', 'callback' => [$this, 'disable_charge_card'], 'permission_callback' => [$this, 'check_permission'], ]); // ── Rebate ── // Rebate list register_rest_route(self::API_NAMESPACE, '/payment/rebate/log', [ 'methods' => 'GET', 'callback' => [$this, 'get_rebate_list'], 'permission_callback' => [$this, 'check_permission'], ]); register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'get_rebate_detail'], 'permission_callback' => [$this, 'check_permission'], ]); // Rebate settle register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/settle', [ 'methods' => 'POST', 'callback' => [$this, 'settle_rebate'], 'permission_callback' => [$this, 'check_permission'], ]); // Rebate cancel register_rest_route(self::API_NAMESPACE, '/payment/rebate/(?P\d+)/cancel', [ 'methods' => 'POST', 'callback' => [$this, 'cancel_rebate'], 'permission_callback' => [$this, 'check_permission'], ]); } public function rest_payment_overview(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Settings')) { return $this->error('设置模块未初始化', 500); } $paid = AKSRT_Settings::get_paid_content_settings(); $recharge = AKSRT_Settings::get_payment_recharge_settings(); $vip = AKSRT_Settings::get_payment_vip_settings(); $revenue = AKSRT_Settings::get_payment_revenue_settings(); $affiliate = AKSRT_Settings::get_payment_affiliate_settings(); $gateway = AKSRT_Settings::get_payment_gateway_settings(); return $this->success([ 'data' => [ 'paid' => $paid, 'recharge' => $recharge, 'vip' => $vip, 'revenue' => $revenue, 'affiliate' => $affiliate, 'gateway' => $gateway, 'currency_name' => AKSRT_Settings::get_currency_name(), ], ]); } /** * 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]; if (class_exists('AKSRT_Revenue')) { $revenue_stats = AKSRT_Revenue::get_instance()->get_stats(); } $withdrawal_stats = ['total' => 0, 'pending' => 0, 'completed' => 0, 'total_paid' => 0]; if (class_exists('AKSRT_Withdrawal')) { $withdrawal_stats = AKSRT_Withdrawal::get_instance()->get_stats(); } $response = [ 'revenue' => $revenue_stats, 'withdrawal' => $withdrawal_stats, ]; // Time-series data if ($request->get_param('time_series')) { $response['time_series'] = $this->get_revenue_time_series($request); } return $this->success(['data' => $response]); } /** * Build revenue time-series data. */ private function get_revenue_time_series(WP_REST_Request $request): array { global $wpdb; $prefix = $wpdb->prefix; $date_from = sanitize_text_field((string) $request->get_param('date_from')); $date_to = sanitize_text_field((string) $request->get_param('date_to')); $granularity = sanitize_text_field((string) $request->get_param('granularity')); // Default: last 30 days if (!$date_from) { $date_from = gmdate('Y-m-d', strtotime('-30 days')); } if (!$date_to) { $date_to = gmdate('Y-m-d'); } // Granularity: day, week, month $date_format = '%Y-%m-%d'; if ($granularity === 'week') { $date_format = '%x-W%v'; } elseif ($granularity === 'month') { $date_format = '%Y-%m'; } $table = $prefix . 'aksrt_revenue'; $results = $wpdb->get_results($wpdb->prepare( "SELECT 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 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, ]; } } return $series; } /** * GET /payment/withdrawals — 提现列表,统一 page + per_page 分页 */ public function get_withdrawals(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Withdrawal')) { return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1, 'stats' => []]]); } $status = sanitize_key((string) $request->get_param('status')); $page = max(1, (int) $request->get_param('page')); $per_page = max(1, min(50, (int) $request->get_param('per_page'))) ?: 20; $items = AKSRT_Withdrawal::get_instance()->get_all([ 'status' => $status, 'limit' => $per_page, 'offset' => ($page - 1) * $per_page, ]); $total = AKSRT_Withdrawal::get_instance()->get_count(['status' => $status]); $stats = AKSRT_Withdrawal::get_instance()->get_stats(); return $this->success([ 'data' => [ 'items' => $items, 'total' => $total, 'page' => $page, 'total_pages' => max(1, (int) ceil($total / $per_page)), 'stats' => $stats, ], ]); } /** * POST /payment/withdrawals/{id} — 更新提现状态 */ public function update_withdrawal_status(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Withdrawal')) { return $this->error('提现模块未初始化', 500); } $id = (int) $request->get_param('id'); $params = $this->get_json_params($request); $new_status = sanitize_key((string) ($params['status'] ?? '')); $note = sanitize_textarea_field((string) ($params['note'] ?? '')); $valid_statuses = ['pending', 'approved', 'processing', 'completed', 'rejected']; if (!in_array($new_status, $valid_statuses, true)) { return $this->error('无效的提现状态'); } if ($id <= 0) { return $this->error('无效的提现 ID'); } $success = AKSRT_Withdrawal::get_instance()->update_status($id, $new_status, $note); if ($success) { return $this->success(['message' => __('提现状态已更新', 'aksrt')]); } return $this->error(__('更新失败', 'aksrt')); } /* ------------------------------------------------------------------ * Charge Card * ----------------------------------------------------------------*/ /** * GET /payment/charge-cards — 充值卡列表 */ public function get_charge_cards(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Charge_Card')) { return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]); } global $wpdb; $table = AKSRT_DB_Migrator::table('charge_card'); $page = max(1, (int) ($request->get_param('page') ?: 1)); $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20))); $status = sanitize_key((string) $request->get_param('status')); $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); $date_from = sanitize_text_field((string) $request->get_param('date_from')); $date_to = sanitize_text_field((string) $request->get_param('date_to')); $where = []; $params = []; if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } if ($date_from !== '') { $where[] = 'created_at >= %s'; $params[] = $date_from . ' 00:00:00'; } if ($date_to !== '') { $where[] = 'created_at <= %s'; $params[] = $date_to . ' 23:59:59'; } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500"; $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A); $items = []; foreach ((array) $rows as $row) { $item = $this->build_charge_card_admin_item($row); if ($keyword !== '' && mb_stripos(implode(' ', array_filter([ (string) ($item['code'] ?? ''), (string) ($item['note'] ?? ''), (string) ($item['used_by_user']['display_name'] ?? ''), (string) ($item['used_by_user']['email'] ?? ''), ])), $keyword) === false) { continue; } $items[] = $item; } $total = count($items); $offset = ($page - 1) * $per_page; $stats = AKSRT_Charge_Card::get_instance()->get_stats(); return $this->success([ 'data' => [ 'items' => array_values(array_slice($items, $offset, $per_page)), 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)), 'stats' => $stats, 'options' => [ 'statuses' => [ ['label' => __('全部状态', 'aksrt'), 'value' => ''], ['label' => __('未使用', 'aksrt'), 'value' => 'active'], ['label' => __('已使用', 'aksrt'), 'value' => 'used'], ['label' => __('已过期', 'aksrt'), 'value' => 'expired'], ['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'], ], ], ], ]); } /** * POST /payment/charge-cards — 创建充值卡 */ public function create_charge_card(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Charge_Card')) { return $this->error('充值卡系统未启用', 500); } $params = $this->get_json_params($request); $code = sanitize_text_field((string) ($params['code'] ?? '')); $amount = (float) ($params['amount'] ?? 0); if ($code === '' || $amount <= 0) { return $this->error('卡号和金额不能为空'); } $id = AKSRT_Charge_Card::get_instance()->create($params); if ($id > 0) { return $this->success([ 'message' => __('充值卡已创建', 'aksrt'), 'data' => ['id' => $id], ], 201); } return $this->error(__('创建失败(可能卡号已存在)', 'aksrt')); } /** * POST /payment/charge-cards/batch — 批量生成充值卡 */ public function batch_create_charge_cards(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Charge_Card')) { return $this->error('充值卡系统未启用', 500); } $params = $this->get_json_params($request); $amount = (float) ($params['amount'] ?? 0); $count = max(1, min(100, (int) ($params['count'] ?? 1))); $length = max(8, min(32, (int) ($params['length'] ?? 16))); $prefix = sanitize_text_field((string) ($params['prefix'] ?? '')); $note = sanitize_textarea_field((string) ($params['note'] ?? '')); $expires = sanitize_text_field((string) ($params['expires_at'] ?? '')); if ($amount <= 0) { return $this->error('金额必须大于 0'); } $service = AKSRT_Charge_Card::get_instance(); $codes = []; for ($i = 0; $i < $count; $i++) { $code = $prefix . strtoupper(wp_generate_password($length, false)); $id = $service->create([ 'code' => $code, 'amount' => $amount, 'expires_at' => $expires, 'note' => $note, ]); if ($id > 0) { $codes[] = $code; } } return $this->success([ 'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)), 'data' => ['codes' => $codes, 'count' => count($codes)], ]); } /** * POST /payment/charge-cards/{id}/disable — 禁用充值卡 */ public function disable_charge_card(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Charge_Card')) { return $this->error('充值卡系统未启用', 500); } $id = (int) $request->get_param('id'); if ($id <= 0) { return $this->error('参数无效'); } $success = AKSRT_Charge_Card::get_instance()->disable($id); if ($success) { return $this->success(['message' => __('充值卡已禁用', 'aksrt')]); } return $this->error(__('操作失败', 'aksrt')); } public function get_charge_card_detail(WP_REST_Request $request): WP_REST_Response { global $wpdb; $id = (int) $request->get_param('id'); $table = AKSRT_DB_Migrator::table('charge_card'); if ($id <= 0 || !$table) { return $this->error(__('充值卡不存在', 'aksrt'), 404); } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A); if (!$row) { return $this->error(__('充值卡不存在', 'aksrt'), 404); } return $this->success([ 'data' => [ 'card' => $this->build_charge_card_admin_item($row), ], ]); } /* ------------------------------------------------------------------ * Rebate * ----------------------------------------------------------------*/ /** * GET /payment/rebate/log — 返佣记录列表 */ public function get_rebate_list(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Rebate')) { return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]); } global $wpdb; $table = AKSRT_DB_Migrator::table('rebate_log'); $page = max(1, (int) ($request->get_param('page') ?: 1)); $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20))); $status = sanitize_key((string) $request->get_param('status')); $type = sanitize_key((string) $request->get_param('type')); $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); $user_id = (int) $request->get_param('user_id'); $date_from = sanitize_text_field((string) $request->get_param('date_from')); $date_to = sanitize_text_field((string) $request->get_param('date_to')); $where = []; $params = []; if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } if ($type !== '') { $where[] = 'type = %s'; $params[] = $type; } if ($user_id > 0) { $where[] = 'user_id = %d'; $params[] = $user_id; } if ($date_from !== '') { $where[] = 'created_at >= %s'; $params[] = $date_from . ' 00:00:00'; } if ($date_to !== '') { $where[] = 'created_at <= %s'; $params[] = $date_to . ' 23:59:59'; } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500"; $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A); $items = []; foreach ((array) $rows as $row) { $item = $this->build_rebate_admin_item($row); if ($keyword !== '' && mb_stripos(implode(' ', array_filter([ (string) ($item['order_number'] ?? ''), (string) ($item['note'] ?? ''), (string) ($item['user']['display_name'] ?? ''), (string) ($item['source_user']['display_name'] ?? ''), ])), $keyword) === false) { continue; } $items[] = $item; } $total = count($items); $offset = ($page - 1) * $per_page; $stats = AKSRT_Rebate::get_instance()->get_stats(); return $this->success([ 'data' => [ 'items' => array_values(array_slice($items, $offset, $per_page)), 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)), 'stats' => $stats, 'options' => [ 'statuses' => [ ['label' => __('全部状态', 'aksrt'), 'value' => ''], ['label' => __('待结算', 'aksrt'), 'value' => 'pending'], ['label' => __('已结算', 'aksrt'), 'value' => 'settled'], ['label' => __('已取消', 'aksrt'), 'value' => 'cancelled'], ], 'types' => [ ['label' => __('全部类型', 'aksrt'), 'value' => ''], ['label' => __('订单返佣', 'aksrt'), 'value' => 'order'], ['label' => __('邀请返佣', 'aksrt'), 'value' => 'invite'], ], ], ], ]); } /** * POST /payment/rebate/{id}/settle — 结算返佣 */ public function settle_rebate(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Rebate')) { return $this->error('返佣系统未启用', 500); } $id = (int) $request->get_param('id'); $params = $this->get_json_params($request); $note = sanitize_textarea_field((string) ($params['note'] ?? '')); if ($id <= 0) { return $this->error('参数无效'); } $success = AKSRT_Rebate::get_instance()->settle($id); if ($success) { $this->append_rebate_process_log($id, 'settled', $note); return $this->success(['message' => __('返佣已结算', 'aksrt')]); } return $this->error(__('结算失败', 'aksrt')); } /** * POST /payment/rebate/{id}/cancel — 取消返佣 */ public function cancel_rebate(WP_REST_Request $request): WP_REST_Response { if (!class_exists('AKSRT_Rebate')) { return $this->error('返佣系统未启用', 500); } $id = (int) $request->get_param('id'); $params = $this->get_json_params($request); $note = sanitize_textarea_field((string) ($params['note'] ?? '')); if ($id <= 0) { return $this->error('参数无效'); } $success = AKSRT_Rebate::get_instance()->cancel($id); if ($success) { $this->append_rebate_process_log($id, 'cancelled', $note); return $this->success(['message' => __('返佣已取消', 'aksrt')]); } return $this->error(__('操作失败', 'aksrt')); } public function get_rebate_detail(WP_REST_Request $request): WP_REST_Response { global $wpdb; $id = (int) $request->get_param('id'); $table = AKSRT_DB_Migrator::table('rebate_log'); if ($id <= 0 || !$table) { return $this->error(__('返佣记录不存在', 'aksrt'), 404); } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A); if (!$row) { return $this->error(__('返佣记录不存在', 'aksrt'), 404); } return $this->success([ 'data' => [ 'record' => $this->build_rebate_admin_item($row), 'history' => $this->get_rebate_process_log($id), ], ]); } private function build_charge_card_admin_item(array $row): array { $used_by = (int) ($row['used_by'] ?? 0); $user = $used_by > 0 ? get_userdata($used_by) : null; $status = sanitize_key((string) ($row['status'] ?? 'active')); $statusMap = [ 'active' => __('未使用', 'aksrt'), 'used' => __('已使用', 'aksrt'), 'expired' => __('已过期', 'aksrt'), 'disabled' => __('已禁用', 'aksrt'), ]; return [ 'id' => (int) ($row['id'] ?? 0), 'code' => (string) ($row['code'] ?? ''), 'amount' => (float) ($row['amount'] ?? 0), 'balance' => (float) ($row['balance'] ?? 0), 'status' => $status, 'status_label' => $statusMap[$status] ?? $status, 'used_by' => $used_by, 'used_at' => (string) ($row['used_at'] ?? ''), 'expires_at' => (string) ($row['expires_at'] ?? ''), 'created_at' => (string) ($row['created_at'] ?? ''), 'note' => (string) ($row['note'] ?? ''), 'used_by_user' => $user instanceof WP_User ? [ 'id' => (int) $user->ID, 'display_name' => $user->display_name, 'email' => $user->user_email, ] : null, ]; } private function build_rebate_admin_item(array $row): array { $user_id = (int) ($row['user_id'] ?? 0); $source_user_id = (int) ($row['source_user_id'] ?? 0); $user = $user_id > 0 ? get_userdata($user_id) : null; $source_user = $source_user_id > 0 ? get_userdata($source_user_id) : null; $status = sanitize_key((string) ($row['status'] ?? 'pending')); $type = sanitize_key((string) ($row['type'] ?? 'order')); $statusMap = [ 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), ]; $typeMap = [ 'order' => __('订单返佣', 'aksrt'), 'invite' => __('邀请返佣', 'aksrt'), ]; return [ 'id' => (int) ($row['id'] ?? 0), 'order_number' => (string) ($row['order_number'] ?? ''), 'amount' => (float) ($row['amount'] ?? 0), 'status' => $status, 'status_label' => $statusMap[$status] ?? $status, 'type' => $type, 'type_label' => $typeMap[$type] ?? $type, 'created_at' => (string) ($row['created_at'] ?? ''), 'settled_at' => (string) ($row['settled_at'] ?? ''), 'note' => (string) ($row['note'] ?? ''), 'history_count' => count($this->get_rebate_process_log((int) ($row['id'] ?? 0))), 'user' => $user instanceof WP_User ? [ 'id' => (int) $user->ID, 'display_name' => $user->display_name, 'email' => $user->user_email, ] : null, 'source_user' => $source_user instanceof WP_User ? [ 'id' => (int) $source_user->ID, 'display_name' => $source_user->display_name, 'email' => $source_user->user_email, ] : null, ]; } private function append_rebate_process_log(int $rebate_id, string $event, string $note = ''): void { $log = get_option('aksrt_rebate_process_log', []); if (!is_array($log)) { $log = []; } $rebate_id = (int) $rebate_id; if (!isset($log[$rebate_id]) || !is_array($log[$rebate_id])) { $log[$rebate_id] = []; } $log[$rebate_id][] = [ 'event' => sanitize_key($event), 'note' => sanitize_textarea_field($note), 'operator' => get_current_user_id(), 'operator_name' => ($user = wp_get_current_user()) ? $user->display_name : '', 'time' => current_time('mysql'), ]; update_option('aksrt_rebate_process_log', $log, false); } private function get_rebate_process_log(int $rebate_id): array { $log = get_option('aksrt_rebate_process_log', []); if (!is_array($log) || !isset($log[$rebate_id]) || !is_array($log[$rebate_id])) { return []; } return array_values($log[$rebate_id]); } }