__('分成配置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/payment/revenue'), 'icon' => 'dashicons-admin-generic', ]] ); $this->render_flash(); $policy = $this->revenue_policy(); $filters = $this->list_filters(); $query_args = $this->list_query_args($filters); $per_page = AdminListPagination::DEFAULT_PER_PAGE; $page_num = max(1, (int) ($_GET['paged'] ?? 1)); $svc = Revenue::get_instance(); $this->render_policy_callout($policy); $this->render_income_stats($svc->get_stats($filters['user_id']), $filters['user_id']); // 状态计数同时给出状态视图的数字与当前视图的总数,查询前就能夹取页码。 $repo_args = $this->list_repository_args($filters); $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; } $result = $svc->get_list($repo_args + [ 'status' => $filters['status'], 'limit' => $per_page, 'offset' => ($page_num - 1) * $per_page, ]); $items = (array) ($result['items'] ?? []); $this->prime_list_caches($items); $this->render_status_views($filters, $query_args, $counts); $this->render_filter_card($filters); $can_settle = $policy['enabled']; $has_pending = $can_settle && in_array('pending', array_column($items, 'status'), true); NativeUi::table_card_open(__('分成记录', 'aksrt'), $has_pending ? $this->batch_form_html() : ''); echo ''; echo ''; echo ''; foreach ([ 'record' => __('记录 / 产生时间', 'aksrt'), 'author' => __('作者', 'aksrt'), 'source' => __('来源文章 / 订单', 'aksrt'), 'base' => __('订单金额 × 比例', 'aksrt'), 'amount' => __('分成', 'aksrt'), 'status' => __('状态', 'aksrt'), 'actions' => __('操作', 'aksrt'), ] as $key => $label) { echo ''; } echo ''; if (!$items) { $filtered = array_diff_key($query_args, ['status' => true]) !== []; echo NativeUi::empty_row(8, __('暂无分成记录', 'aksrt'), $filtered // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ? __('没有符合当前筛选条件的记录,请调整或重置筛选。', 'aksrt') : __('付费内容订单支付成功后,作者分成会出现在这里。', 'aksrt')); } foreach ($items as $row) { $this->render_income_row($row, $can_settle, $policy['cycle_days']); } echo '
' . esc_html__('分成记录', 'aksrt') . '
'; if ($has_pending) { echo ''; } echo '' . esc_html($label) . '
'; NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); NativeUi::card_close(); } private function render_income_row(array $row, bool $can_settle, int $cycle_days): void { $id = (int) ($row['id'] ?? 0); $status = (string) ($row['status'] ?? ''); $is_points = (string) ($row['currency'] ?? '') === 'points'; $post_id = (int) ($row['post_id'] ?? 0); $buyer_id = (int) ($row['buyer_id'] ?? 0); $order_number = (string) ($row['order_number'] ?? ''); $pending = $status === 'pending'; echo ''; echo ''; if ($pending && $can_settle) { echo ''; } echo ''; echo '#' . esc_html((string) $id) . '' . '
' . esc_html((string) ($row['created_at'] ?? '')) . '
'; echo '' . NativeUi::user_cell((int) ($row['author_id'] ?? 0)) . ''; $title = $post_id > 0 ? (string) get_the_title($post_id) : ''; $post_html = $post_id > 0 ? '' . esc_html($title !== '' ? $title : '#' . $post_id) . '' : '—'; $buyer = $buyer_id > 0 ? get_userdata($buyer_id) : false; $order_url = admin_url('admin.php?page=aksrt_orders&order=' . rawurlencode($order_number)); echo '' . $post_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. . '
' . esc_html($order_number) . '' . ($buyer ? ' · ' . esc_html(sprintf(__('买家 %s', 'aksrt'), $buyer->display_name)) : '') . '
'; echo '' . esc_html($this->format_amount($row['original_amount'] ?? 0, $is_points)) . '
' . esc_html(sprintf( /* translators: 1 share ratio 2 author level label */ __('比例 %1$d%% · %2$s', 'aksrt'), (int) ($row['share_ratio'] ?? 0), $this->author_level_label((int) ($row['author_level'] ?? 0)) )) . '
'; echo '' . esc_html($this->format_amount($row['amount'] ?? 0, $is_points)) . '' . '
' . esc_html($is_points ? __('积分钱包', 'aksrt') : __('余额钱包', 'aksrt')) . '
'; echo '' . NativeUi::status_badge($status, $this->status_badge_map()) . $this->status_hint($row, $cycle_days) . ''; echo ''; if ($pending && $can_settle) { echo $this->settle_form_html($id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } else { echo '—'; } echo ''; } /** 已结算显示结算时间;待结算显示预计自动结算时间(产生时间 + 结算周期,站点时区)。 */ private function status_hint(array $row, int $cycle_days): string { if (($row['status'] ?? '') === 'settled') { $at = (string) ($row['settled_at'] ?? ''); return $at !== '' ? '
' . esc_html(sprintf(__('%s 结算', 'aksrt'), $at)) . '
' : ''; } $created = (string) ($row['created_at'] ?? ''); if ($created === '') { return ''; } try { $due = (new \DateTimeImmutable($created, wp_timezone()))->modify('+' . $cycle_days . ' days'); } catch (\Exception $e) { return ''; } $text = $due <= new \DateTimeImmutable('now', wp_timezone()) ? __('已到期,等待自动结算', 'aksrt') /* translators: %s expected settle time */ : sprintf(__('预计 %s 自动结算', 'aksrt'), $due->format('m-d H:i')); return '
' . esc_html($text) . '
'; } private function author_level_label(int $level): string { return $level > 0 /* translators: %d VIP level */ ? sprintf(__('VIP%d 作者', 'aksrt'), $level) : __('普通作者', 'aksrt'); } /** @param array{enabled:bool,cycle_days:int} $policy */ private function render_policy_callout(array $policy): void { if (!$policy['enabled']) { echo '
' . esc_html__('创作分成功能未启用:不会产生新的分成记录,已有的待结算记录也无法结算。', 'aksrt') . ' ' . esc_html__('前往启用', 'aksrt') . '
'; return; } echo '
' . esc_html(sprintf( /* translators: %d settlement cycle days */ __('结算周期 %d 天:分成产生满周期后由系统每小时自动结算到作者钱包。手动结算会立即入账,不可撤销。', 'aksrt'), $policy['cycle_days'] )) . '
'; } /** 现金与积分分开统计,不跨单位相加;按用户筛选时统计跟随该用户。 */ private function render_income_stats(array $stats, int $user_id): void { $cash = (array) ($stats['cash'] ?? []); $points = (array) ($stats['points'] ?? []); $scope = $user_id > 0 /* translators: %d user ID */ ? sprintf(__('用户 #%d', 'aksrt'), $user_id) : __('全站', 'aksrt'); $count_hint = static function (array $bucket) use ($scope): string { /* translators: 1 scope 2 record count */ return sprintf(__('%1$s · %2$s 条记录', 'aksrt'), $scope, number_format_i18n((int) ($bucket['count'] ?? 0))); }; $pending_url = $this->page_url(array_filter(['status' => 'pending', 'user_id' => $user_id ?: null])); $settled_url = $this->page_url(array_filter(['status' => 'settled', 'user_id' => $user_id ?: null])); NativeUi::stats([ ['label' => __('现金待结算', 'aksrt'), 'value' => $this->format_amount($cash['pending'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'], ['label' => __('现金已结算', 'aksrt'), 'value' => $this->format_amount($cash['settled'] ?? '0', false), 'url' => add_query_arg('currency', 'cny', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], ['label' => __('现金分成合计', 'aksrt'), 'value' => $this->format_amount($cash['total'] ?? '0', false), 'hint' => $count_hint($cash), 'icon' => 'dashicons-chart-area', 'tone' => 'info'], ['label' => __('积分待结算', 'aksrt'), 'value' => $this->format_amount($points['pending'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $pending_url), 'icon' => 'dashicons-clock', 'tone' => 'warning'], ['label' => __('积分已结算', 'aksrt'), 'value' => $this->format_amount($points['settled'] ?? 0, true), 'url' => add_query_arg('currency', 'points', $settled_url), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], ['label' => __('积分分成合计', 'aksrt'), 'value' => $this->format_amount($points['total'] ?? 0, true), 'hint' => $count_hint($points), 'icon' => 'dashicons-star-filled', 'tone' => 'info'], ]); } /** * @param array $filters * @param array $query_args * @param array $counts */ private function render_status_views(array $filters, 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 */ private function render_filter_card(array $filters): void { NativeUi::card_open('', '', false, 'aksrt-income-filter-card'); echo ''; NativeUi::card_close(); } /** @param array $options */ private function filter_select(string $name, string $label, array $options, string $current): string { $html = ''; } /** @return array */ private function currency_options(): array { return [ '' => __('全部币种', 'aksrt'), 'cny' => __('现金(CNY)', 'aksrt'), 'points' => __('积分', 'aksrt'), ]; } /** @return array */ private function order_status_options(): array { return ['' => __('全部订单状态', 'aksrt')] + \AKSRT\Core\DomainModel::order_status_labels(); } /** 批量结算表单只放提交按钮;表格里的复选框通过 form="aksrt-income-batch" 关联进来。 */ private function batch_form_html(): string { return '
' . '' . $this->operation_key_field() . wp_nonce_field('aksrt_pay_batch_settle_income', '_wpnonce', true, false) . '' . esc_html__('未选择记录', 'aksrt') . '' . '' . '
'; } private function settle_form_html(int $id): string { return '
' . '' . '' . $this->operation_key_field() . wp_nonce_field('aksrt_pay_settle_income_' . $id, '_wpnonce', true, false) . '
'; } /** * 从请求读取并校验筛选条件;非法值一律视为未筛选。 * * @return array{status:string,order_number:string,currency:string,order_status:string,user_id:int,post_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) ? $value : ''; }; $status = sanitize_key($read('status')); $currency = sanitize_key($read('currency')); $order_status = sanitize_key($read('order_status')); return [ 'status' => isset($this->status_labels()[$status]) ? $status : '', 'order_number' => $read('order_number'), 'currency' => $currency !== '' && isset($this->currency_options()[$currency]) ? $currency : '', 'order_status' => $order_status !== '' && isset($this->order_status_options()[$order_status]) ? $order_status : '', 'user_id' => max(0, (int) $read('user_id')), 'post_id' => max(0, (int) $read('post_id')), 'date_from' => $date($read('date_from')), 'date_to' => $date($read('date_to')), ]; } /** * 当前筛选的 URL 参数(不含 paged):状态视图、翻页与重置链接共用同一份,避免漂移。 * * @param array $filters * @return array */ private function list_query_args(array $filters): array { return array_filter($filters, static function ($value): bool { return $value !== '' && $value !== 0; }); } /** * 筛选条件 → Revenue 查询参数(不含状态与分页,由调用方按需追加)。 * * @param array $filters * @return array */ private function list_repository_args(array $filters): array { return array_diff_key($filters, ['status' => true]); } /** 一次性预取本页作者、买家与来源文章,避免逐行查询。 */ private function prime_list_caches(array $items): void { $user_ids = array_merge(array_column($items, 'author_id'), array_column($items, 'buyer_id')); $user_ids = array_values(array_unique(array_filter(array_map('intval', $user_ids)))); $post_ids = array_values(array_unique(array_filter(array_map('intval', array_column($items, 'post_id'))))); if ($user_ids !== []) { cache_users($user_ids); } if ($post_ids !== []) { _prime_post_caches($post_ids, false, false); } } }