|null */ private ?array $payment_labels = null; public static function get_instance(): self { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function init(): void { if (!is_admin()) { return; } add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); add_action('admin_post_jio_pay_update_order_status', [$this, 'handle_update_order_status']); add_action('admin_post_jio_pay_update_order_note', [$this, 'handle_update_order_note']); add_action('admin_post_jio_pay_clear_closed_orders', [$this, 'handle_clear_closed_orders']); add_action('admin_post_jio_pay_delete_order', [$this, 'handle_delete_order']); add_action('admin_post_jio_pay_refund_order', [$this, 'handle_refund_order']); } public function enqueue_assets(): void { $page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : ''; if ($page !== self::PAGE_SLUG) { return; } $css = '/assets/css/admin/orders.css'; $js = '/assets/js/admin-orders.js'; wp_enqueue_style('jio-admin-orders', JIO_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(JIO_DIR . $css)); wp_enqueue_script('jio-admin-orders', JIO_URI . $js, [], (string) filemtime(JIO_DIR . $js), true); } public function render_orders_page(): void { $this->ensure_cap(); NativeUi::page_open('jio-orders-page'); if (!class_exists('\Jio\Payment\OrderRepository')) { NativeUi::header(__('订单明细', 'jio'), '', 'dashicons-list-view'); echo '

' . esc_html__('订单服务不可用。', 'jio') . '

'; NativeUi::page_close(); return; } $detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : ''; if ($detail_order !== '') { $this->render_order_detail_page($detail_order); } else { $this->render_order_list_page(); } NativeUi::page_close(); } /* ------------------------------------------------------------------ * admin-post 处理器 * ----------------------------------------------------------------*/ public function handle_update_order_status(): void { $this->ensure_cap(); $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; check_admin_referer('jio_pay_update_order_status_' . $order_number); $ok = false; if ($order_number !== '' && $new_status !== '' && class_exists('\Jio\Payment\OrderRepository')) { $ok = \Jio\Payment\OrderRepository::get_instance()->update_status($order_number, $new_status); } $this->redirect_back(['jio_pay_msg' => $ok ? 'order_updated' : 'order_failed']); } /** * 「标记退款」:把已支付订单推到已退款。只改订单状态,不调用渠道退款。 * * 退款闸门只剩「订单当前为已支付」——工单系统与订单退款已解耦, * 不再要求先有一条进行中的工单。 */ public function handle_refund_order(): void { $this->ensure_cap(); $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; check_admin_referer('jio_pay_refund_order_' . $order_number); $code = 'order_refund_failed'; if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) { $repo = \Jio\Payment\OrderRepository::get_instance(); $order = $repo->get($order_number); if (!is_array($order)) { $code = 'order_refund_missing'; } elseif ((string) ($order['status'] ?? '') !== DomainModel::ORDER_STATUS_PAID) { $code = 'order_refund_not_paid'; } else { $ok = $repo->update_status($order_number, DomainModel::ORDER_STATUS_REFUNDED, [ 'allow_refund' => true, 'note' => __('管理员在订单明细中标记退款', 'jio'), ]); $code = $ok ? 'order_refunded' : 'order_refund_failed'; } } $this->redirect_back(['jio_pay_msg' => $code]); } public function handle_update_order_note(): void { $this->ensure_cap(); $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; $admin_note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : ''; check_admin_referer('jio_pay_update_order_note_' . $order_number); $ok = false; if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) { $ok = \Jio\Payment\OrderRepository::get_instance()->update_admin_note($order_number, $admin_note); } $this->redirect_back(['jio_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed']); } public function handle_clear_closed_orders(): void { $this->ensure_cap(); check_admin_referer('jio_pay_clear_closed_orders'); $days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14; $deleted = 0; if (class_exists('\Jio\Payment\OrderRepository')) { $deleted = \Jio\Payment\OrderRepository::get_instance()->clear_closed_orders($days); } $this->redirect_back([ 'jio_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty', 'cleared' => $deleted, ], true); } public function handle_delete_order(): void { $this->ensure_cap(); $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; check_admin_referer('jio_pay_delete_order_' . $order_number); $ok = false; if ($order_number !== '' && class_exists('\Jio\Payment\OrderRepository')) { $repo = \Jio\Payment\OrderRepository::get_instance(); $order = $repo->get($order_number); // 只允许硬删已关闭订单。 if (is_array($order) && (string) ($order['status'] ?? '') === DomainModel::ORDER_STATUS_CLOSED) { $ok = $repo->delete_order($order_number); } } // 删除成功后详情已不存在,回到列表;失败时留在原处。 $this->redirect_back(['jio_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed'], $ok); } /** * 回到发起操作的订单页(保留列表筛选与分页,或停在详情)。 * $to_list 时去掉 order 参数回到列表。来源不是本页时落到列表首页。 */ private function redirect_back(array $args, bool $to_list = false): void { $query = []; $referer = wp_get_referer(); if (is_string($referer) && $referer !== '') { parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed); if (($parsed['page'] ?? '') === self::PAGE_SLUG) { unset($parsed['page'], $parsed['jio_pay_msg'], $parsed['cleared']); if ($to_list) { unset($parsed['order']); } $query = $parsed; } } wp_safe_redirect($this->page_url(self::PAGE_SLUG, array_merge($query, $args))); exit; } /* ------------------------------------------------------------------ * 订单动作表单(列表行与详情侧栏共用) * ----------------------------------------------------------------*/ /** 当前状态可执行的动作按钮;没有可执行动作时返回空串。 */ private function order_actions_html(string $order_number, string $status, bool $small = true): string { $labels = [ DomainModel::ORDER_STATUS_PAID => __('确认收款', 'jio'), DomainModel::ORDER_STATUS_CLOSED => __('关闭订单', 'jio'), ]; $confirms = [ DomainModel::ORDER_STATUS_PAID => __('确认将该订单标记为已支付?系统会立即为用户发放对应权益。', 'jio'), DomainModel::ORDER_STATUS_CLOSED => __('确认关闭该订单?关闭后用户无法继续支付。', 'jio'), ]; $html = ''; foreach ((array) (DomainModel::order_transitions()[$status] ?? []) as $target) { // 已退款只走下方专用的退款动作(带 allow_refund 闸门),不进通用状态按钮。 if (!isset($labels[$target])) { continue; } $html .= $this->action_form( 'jio_pay_update_order_status', 'jio_pay_update_order_status_' . $order_number, ['order_number' => $order_number, 'new_status' => $target], $labels[$target], $confirms[$target], $target === DomainModel::ORDER_STATUS_CLOSED, $small ); } // 已支付是已退款的唯一前置状态。 if ($status === DomainModel::ORDER_STATUS_PAID) { $html .= $this->action_form( 'jio_pay_refund_order', 'jio_pay_refund_order_' . $order_number, ['order_number' => $order_number], __('标记退款', 'jio'), __('只会把订单标记为已退款,不会自动原路退回款项。确认继续?此操作不可撤销。', 'jio'), true, $small ); } if ($status === DomainModel::ORDER_STATUS_CLOSED) { $html .= $this->action_form( 'jio_pay_delete_order', 'jio_pay_delete_order_' . $order_number, ['order_number' => $order_number], __('删除', 'jio'), __('确认永久删除此订单?不可恢复。', 'jio'), true, $small ); } return $html; } /** * 单按钮 POST 表单。确认文案放在 data-jio-confirm,由 admin-orders.js 拦截提交。 * * @param array $fields */ private function action_form(string $action, string $nonce_action, array $fields, string $label, string $confirm = '', bool $danger = false, bool $small = true): string { $html = '
'; $html .= ''; foreach ($fields as $name => $value) { $html .= ''; } $html .= wp_nonce_field($nonce_action, '_wpnonce', true, false); $class = 'button' . ($small ? ' button-small' : '') . ($danger ? ' is-danger' : ''); return $html . '
'; } /* ------------------------------------------------------------------ * 展示助手 * ----------------------------------------------------------------*/ /** @return array */ private function status_labels(): array { return DomainModel::order_status_labels(); } /** * 订单状态键 → [文案, 色调] 映射,供 NativeUi::status_badge() 用。 * * @return array */ private function status_badge_map(): array { $tones = [ DomainModel::ORDER_STATUS_PENDING => 'warning', DomainModel::ORDER_STATUS_PAID => 'success', DomainModel::ORDER_STATUS_REFUNDED => 'info', DomainModel::ORDER_STATUS_CLOSED => 'neutral', ]; $map = []; foreach ($this->status_labels() as $key => $label) { $map[$key] = [(string) $label, $tones[$key] ?? 'neutral']; } return $map; } private function product_type_label(string $type): string { return DomainModel::order_product_type_labels()[$type] ?? ($type !== '' ? $type : '—'); } /** 商品名:订单快照里的标题优先,付费内容回落到文章标题;都没有时返回空串。 */ private function product_title(array $order): string { $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; $title = (string) ($meta['product_title'] ?? $meta['title'] ?? $meta['package_title'] ?? ''); $id = (int) ($order['product_id'] ?? 0); if ($title === '' && $id > 0 && DomainModel::is_resource_product_type((string) ($order['product_type'] ?? ''))) { $title = (string) get_the_title($id); } return $title; } /** 支付方式键 → 显示名:已注册网关取网关名称,其余为站内固定方式。 */ private function payment_method_label(string $method): string { if ($this->payment_labels === null) { $this->payment_labels = [ 'points' => __('积分', 'jio'), 'free' => __('免费', 'jio'), ]; if (class_exists('\Jio\Payment\Gateway')) { foreach (\Jio\Payment\Gateway::get_instance()->get_all() as $id => $gateway) { $this->payment_labels[(string) $id] = $gateway->get_name(); } } } return $this->payment_labels[$method] ?? ($method !== '' ? $method : '—'); } /** @return string[] */ private function third_party_gateway_ids(): array { return array_values(array_diff( array_keys(\Jio\Payment\Gateway::get_instance()->get_all()), ['balance', 'points'] )); } private function is_points_order(array $order): bool { return sanitize_key((string) ($order['currency'] ?? '')) === 'points' || sanitize_key((string) ($order['payment_method'] ?? '')) === 'points'; } /** @param array $order */ private function order_amount(array $order): string { if ($this->is_points_order($order)) { return max(0, (int) ($order['amount'] ?? 0)) . ' ' . __('积分', 'jio'); } return $this->cash_amount((string) ($order['amount'] ?? '0'), (string) ($order['currency'] ?? '')); } private function cash_amount(string $value, string $currency): string { return $this->cash_decimal($value) . ' ' . strtoupper($currency !== '' ? $currency : 'CNY'); } private function cash_decimal(string $value): string { try { return \Jio\Ledger\AssetAmount::balance($value)->decimal(); } catch (\InvalidArgumentException $e) { return '0.00'; } } private function render_flash(): void { if (empty($_GET['jio_pay_msg'])) { return; } $code = sanitize_key((string) wp_unslash($_GET['jio_pay_msg'])); // 码 => [文案, 是否失败] $map = [ 'order_updated' => [__('订单状态已更新。', 'jio'), false], 'order_failed' => [__('订单状态更新失败,订单可能已被其他操作改变,请刷新后重试。', 'jio'), true], 'order_refunded' => [__('订单已标记为已退款。款项请在支付渠道后台原路退回。', 'jio'), false], 'order_refund_failed' => [__('订单标记退款失败。', 'jio'), true], 'order_refund_missing' => [__('订单不存在。', 'jio'), true], 'order_refund_not_paid' => [__('仅已支付订单可以标记退款。', 'jio'), true], 'order_note_updated' => [__('运营备注已保存。', 'jio'), false], 'order_note_failed' => [__('运营备注保存失败。', 'jio'), true], 'orders_cleared' => [sprintf( /* translators: %d deleted count */ __('已清理 %d 条关闭订单。', 'jio'), max(0, (int) ($_GET['cleared'] ?? 0)) ), false], 'orders_clear_empty' => [__('没有可清理的关闭订单。', 'jio'), false], 'order_deleted' => [__('订单已删除。', 'jio'), false], 'order_delete_failed' => [__('订单删除失败(仅允许删除已关闭订单)。', 'jio'), true], ]; if (!isset($map[$code])) { return; } [$message, $failed] = $map[$code]; echo '

' . esc_html($message) . '

'; } private function ensure_cap(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('您没有权限访问此页面。', 'jio')); } } /** 参数值统一 urlencode:add_query_arg 不编码值,搜索词里的 & / # 会截断链接。 */ private function page_url(string $slug, array $args = []): string { return add_query_arg(urlencode_deep(array_merge(['page' => $slug], $args)), admin_url('admin.php')); } }