__('提现策略', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/payment/withdrawal'), 'icon' => 'dashicons-admin-generic', ]] ); $this->render_flash(); $svc = Withdrawal::get_instance(); $filters = $this->list_filters(); $per_page = AdminListPagination::DEFAULT_PER_PAGE; $page_num = max(1, (int) ($_GET['paged'] ?? 1)); $retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0; $retry_key = isset($_GET['retry_key']) ? sanitize_text_field((string) wp_unslash($_GET['retry_key'])) : ''; // 翻页、状态视图与重置链接共用的筛选参数(不含 paged)。 // retry_id / retry_key 是一次性动作参数,绝不能进翻页链接。 $query_args = array_filter([ 'status' => $filters['status'], 's' => $filters['keyword'], 'source' => $filters['source'], 'method' => $filters['method'], 'user_id' => $filters['user_id'], 'date_from' => $filters['date_from'], 'date_to' => $filters['date_to'], ], static function ($value): bool { return $value !== '' && $value !== 0; }); $stats = $svc->get_stats(); $this->render_process_callout($stats); $this->render_withdrawal_stats($stats); // 状态计数同时给出状态视图的数字与当前视图的总数,查询前就能夹取页码。 $repo_args = array_diff_key($filters, ['status' => true]); $counts = $svc->get_status_counts($repo_args); $total = $filters['status'] !== '' ? (int) ($counts[$filters['status']] ?? 0) : array_sum($counts); $pages = max(1, (int) ceil($total / $per_page)); // 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外:表格空掉 // 且分页条不输出,只能手改 URL 退出。先夹到最后一页再查。 if ($page_num > $pages) { $page_num = $pages; } $items = $svc->get_all($repo_args + [ 'status' => $filters['status'], 'limit' => $per_page, 'offset' => ($page_num - 1) * $per_page, ]); $user_ids = array_values(array_unique(array_filter(array_map('intval', array_column($items, 'user_id'))))); if ($user_ids !== []) { cache_users($user_ids); } $this->render_status_views($filters['status'], $query_args, $counts); $this->render_filter_card($filters, $query_args); NativeUi::table_card_open(__('提现申请', 'aksrt')); echo ''; echo ''; foreach ([ 'number' => esc_html__('订单号', 'aksrt'), 'user' => esc_html__('用户', 'aksrt'), 'amount' => esc_html__('金额', 'aksrt'), 'payee' => esc_html__('收款信息', 'aksrt'), 'status' => esc_html__('状态', 'aksrt'), 'note' => esc_html__('备注', 'aksrt'), 'actions' => esc_html__('操作', 'aksrt'), ] as $key => $label) { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. } echo ''; if (!$items) { $filtered = array_diff_key($query_args, ['status' => true]) !== []; echo NativeUi::empty_row(7, __('暂无提现记录', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ? __('没有符合当前筛选条件的申请,请调整或重置筛选。', 'aksrt') : __('用户在用户中心发起余额或佣金提现后,申请会出现在这里。', 'aksrt')); } foreach ($items as $row) { $this->render_withdrawal_row($row, $retry_id, $retry_key); } echo '
' . esc_html__('提现申请', 'aksrt') . '
' . $label . '
'; NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); NativeUi::card_close(); $this->render_process_dialog(); echo '
'; } private function render_withdrawal_row(array $row, int $retry_id, string $retry_key): void { $id = (int) ($row['id'] ?? 0); $user_id = (int) ($row['user_id'] ?? 0); $status = (string) ($row['status'] ?? ''); $number = (string) ($row['withdrawal_number'] ?? ''); $gross = (string) ($row['gross_amount'] ?? '0.00'); $net = (string) ($row['amount'] ?? '0.00'); $source = (string) ($row['source'] ?? Withdrawal::SOURCE_BALANCE); $method = (string) ($row['method'] ?? ''); $account = (string) ($row['display_account'] ?? ''); $note = trim((string) ($row['note'] ?? '')); $source_label = $this->source_labels()[$source] ?? $source; $method_label = $this->method_labels()[$method] ?? ($method !== '' ? $method : '—'); $user = $user_id > 0 ? get_userdata($user_id) : false; echo ''; echo '' . esc_html($number !== '' ? $number : '#' . $id) . '' . '
' . esc_html(sprintf(__('%s 申请', 'aksrt'), (string) ($row['created_at'] ?? ''))) . '
'; echo '' . NativeUi::user_cell($user_id) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped . '
' . esc_html(sprintf(__('来源:%s', 'aksrt'), $source_label)) . '
'; echo '' . esc_html($this->format_money($net)) . '' . '
' . esc_html(sprintf( /* translators: 1 gross 2 fee 3 fee percent */ __('申请 %1$s · 手续费 %2$s(%3$d%%)', 'aksrt'), $this->format_money($gross), $this->format_money($this->fee_decimal($gross, $net)), (int) ($row['fee_rate'] ?? 0) )) . '
'; echo '' . esc_html($method_label) . ''; if ($account !== '') { echo ''; } echo ''; $processed = (string) ($row['processed_at'] ?? ''); echo '' . NativeUi::status_badge($status, $this->status_badge_map()) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped . ($processed !== '' ? '
' . esc_html(sprintf(__('%s 处理', 'aksrt'), $processed)) . '
' : '') . ''; echo '' . ($note !== '' ? '
' . nl2br(esc_html($note)) . '
' : '—') . ''; echo ''; $next = $this->next_statuses($status); if ($next === []) { echo '—'; return; } $data = [ 'id' => (string) $id, 'nonce' => wp_create_nonce('aksrt_pay_update_withdrawal_' . $id), 'key' => $this->request_key_for($id, $retry_id, $retry_key), 'number' => $number !== '' ? $number : '#' . $id, 'user' => $user ? $user->display_name . ' #' . $user_id : '#' . $user_id, 'net' => $this->format_money($net), 'gross' => $this->format_money($gross), 'source' => $source_label, 'payee' => $method_label . ($account !== '' ? ' · ' . $account : ''), ]; $attrs = ''; foreach ($data as $key => $value) { $attrs .= ' data-' . $key . '="' . esc_attr($value) . '"'; } $buttons = [ Withdrawal::STATUS_PROCESSING => [__('开始处理', 'aksrt'), 'button-primary'], Withdrawal::STATUS_COMPLETED => [__('确认打款', 'aksrt'), 'button-primary'], Withdrawal::STATUS_REJECTED => [__('拒绝', 'aksrt'), 'is-danger'], ]; echo '
'; foreach ($next as $target) { [$label, $class] = $buttons[$target]; echo ''; } echo '
'; } /** 处理流程 + 当前提现策略;有待办时用警示色提醒冻结金额。 */ private function render_process_callout(array $stats): void { $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_withdrawal_settings() : []; $open = (int) ($stats['pending'] ?? 0) + (int) ($stats['processing'] ?? 0); $policy = sprintf( /* translators: 1 min amount 2 fee percent 3 daily count 4 daily limit */ __('当前策略:最低 %1$s,手续费 %2$d%%,每日 %3$d 次 / %4$s。', 'aksrt'), $this->format_money((string) ($settings['min_amount'] ?? '0.00')), (int) ($settings['service_charge_rate'] ?? 0), (int) ($settings['daily_count'] ?? 0), $this->format_money((string) ($settings['daily_limit'] ?? '0.00')) ); $flow = __('处理流程:开始处理 → 按收款信息线下打款 → 确认打款。确认打款与拒绝都不可撤销。', 'aksrt'); echo '
'; if ($open > 0) { echo '' . esc_html(sprintf( /* translators: 1 open count 2 frozen amount */ __('%1$d 笔申请待办,冻结中 %2$s。', 'aksrt'), $open, $this->format_money((string) ($stats['open_gross'] ?? '0.00')) )) . ' '; } echo esc_html($flow . ' ' . $policy) . '
'; } private function render_withdrawal_stats(array $stats): void { NativeUi::stats([ ['label' => __('待处理', 'aksrt'), 'value' => number_format_i18n((int) ($stats['pending'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PENDING]), 'icon' => 'dashicons-clock', 'tone' => 'warning'], ['label' => __('处理中', 'aksrt'), 'value' => number_format_i18n((int) ($stats['processing'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_PROCESSING]), 'icon' => 'dashicons-update', 'tone' => 'info'], ['label' => __('冻结中金额', 'aksrt'), 'value' => $this->format_money((string) ($stats['open_gross'] ?? '0.00')), 'hint' => __('待处理 + 处理中的申请毛额', 'aksrt'), 'icon' => 'dashicons-lock', 'tone' => 'warning'], ['label' => __('已打款', 'aksrt'), 'value' => $this->format_money((string) ($stats['paid_net'] ?? '0.00')), 'url' => $this->page_url(['status' => Withdrawal::STATUS_COMPLETED]), 'hint' => sprintf( /* translators: 1 completed count 2 fee total */ __('%1$s 笔 · 手续费 %2$s', 'aksrt'), number_format_i18n((int) ($stats['completed'] ?? 0)), $this->format_money((string) ($stats['paid_fee'] ?? '0.00')) ), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], ['label' => __('已拒绝', 'aksrt'), 'value' => number_format_i18n((int) ($stats['rejected'] ?? 0)), 'url' => $this->page_url(['status' => Withdrawal::STATUS_REJECTED]), 'icon' => 'dashicons-no-alt', 'tone' => 'danger'], ]); } /** @param array $counts */ private function render_status_views(string $current, array $query_args, array $counts): void { $base = array_diff_key($query_args, ['status' => true]); $views = ['' => [__('全部', 'aksrt'), array_sum($counts)]]; foreach ($this->status_labels() as $key => $label) { $views[$key] = [$label, (int) ($counts[$key] ?? 0)]; } echo ''; } /** * @param array $filters * @param array $query_args */ private function render_filter_card(array $filters, array $query_args): void { NativeUi::card_open('', '', false, 'aksrt-wd-filter-card'); echo ''; NativeUi::card_close(); } /** @param array $options */ private function filter_select(string $name, string $label, array $options, string $current): string { $html = ''; } /** * 共享处理弹窗:行内按钮把记录摘要、nonce 与幂等键写进来(admin-withdrawals.js), * 三种目标状态各自的说明与备注要求用 data-mode 区分。 */ private function render_process_dialog(): void { echo ''; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; wp_referer_field(); $titles = [ Withdrawal::STATUS_PROCESSING => __('开始处理提现', 'aksrt'), Withdrawal::STATUS_COMPLETED => __('确认已打款', 'aksrt'), Withdrawal::STATUS_REJECTED => __('拒绝提现', 'aksrt'), ]; echo '

'; foreach ($titles as $mode => $title) { echo ''; } echo '

'; echo '
'; foreach ([ 'number' => __('订单号', 'aksrt'), 'user' => __('用户', 'aksrt'), 'net' => __('应打款', 'aksrt'), 'gross' => __('申请金额', 'aksrt'), 'source' => __('来源', 'aksrt'), 'payee' => __('收款信息', 'aksrt'), ] as $field => $label) { echo '
' . esc_html($label) . '
'; } echo '
'; $hints = [ Withdrawal::STATUS_PROCESSING => [__('标记为处理中后,请按上方收款信息线下打款,完成后回到这里确认打款。', 'aksrt'), ''], Withdrawal::STATUS_COMPLETED => [__('请确认已向用户实际打出「应打款」金额。确认后状态不可更改,用户会收到完成通知。', 'aksrt'), 'is-warning'], Withdrawal::STATUS_REJECTED => [__('拒绝后,冻结的申请金额会退回用户的原资金池,拒绝原因会通知用户。此操作不可撤销。', 'aksrt'), 'is-danger'], ]; foreach ($hints as $mode => [$hint, $tone]) { echo ''; } echo ''; echo '
'; $submits = [ Withdrawal::STATUS_PROCESSING => [__('开始处理', 'aksrt'), 'button-primary'], Withdrawal::STATUS_COMPLETED => [__('确认已打款', 'aksrt'), 'button-primary'], Withdrawal::STATUS_REJECTED => [__('确认拒绝', 'aksrt'), 'button-primary is-danger'], ]; echo '
'; } /** * 从请求读取并校验筛选条件;非法值一律视为未筛选。 * * @return array{status:string,keyword:string,source:string,method:string,user_id:int,date_from:string,date_to:string} */ private function list_filters(): array { $read = static function (string $key): string { return isset($_GET[$key]) ? sanitize_text_field((string) wp_unslash($_GET[$key])) : ''; }; $date = static function (string $value): string { return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : ''; }; $status = sanitize_key($read('status')); $source = sanitize_key($read('source')); $method = sanitize_key($read('method')); return [ 'status' => isset($this->status_labels()[$status]) ? $status : '', 'keyword' => trim($read('s')), 'source' => isset($this->source_labels()[$source]) ? $source : '', 'method' => isset($this->method_labels()[$method]) ? $method : '', 'user_id' => max(0, (int) $read('user_id')), 'date_from' => $date($read('date_from')), 'date_to' => $date($read('date_to')), ]; } }