From 22dfed597751bf5e294b2279f120cfdaad47539e Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Jul 2026 09:52:29 +0800 Subject: [PATCH] refactor(admin): split pay menus modules; plate icon/sort fields --- docs/zibll-gap-matrix.md | 4 +- functions.php | 9 + inc/bootstrap.php | 3 + .../class-aksrt-admin-pay-after-sale.php | 365 +++++++ .../class-aksrt-admin-pay-charge-cards.php | 350 ++++++ .../native/class-aksrt-admin-pay-menus.php | 995 +----------------- .../native/class-aksrt-admin-pay-shipping.php | 509 +++++++++ .../forum/class-aksrt-plate-meta-box.php | 32 + 8 files changed, 1304 insertions(+), 963 deletions(-) create mode 100644 inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php create mode 100644 inc/classes/admin/native/class-aksrt-admin-pay-charge-cards.php create mode 100644 inc/classes/admin/native/class-aksrt-admin-pay-shipping.php diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md index 85b066f5..c38a1d47 100644 --- a/docs/zibll-gap-matrix.md +++ b/docs/zibll-gap-matrix.md @@ -99,8 +99,8 @@ Note: zibll「商品管理」`zibpay_product_page` is **paid article sales**, no | 2b Shipping/after-sale/coupons/income/rebate | done | | 3 users.php submenus | done | | 4 SPA strip | done (ops routes redirect to native) | -| 5+ behavior parity | in progress — shipping list create + pending-to-ship board + express timeline query; after-sale filters/actions; order note/quick actions; coupons field parity + batch/enable + product_scope create/edit/batch (native module split); charge-cards expires + code dump + search/labels; product meta shipping/service/limit fields; plate allow_view/add_limit/follow/role + moderator roles UI; topic visibility column | -| 5 remaining | optional plate icon/sort native fields; further pay-menus modular split (shipping/charge-card still in shell); zibll pay-hide/points plate options out of scope | +| 5+ behavior parity | done for ops shell — shipping/pending/express; after-sale; order note/actions; coupons create/edit/batch + product_scope; charge-cards; product commerce meta; plate allow_view/add_limit/follow/role + moderator roles + icon/sort; topic visibility; pay-menus modular split (coupons/charge-cards/shipping/after-sale) | +| 5 remaining | zibll pay-hide/points plate options out of scope; optional further shell trim (orders/income still in pay-menus ~1k lines) | ## Dual-UI rule diff --git a/functions.php b/functions.php index 3e6d147c..45e0d6d5 100644 --- a/functions.php +++ b/functions.php @@ -359,6 +359,15 @@ if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) { if (is_admin() && class_exists('AKSRT_Admin_Pay_Coupons')) { AKSRT_Admin_Pay_Coupons::get_instance()->init(); } +if (is_admin() && class_exists('AKSRT_Admin_Pay_Charge_Cards')) { + AKSRT_Admin_Pay_Charge_Cards::get_instance()->init(); +} +if (is_admin() && class_exists('AKSRT_Admin_Pay_Shipping')) { + AKSRT_Admin_Pay_Shipping::get_instance()->init(); +} +if (is_admin() && class_exists('AKSRT_Admin_Pay_After_Sale')) { + AKSRT_Admin_Pay_After_Sale::get_instance()->init(); +} // WordPress 原生用户菜单:消息/邀请码/认证/禁封(zibll users.php 子菜单对齐) if (is_admin() && class_exists('AKSRT_Admin_Users_Menus')) { diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 2e2db616..8ffddeec 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -174,6 +174,9 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-category-rewrite.php'; require_once AKSRT_DIR . '/inc/classes/admin/class-aksrt-admin-message-controller.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-menus.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-coupons.php'; +require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-charge-cards.php'; +require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-shipping.php'; +require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-users-menus.php'; require_once AKSRT_DIR . '/inc/classes/class-aksrt-admin.php'; diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php b/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php new file mode 100644 index 00000000..56714da1 --- /dev/null +++ b/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php @@ -0,0 +1,365 @@ +get($order_number); + $user_id = (int) ($order['user_id'] ?? 0); + } + $id = AKSRT_After_Sale::get_instance()->create([ + 'order_number' => $order_number, + 'user_id' => $user_id, + 'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')), + 'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')), + 'amount' => (float) ($_POST['amount'] ?? 0), + 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), + ]); + $ok = $id > 0; + } + + $msg = $ok ? 'after_sale_created' : 'after_sale_create_failed'; + if ($redirect_page === 'after_sale') { + wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number])); + exit; + } + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'order' => $order_number, + 'aksrt_pay_msg' => $msg, + ])); + exit; + } + + public function render_after_sale_page(): void { + $this->ensure_cap(); + $this->render_header(__('售后管理', 'aksrt')); + + if (!class_exists('AKSRT_After_Sale')) { + echo '

' . esc_html__('售后模块未初始化。', 'aksrt') . '

'; + $this->render_footer(); + return; + } + + $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; + $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; + $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; + if ($search === '' && isset($_GET['order_number'])) { + $search = sanitize_text_field((string) wp_unslash($_GET['order_number'])); + } + $page_num = max(1, (int) ($_GET['paged'] ?? 1)); + $per_page = 20; + $svc = AKSRT_After_Sale::get_instance(); + $stats = $svc->get_stats(); + $status_options = $this->after_sale_status_options(true); + $type_options = $this->after_sale_type_options(true); + + echo '

' . esc_html(sprintf( + /* translators: 1: total 2: pending */ + __('售后工单共 %1$d · 待处理 %2$d', 'aksrt'), + (int) ($stats['total'] ?? 0), + (int) ($stats['pending'] ?? 0) + )) . '

'; + + echo '

' . esc_html__('新建售后', 'aksrt') . '

'; + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_create_after_sale'); + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + submit_button(__('创建售后', 'aksrt'), 'primary', '', false); + echo '
'; + + echo '
'; + echo ''; + echo ' '; + echo ' '; + echo ' '; + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo '
'; + + $list_args = [ + 'status' => $status, + 'type' => $type, + 'limit' => $per_page, + 'offset' => ($page_num - 1) * $per_page, + ]; + if ($search !== '') { + $list_args['order_number'] = $search; + } + $result = $svc->get_list($list_args); + $items = $result['items'] ?? []; + $total = (int) ($result['total'] ?? 0); + $pages = max(1, (int) ceil(max(1, $total) / $per_page)); + $row_status_options = $this->after_sale_status_options(false); + + echo ''; + echo ''; + echo ''; + if (!$items) { + echo ''; + } + foreach ($items as $row) { + $id = (int) ($row['id'] ?? 0); + $user_id = (int) ($row['user_id'] ?? 0); + $user = $user_id > 0 ? get_userdata($user_id) : null; + $st = (string) ($row['status'] ?? ''); + $order_number = (string) ($row['order_number'] ?? ''); + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无售后工单', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; + if ($st === 'pending') { + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_after_sale_action_' . $id); + submit_button(__('同意', 'aksrt'), 'small', '', false); + echo '
'; + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_after_sale_action_' . $id); + echo ''; + echo '
'; + } + if ($st === 'processing') { + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_after_sale_action_' . $id); + submit_button(__('确认收货', 'aksrt'), 'small', '', false); + echo '
'; + } + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_update_after_sale_' . $id); + echo ''; + echo ''; + submit_button(__('更新', 'aksrt'), 'small', '', false); + echo '
'; + echo '
'; + $this->render_pagination($pages, $page_num); + $this->render_footer(); + } + + public function handle_update_after_sale(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; + $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; + $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; + check_admin_referer('aksrt_pay_update_after_sale_' . $id); + + $ok = false; + if ($id > 0 && $new_status !== '' && class_exists('AKSRT_After_Sale')) { + $ok = AKSRT_After_Sale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note); + } + wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed'])); + exit; + } + + public function handle_after_sale_action(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; + $as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : ''; + check_admin_referer('aksrt_pay_after_sale_action_' . $id); + + $ok = false; + if ($id > 0 && class_exists('AKSRT_After_Sale')) { + $svc = AKSRT_After_Sale::get_instance(); + $operator_id = get_current_user_id(); + if ($as_action === 'approve') { + $ok = $svc->merchant_approve($id, $operator_id); + } elseif ($as_action === 'reject') { + $ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt')); + } elseif ($as_action === 'confirm_return') { + $ok = $svc->merchant_confirm_return($id, $operator_id); + } + } + + wp_safe_redirect($this->page_url('aksrt_after_sale', [ + 'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed', + ])); + exit; + } + + + private function after_sale_status_options(bool $include_all = false): array { + $options = []; + if ($include_all) { + $options[''] = __('全部状态', 'aksrt'); + } + if (class_exists('AKSRT_After_Sale')) { + foreach (AKSRT_After_Sale::get_status_options() as $item) { + $value = (string) ($item['value'] ?? ''); + if ($value === '') { + continue; + } + $options[$value] = (string) ($item['label'] ?? $value); + } + } + return $options; + } + + /** + * @return array + */ + public function after_sale_type_options_public(bool $include_all = false): array { + return $this->after_sale_type_options($include_all); + } + + private function after_sale_type_options(bool $include_all = false): array { + $options = []; + if ($include_all) { + $options[''] = __('全部类型', 'aksrt'); + } + // Keep operational create options broader than public get_type_options(). + $defaults = [ + 'refund' => __('仅退款', 'aksrt'), + 'refund_return' => __('退货退款', 'aksrt'), + 'exchange' => __('换货', 'aksrt'), + 'repair' => __('维修', 'aksrt'), + ]; + if (class_exists('AKSRT_After_Sale')) { + foreach (AKSRT_After_Sale::get_type_options() as $item) { + $value = (string) ($item['value'] ?? ''); + if ($value === '') { + continue; + } + $defaults[$value] = (string) ($item['label'] ?? $value); + } + } + return array_merge($options, $defaults); + } + + + private function ensure_cap(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); + } + } + + private function render_header(string $title): void { + echo '
'; + echo '

' . esc_html($title) . '

'; + $this->render_flash(); + } + + private function render_footer(): void { + echo '
'; + } + + private function render_flash(): void { + if (empty($_GET['aksrt_pay_msg'])) { + return; + } + $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); + $map = [ + 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), + 'after_sale_failed' => __('售后状态更新失败。', 'aksrt'), + 'after_sale_created' => __('售后工单已创建。', 'aksrt'), + 'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'), + 'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'), + 'after_sale_action_failed' => __('售后操作失败。', 'aksrt'), + ]; + if (!isset($map[$code])) { + return; + } + $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; + echo '

' . esc_html($map[$code]) . '

'; + } + + private function page_url(string $slug, array $args = []): string { + return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); + } + + private function render_pagination(int $pages, int $page_num): void { + if ($pages <= 1) { + return; + } + echo '
'; + echo paginate_links([ + 'base' => add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page_num, + ]); + echo '
'; + } +} diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-charge-cards.php b/inc/classes/admin/native/class-aksrt-admin-pay-charge-cards.php new file mode 100644 index 00000000..b7391970 --- /dev/null +++ b/inc/classes/admin/native/class-aksrt-admin-pay-charge-cards.php @@ -0,0 +1,350 @@ +ensure_cap(); + $this->render_header(__('卡密管理', 'aksrt')); + + if (!class_exists('AKSRT_Charge_Card')) { + echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; + $this->render_footer(); + return; + } + + $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; + $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; + $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; + $page_num = max(1, (int) ($_GET['paged'] ?? 1)); + $per_page = 20; + $svc = AKSRT_Charge_Card::get_instance(); + $stats = $svc->get_stats(); + $type_labels = [ + 'balance' => __('余额', 'aksrt'), + 'vip' => __('VIP', 'aksrt'), + 'credits' => __('积分', 'aksrt'), + 'points' => __('积分', 'aksrt'), + ]; + $status_labels = [ + 'active' => __('可用', 'aksrt'), + 'used' => __('已用', 'aksrt'), + 'disabled' => __('已禁用', 'aksrt'), + 'expired' => __('已过期', 'aksrt'), + ]; + + echo '

' . esc_html(sprintf( + /* translators: 1: total 2: active 3: used */ + __('共 %1$d 张 · 可用 %2$d · 已用 %3$d', 'aksrt'), + (int) ($stats['total'] ?? 0), + (int) ($stats['active'] ?? 0), + (int) ($stats['used'] ?? 0) + )) . '

'; + + echo '

' . esc_html__('批量生成卡密', 'aksrt') . '

'; + echo '
'; + echo ''; + wp_nonce_field('aksrt_pay_batch_charge_cards'); + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + submit_button(__('生成', 'aksrt'), 'primary', '', false); + echo '
'; + + if (!empty($_GET['aksrt_generated'])) { + $generated = (int) $_GET['aksrt_generated']; + echo '

' . esc_html(sprintf( + /* translators: %d count */ + __('成功生成 %d 张卡密。', 'aksrt'), + $generated + )) . '

'; + } + + $generated_codes = get_transient('aksrt_pay_card_codes_' . get_current_user_id()); + if (is_array($generated_codes) && $generated_codes) { + delete_transient('aksrt_pay_card_codes_' . get_current_user_id()); + echo '

' . esc_html__('本次生成卡密(请及时复制保存):', 'aksrt') . '

'; + echo ''; + echo '
'; + } + + echo '
'; + echo ''; + echo ' '; + echo ' '; + echo ' '; + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo '
'; + + $result = $svc->get_list([ + 'status' => $status, + 'type' => $type, + 'search' => $search, + 'limit' => $per_page, + 'offset' => ($page_num - 1) * $per_page, + ]); + $items = $result['items'] ?? []; + $total = (int) ($result['total'] ?? 0); + $pages = max(1, (int) ($result['max_pages'] ?? 1)); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if (!$items) { + echo ''; + } + + foreach ($items as $row) { + $id = (int) ($row['id'] ?? 0); + $st = (string) ($row['status'] ?? ''); + $row_type = (string) ($row['type'] ?? ''); + $user_id = (int) ($row['used_by'] ?? $row['user_id'] ?? 0); + $user = $user_id > 0 ? get_userdata($user_id) : null; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('密码', 'aksrt') . '' . esc_html__('面额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('过期', 'aksrt') . '' . esc_html__('使用者', 'aksrt') . '' . esc_html__('创建时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无卡密', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($type_labels[$row_type] ?? $row_type) . '' . esc_html((string) ($row['password'] ?? $row['code'] ?? '')) . '' . esc_html((string) ($row['amount'] ?? '')) . '' . esc_html($status_labels[$st] ?? $st) . '' . esc_html((string) ($row['expires_at'] ?? '—')) . '' . ($user ? esc_html($user->display_name) : '—') . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; + if ($st === 'active') { + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_disable_charge_card_' . $id); + submit_button(__('禁用', 'aksrt'), 'small', '', false); + echo '
'; + } else { + echo '—'; + } + echo '
'; + + if ($pages > 1) { + echo '
'; + echo paginate_links([ + 'base' => add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page_num, + ]); + echo '
'; + } + + $this->render_footer(); + } + + public function handle_disable_charge_card(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + + $id = isset($_POST['card_id']) ? (int) $_POST['card_id'] : 0; + check_admin_referer('aksrt_pay_disable_charge_card_' . $id); + + $ok = false; + if ($id > 0 && class_exists('AKSRT_Charge_Card')) { + $ok = AKSRT_Charge_Card::get_instance()->disable($id); + } + + wp_safe_redirect($this->page_url('aksrt_charge_cards', [ + 'aksrt_pay_msg' => $ok ? 'card_disabled' : 'card_failed', + ])); + exit; + } + + public function handle_batch_charge_cards(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + check_admin_referer('aksrt_pay_batch_charge_cards'); + + if (!class_exists('AKSRT_Charge_Card')) { + wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); + exit; + } + + $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'balance'; + $count = isset($_POST['count']) ? (int) $_POST['count'] : 10; + $length = isset($_POST['length']) ? (int) $_POST['length'] : 16; + $prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : ''; + $amount = isset($_POST['amount']) ? (float) wp_unslash($_POST['amount']) : 0; + $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; + $level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1; + $days = isset($_POST['vip_days']) ? max(1, (int) $_POST['vip_days']) : 30; + $expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : ''; + if ($expires !== '') { + $expires = str_replace('T', ' ', $expires); + if (strlen($expires) === 16) { + $expires .= ':00'; + } + } + + if ($type === 'points') { + $type = 'credits'; + } + + $meta = []; + if ($type === 'balance') { + $meta['amount'] = max(0, $amount); + } elseif ($type === 'credits') { + $meta['points'] = max(0, (int) $amount); + } elseif ($type === 'vip') { + $meta['level'] = $level; + $meta['days'] = $days; + } + + $codes = AKSRT_Charge_Card::get_instance()->batch_create($type, $meta, $count, $length, $prefix, $expires, $note); + $ok = !empty($codes); + if ($ok) { + set_transient('aksrt_pay_card_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS); + } + + wp_safe_redirect($this->page_url('aksrt_charge_cards', [ + 'aksrt_pay_msg' => $ok ? 'card_batch_ok' : 'card_batch_failed', + 'aksrt_generated' => count($codes), + ])); + exit; + } + + + private function ensure_cap(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); + } + } + + private function render_header(string $title): void { + echo '
'; + echo '

' . esc_html($title) . '

'; + $this->render_flash(); + } + + private function render_footer(): void { + echo '
'; + } + + private function render_flash(): void { + if (empty($_GET['aksrt_pay_msg'])) { + return; + } + $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); + $map = [ + 'card_disabled' => __('充值卡已禁用。', 'aksrt'), + 'card_failed' => __('充值卡禁用失败。', 'aksrt'), + 'card_batch_ok' => __('卡密批量生成完成。', 'aksrt'), + 'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'), + ]; + if (!isset($map[$code])) { + return; + } + $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; + echo '

' . esc_html($map[$code]) . '

'; + } + + private function page_url(string $slug, array $args = []): string { + return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); + } + + private function render_pagination(int $pages, int $page_num): void { + if ($pages <= 1) { + return; + } + echo '
'; + echo paginate_links([ + 'base' => add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page_num, + ]); + echo '
'; + } +} diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php index c6d4e099..d5a21310 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php @@ -33,15 +33,7 @@ class AKSRT_Admin_Pay_Menus { add_action('admin_post_aksrt_pay_update_order_status', [$this, 'handle_update_order_status']); add_action('admin_post_aksrt_pay_update_order_note', [$this, 'handle_update_order_note']); add_action('admin_post_aksrt_pay_update_withdrawal', [$this, 'handle_update_withdrawal']); - add_action('admin_post_aksrt_pay_disable_charge_card', [$this, 'handle_disable_charge_card']); - add_action('admin_post_aksrt_pay_batch_charge_cards', [$this, 'handle_batch_charge_cards']); - add_action('admin_post_aksrt_pay_update_shipping', [$this, 'handle_update_shipping']); - add_action('admin_post_aksrt_pay_query_express', [$this, 'handle_query_express']); - add_action('admin_post_aksrt_pay_create_shipping', [$this, 'handle_create_shipping']); - add_action('admin_post_aksrt_pay_update_after_sale', [$this, 'handle_update_after_sale']); - add_action('admin_post_aksrt_pay_create_after_sale', [$this, 'handle_create_after_sale']); - add_action('admin_post_aksrt_pay_after_sale_action', [$this, 'handle_after_sale_action']); - // Coupon admin_post handlers live in AKSRT_Admin_Pay_Coupons. + // charge-cards / shipping / after-sale / coupons live in dedicated native classes. add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']); add_action('admin_post_aksrt_pay_settle_rebate', [$this, 'handle_settle_rebate']); add_action('admin_post_aksrt_pay_cancel_rebate', [$this, 'handle_cancel_rebate']); @@ -65,12 +57,24 @@ class AKSRT_Admin_Pay_Menus { 'shipping' => [ 'slug' => 'aksrt_shipping', 'title' => __('发货及物流', 'aksrt'), - 'callback' => [$this, 'render_shipping_page'], + 'callback' => static function (): void { + if (class_exists('AKSRT_Admin_Pay_Shipping')) { + AKSRT_Admin_Pay_Shipping::get_instance()->render_shipping_page(); + return; + } + wp_die(esc_html__('物流模块未初始化。', 'aksrt')); + }, ], 'after_sale' => [ 'slug' => 'aksrt_after_sale', 'title' => __('售后管理', 'aksrt'), - 'callback' => [$this, 'render_after_sale_page'], + 'callback' => static function (): void { + if (class_exists('AKSRT_Admin_Pay_After_Sale')) { + AKSRT_Admin_Pay_After_Sale::get_instance()->render_after_sale_page(); + return; + } + wp_die(esc_html__('售后模块未初始化。', 'aksrt')); + }, ], 'coupons' => [ 'slug' => 'aksrt_coupons', @@ -86,7 +90,13 @@ class AKSRT_Admin_Pay_Menus { 'charge_cards' => [ 'slug' => 'aksrt_charge_cards', 'title' => __('卡密管理', 'aksrt'), - 'callback' => [$this, 'render_charge_cards_page'], + 'callback' => static function (): void { + if (class_exists('AKSRT_Admin_Pay_Charge_Cards')) { + AKSRT_Admin_Pay_Charge_Cards::get_instance()->render_charge_cards_page(); + return; + } + wp_die(esc_html__('卡密模块未初始化。', 'aksrt')); + }, ], 'income' => [ 'slug' => 'aksrt_income', @@ -206,22 +216,13 @@ class AKSRT_Admin_Pay_Menus { 'order_failed' => __('订单状态更新失败。', 'aksrt'), 'order_note_updated' => __('订单备注已保存。', 'aksrt'), 'order_note_failed' => __('订单备注保存失败。', 'aksrt'), - 'after_sale_action_ok' => __('售后操作已完成。', 'aksrt'), - 'after_sale_action_failed' => __('售后操作失败。', 'aksrt'), 'withdraw_updated' => __('提现状态已更新。', 'aksrt'), 'withdraw_failed' => __('提现状态更新失败。', 'aksrt'), - 'card_disabled' => __('充值卡已禁用。', 'aksrt'), - 'card_failed' => __('充值卡禁用失败。', 'aksrt'), - 'card_batch_ok' => __('卡密批量生成完成。', 'aksrt'), - 'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'), + // Order detail still redirects shipping/after-sale actions here. 'shipping_updated' => __('物流状态已更新。', 'aksrt'), 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), 'shipping_created' => __('发货记录已创建。', 'aksrt'), 'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'), - 'express_ok' => __('物流轨迹已更新。', 'aksrt'), - 'express_failed' => __('物流轨迹查询失败。', 'aksrt'), - 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), - 'after_sale_failed' => __('售后状态更新失败。', 'aksrt'), 'after_sale_created' => __('售后工单已创建。', 'aksrt'), 'after_sale_create_failed' => __('售后工单创建失败。', 'aksrt'), 'income_settled' => __('分成已结算。', 'aksrt'), @@ -779,831 +780,6 @@ class AKSRT_Admin_Pay_Menus { exit; } - /* ------------------------------------------------------------------ - * Charge cards - * ----------------------------------------------------------------*/ - - public function render_charge_cards_page(): void { - $this->ensure_cap(); - $this->render_header(__('卡密管理', 'aksrt')); - - if (!class_exists('AKSRT_Charge_Card')) { - echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = AKSRT_Charge_Card::get_instance(); - $stats = $svc->get_stats(); - $type_labels = [ - 'balance' => __('余额', 'aksrt'), - 'vip' => __('VIP', 'aksrt'), - 'credits' => __('积分', 'aksrt'), - 'points' => __('积分', 'aksrt'), - ]; - $status_labels = [ - 'active' => __('可用', 'aksrt'), - 'used' => __('已用', 'aksrt'), - 'disabled' => __('已禁用', 'aksrt'), - 'expired' => __('已过期', 'aksrt'), - ]; - - echo '

' . esc_html(sprintf( - /* translators: 1: total 2: active 3: used */ - __('共 %1$d 张 · 可用 %2$d · 已用 %3$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['active'] ?? 0), - (int) ($stats['used'] ?? 0) - )) . '

'; - - echo '

' . esc_html__('批量生成卡密', 'aksrt') . '

'; - echo '
'; - echo ''; - wp_nonce_field('aksrt_pay_batch_charge_cards'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('生成', 'aksrt'), 'primary', '', false); - echo '
'; - - if (!empty($_GET['aksrt_generated'])) { - $generated = (int) $_GET['aksrt_generated']; - echo '

' . esc_html(sprintf( - /* translators: %d count */ - __('成功生成 %d 张卡密。', 'aksrt'), - $generated - )) . '

'; - } - - $generated_codes = get_transient('aksrt_pay_card_codes_' . get_current_user_id()); - if (is_array($generated_codes) && $generated_codes) { - delete_transient('aksrt_pay_card_codes_' . get_current_user_id()); - echo '

' . esc_html__('本次生成卡密(请及时复制保存):', 'aksrt') . '

'; - echo ''; - echo '
'; - } - - echo '
'; - echo ''; - echo ' '; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $result = $svc->get_list([ - 'status' => $status, - 'type' => $type, - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ($result['max_pages'] ?? 1)); - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - - if (!$items) { - echo ''; - } - - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $st = (string) ($row['status'] ?? ''); - $row_type = (string) ($row['type'] ?? ''); - $user_id = (int) ($row['used_by'] ?? $row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - - echo '
ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('密码', 'aksrt') . '' . esc_html__('面额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('过期', 'aksrt') . '' . esc_html__('使用者', 'aksrt') . '' . esc_html__('创建时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无卡密', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($type_labels[$row_type] ?? $row_type) . '' . esc_html((string) ($row['password'] ?? $row['code'] ?? '')) . '' . esc_html((string) ($row['amount'] ?? '')) . '' . esc_html($status_labels[$st] ?? $st) . '' . esc_html((string) ($row['expires_at'] ?? '—')) . '' . ($user ? esc_html($user->display_name) : '—') . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - if ($st === 'active') { - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_disable_charge_card_' . $id); - submit_button(__('禁用', 'aksrt'), 'small', '', false); - echo '
'; - } else { - echo '—'; - } - echo '
'; - - if ($pages > 1) { - echo '
'; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
'; - } - - $this->render_footer(); - } - - public function handle_disable_charge_card(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - - $id = isset($_POST['card_id']) ? (int) $_POST['card_id'] : 0; - check_admin_referer('aksrt_pay_disable_charge_card_' . $id); - - $ok = false; - if ($id > 0 && class_exists('AKSRT_Charge_Card')) { - $ok = AKSRT_Charge_Card::get_instance()->disable($id); - } - - wp_safe_redirect($this->page_url('aksrt_charge_cards', [ - 'aksrt_pay_msg' => $ok ? 'card_disabled' : 'card_failed', - ])); - exit; - } - - public function handle_batch_charge_cards(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - check_admin_referer('aksrt_pay_batch_charge_cards'); - - if (!class_exists('AKSRT_Charge_Card')) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - - $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'balance'; - $count = isset($_POST['count']) ? (int) $_POST['count'] : 10; - $length = isset($_POST['length']) ? (int) $_POST['length'] : 16; - $prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : ''; - $amount = isset($_POST['amount']) ? (float) wp_unslash($_POST['amount']) : 0; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - $level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1; - $days = isset($_POST['vip_days']) ? max(1, (int) $_POST['vip_days']) : 30; - $expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : ''; - if ($expires !== '') { - $expires = str_replace('T', ' ', $expires); - if (strlen($expires) === 16) { - $expires .= ':00'; - } - } - - if ($type === 'points') { - $type = 'credits'; - } - - $meta = []; - if ($type === 'balance') { - $meta['amount'] = max(0, $amount); - } elseif ($type === 'credits') { - $meta['points'] = max(0, (int) $amount); - } elseif ($type === 'vip') { - $meta['level'] = $level; - $meta['days'] = $days; - } - - $codes = AKSRT_Charge_Card::get_instance()->batch_create($type, $meta, $count, $length, $prefix, $expires, $note); - $ok = !empty($codes); - if ($ok) { - set_transient('aksrt_pay_card_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS); - } - - wp_safe_redirect($this->page_url('aksrt_charge_cards', [ - 'aksrt_pay_msg' => $ok ? 'card_batch_ok' : 'card_batch_failed', - 'aksrt_generated' => count($codes), - ])); - exit; - } - - /* ------------------------------------------------------------------ - * Shipping - * ----------------------------------------------------------------*/ - - public function render_shipping_page(): void { - $this->ensure_cap(); - $this->render_header(__('发货及物流', 'aksrt')); - - if (!class_exists('AKSRT_Shipping')) { - echo '

' . esc_html__('物流模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records'; - if (!in_array($view, ['records', 'pending'], true)) { - $view = 'records'; - } - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = AKSRT_Shipping::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->shipping_status_options(true); - - $pending_total = 0; - if (class_exists('AKSRT_Order_Repository')) { - $pending_total = (int) (AKSRT_Order_Repository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0); - } - - echo '

' . esc_html(sprintf( - /* translators: 1: shipping records 2: pending shipment orders */ - __('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - $pending_total - )) . '

'; - - echo ''; - - if ($view === 'pending') { - $this->render_pending_shipment_board($search, $page_num, $per_page); - $this->render_footer(); - return; - } - - echo '

' . esc_html__('新建发货', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建发货', 'aksrt'), 'primary', '', false); - echo '
'; - - echo '
'; - echo ''; - echo ''; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $args = [ - 'status' => $status, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) { - $args['order_number'] = $search; - } else { - $args['tracking_no'] = $search; - } - } - $result = $svc->get_list($args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->shipping_status_options(false); - $show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0; - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - if ($show_express_id === $id) { - echo ''; - } - } - echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无物流记录', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['carrier'] ?? '')) . '' . esc_html((string) ($row['tracking_no'] ?? '')) . '' . esc_html($row_status_options[$st] ?? $st) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_shipping_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_query_express_' . $id); - submit_button(__('查物流', 'aksrt'), 'small', '', false); - echo '
'; - echo '' . esc_html__('轨迹', 'aksrt') . ''; - echo '
'; - $this->render_express_timeline_panel($id); - echo '
'; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void { - if (!class_exists('AKSRT_Order_Repository')) { - echo '

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

'; - return; - } - - echo '
'; - echo ''; - echo ''; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $result = AKSRT_Order_Repository::get_instance()->get_pending_shipment_orders([ - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ($result['max_pages'] ?? 1)); - $labels = class_exists('AKSRT_Domain_Model') - ? AKSRT_Domain_Model::order_status_labels() - : []; - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - - if (!$items) { - echo ''; - } - - foreach ($items as $row) { - $order_number = (string) ($row['order_number'] ?? ''); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $status_key = (string) ($row['status'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
' . esc_html__('订单号', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('金额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('支付时间', 'aksrt') . '' . esc_html__('立即发货', 'aksrt') . '
' . esc_html__('暂无待发货订单', 'aksrt') . '
' . esc_html($order_number) . '' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '' . esc_html((string) ($row['product_type'] ?? '')) . '' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '' . esc_html($labels[$status_key] ?? $status_key) . '' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . ''; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - $this->render_carrier_field(''); - echo ''; - echo ''; - submit_button(__('发货', 'aksrt'), 'primary small', '', false); - echo '
'; - echo '
'; - $this->render_pagination($pages, $page_num); - } - - public function handle_update_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_shipping_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('AKSRT_Shipping')) { - $ok = AKSRT_Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id()); - } - - $redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : ''; - if ($redirect_order !== '') { - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $redirect_order, - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', - ])); - exit; - } - - public function handle_query_express(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; - $force = !empty($_POST['force']); - check_admin_referer('aksrt_pay_query_express_' . $id); - - $ok = false; - $order_number = ''; - if ($id > 0 && class_exists('AKSRT_Shipping')) { - $svc = AKSRT_Shipping::get_instance(); - $record = $svc->get($id); - $order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : ''; - $result = $svc->query_express($id, $force); - $ok = !empty($result['success']); - } - - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'express_id' => $id, - 's' => $order_number, - 'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed', - ])); - exit; - } - - public function handle_create_shipping(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - $redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order'; - $nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number); - check_admin_referer($nonce_action); - - $ok = false; - if ($order_number !== '' && class_exists('AKSRT_Shipping')) { - if (class_exists('AKSRT_Order_Repository')) { - $order = AKSRT_Order_Repository::get_instance()->get($order_number); - if (!$order) { - $this->redirect_shipping_create_result(false, $order_number, $redirect_page); - } - } - $id = AKSRT_Shipping::get_instance()->create([ - 'order_number' => $order_number, - 'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')), - 'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - 'operator_id' => get_current_user_id(), - ]); - $ok = $id > 0; - } - - $this->redirect_shipping_create_result($ok, $order_number, $redirect_page); - } - - private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void { - $msg = $ok ? 'shipping_created' : 'shipping_create_failed'; - if ($redirect_page === 'shipping') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'records', - 'aksrt_pay_msg' => $msg, - 's' => $order_number, - ])); - exit; - } - if ($redirect_page === 'pending') { - wp_safe_redirect($this->page_url('aksrt_shipping', [ - 'view' => 'pending', - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - public function handle_create_after_sale(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - $redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order'; - $nonce_action = $redirect_page === 'after_sale' ? 'aksrt_pay_create_after_sale' : ('aksrt_pay_create_after_sale_' . $order_number); - check_admin_referer($nonce_action); - - $ok = false; - if ($order_number !== '' && class_exists('AKSRT_After_Sale')) { - $user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0; - if ($user_id <= 0 && class_exists('AKSRT_Order_Repository')) { - $order = AKSRT_Order_Repository::get_instance()->get($order_number); - $user_id = (int) ($order['user_id'] ?? 0); - } - $id = AKSRT_After_Sale::get_instance()->create([ - 'order_number' => $order_number, - 'user_id' => $user_id, - 'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'refund')), - 'reason' => sanitize_textarea_field((string) wp_unslash($_POST['reason'] ?? '')), - 'amount' => (float) ($_POST['amount'] ?? 0), - 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), - ]); - $ok = $id > 0; - } - - $msg = $ok ? 'after_sale_created' : 'after_sale_create_failed'; - if ($redirect_page === 'after_sale') { - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $msg, 's' => $order_number])); - exit; - } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'order' => $order_number, - 'aksrt_pay_msg' => $msg, - ])); - exit; - } - - public function render_after_sale_page(): void { - $this->ensure_cap(); - $this->render_header(__('售后管理', 'aksrt')); - - if (!class_exists('AKSRT_After_Sale')) { - echo '

' . esc_html__('售后模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); - return; - } - - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - if ($search === '' && isset($_GET['order_number'])) { - $search = sanitize_text_field((string) wp_unslash($_GET['order_number'])); - } - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = AKSRT_After_Sale::get_instance(); - $stats = $svc->get_stats(); - $status_options = $this->after_sale_status_options(true); - $type_options = $this->after_sale_type_options(true); - - echo '

' . esc_html(sprintf( - /* translators: 1: total 2: pending */ - __('售后工单共 %1$d · 待处理 %2$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['pending'] ?? 0) - )) . '

'; - - echo '

' . esc_html__('新建售后', 'aksrt') . '

'; - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_create_after_sale'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('创建售后', 'aksrt'), 'primary', '', false); - echo '
'; - - echo '
'; - echo ''; - echo ' '; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
'; - - $list_args = [ - 'status' => $status, - 'type' => $type, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, - ]; - if ($search !== '') { - $list_args['order_number'] = $search; - } - $result = $svc->get_list($list_args); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ceil(max(1, $total) / $per_page)); - $row_status_options = $this->after_sale_status_options(false); - - echo ''; - echo ''; - echo ''; - if (!$items) { - echo ''; - } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; - $st = (string) ($row['status'] ?? ''); - $order_number = (string) ($row['order_number'] ?? ''); - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无售后工单', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['type_label'] ?? $type_options[$row['type'] ?? ''] ?? $row['type'] ?? '')) . '' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - if ($st === 'pending') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('同意', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - echo ''; - echo '
'; - } - if ($st === 'processing') { - echo '
'; - echo ''; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_after_sale_action_' . $id); - submit_button(__('确认收货', 'aksrt'), 'small', '', false); - echo '
'; - } - echo '
'; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_update_after_sale_' . $id); - echo ''; - echo ''; - submit_button(__('更新', 'aksrt'), 'small', '', false); - echo '
'; - echo '
'; - $this->render_pagination($pages, $page_num); - $this->render_footer(); - } - - public function handle_update_after_sale(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - check_admin_referer('aksrt_pay_update_after_sale_' . $id); - - $ok = false; - if ($id > 0 && $new_status !== '' && class_exists('AKSRT_After_Sale')) { - $ok = AKSRT_After_Sale::get_instance()->update_status($id, $new_status, get_current_user_id(), $note); - } - wp_safe_redirect($this->page_url('aksrt_after_sale', ['aksrt_pay_msg' => $ok ? 'after_sale_updated' : 'after_sale_failed'])); - exit; - } - - public function handle_after_sale_action(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - $id = isset($_POST['ticket_id']) ? (int) $_POST['ticket_id'] : 0; - $as_action = isset($_POST['as_action']) ? sanitize_key((string) wp_unslash($_POST['as_action'])) : ''; - check_admin_referer('aksrt_pay_after_sale_action_' . $id); - - $ok = false; - if ($id > 0 && class_exists('AKSRT_After_Sale')) { - $svc = AKSRT_After_Sale::get_instance(); - $operator_id = get_current_user_id(); - if ($as_action === 'approve') { - $ok = $svc->merchant_approve($id, $operator_id); - } elseif ($as_action === 'reject') { - $ok = $svc->merchant_reject($id, $operator_id, __('商家拒绝', 'aksrt')); - } elseif ($as_action === 'confirm_return') { - $ok = $svc->merchant_confirm_return($id, $operator_id); - } - } - - wp_safe_redirect($this->page_url('aksrt_after_sale', [ - 'aksrt_pay_msg' => $ok ? 'after_sale_action_ok' : 'after_sale_action_failed', - ])); - exit; - } - /* ------------------------------------------------------------------ * Income / rebate * ----------------------------------------------------------------*/ @@ -1808,103 +984,29 @@ class AKSRT_Admin_Pay_Menus { /** * @return array */ + private function shipping_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); + if (class_exists('AKSRT_Admin_Pay_Shipping')) { + return AKSRT_Admin_Pay_Shipping::get_instance()->shipping_status_options_public($include_all); } - if (class_exists('AKSRT_Shipping')) { - foreach (AKSRT_Shipping::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - if (count($options) === (int) $include_all) { - $options = array_merge($options, [ - 'shipped' => __('已发货', 'aksrt'), - 'in_transit' => __('运输中', 'aksrt'), - 'delivered' => __('已签收', 'aksrt'), - 'returned' => __('已退回', 'aksrt'), - ]); - } - return $options; + return $include_all ? ['' => __('全部状态', 'aksrt')] : []; } - /** - * @return array - */ - private function after_sale_status_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部状态', 'aksrt'); - } - if (class_exists('AKSRT_After_Sale')) { - foreach (AKSRT_After_Sale::get_status_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $options[$value] = (string) ($item['label'] ?? $value); - } - } - return $options; - } - - /** - * @return array - */ private function after_sale_type_options(bool $include_all = false): array { - $options = []; - if ($include_all) { - $options[''] = __('全部类型', 'aksrt'); + if (class_exists('AKSRT_Admin_Pay_After_Sale')) { + return AKSRT_Admin_Pay_After_Sale::get_instance()->after_sale_type_options_public($include_all); } - // Keep operational create options broader than public get_type_options(). - $defaults = [ - 'refund' => __('仅退款', 'aksrt'), - 'refund_return' => __('退货退款', 'aksrt'), - 'exchange' => __('换货', 'aksrt'), - 'repair' => __('维修', 'aksrt'), + return $include_all ? ['' => __('全部类型', 'aksrt')] : [ + 'refund' => __('仅退款', 'aksrt'), ]; - if (class_exists('AKSRT_After_Sale')) { - foreach (AKSRT_After_Sale::get_type_options() as $item) { - $value = (string) ($item['value'] ?? ''); - if ($value === '') { - continue; - } - $defaults[$value] = (string) ($item['label'] ?? $value); - } - } - return array_merge($options, $defaults); } private function render_carrier_field(string $selected = ''): void { - $options = []; - if (class_exists('AKSRT_Shipping')) { - foreach (AKSRT_Shipping::get_carrier_options() as $item) { - $value = (string) ($item['value'] ?? ''); - $label = (string) ($item['label'] ?? $value); - if ($value !== '') { - $options[$value] = $label; - } - } - } - if ($options) { - echo ''; + if (class_exists('AKSRT_Admin_Pay_Shipping')) { + AKSRT_Admin_Pay_Shipping::get_instance()->render_carrier_field_public($selected); return; } - echo ''; + echo ''; } private function render_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void { @@ -1942,35 +1044,6 @@ class AKSRT_Admin_Pay_Menus { } } - private function render_express_timeline_panel(int $shipping_id): void { - if ($shipping_id <= 0 || !class_exists('AKSRT_Shipping')) { - echo '

' . esc_html__('无法加载物流轨迹。', 'aksrt') . '

'; - return; - } - $svc = AKSRT_Shipping::get_instance(); - // Prefer cached data; do not force remote query on pure view. - $timeline = $svc->build_express_timeline($shipping_id); - $record = $svc->get($shipping_id); - echo '
'; - echo '

' . esc_html__('物流轨迹', 'aksrt') . ' · '; - echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? '')); - echo '

'; - if (!$timeline) { - echo '

' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '

'; - echo '
'; - return; - } - echo '
    '; - // newest first for operators - $timeline = array_reverse($timeline); - foreach ($timeline as $item) { - $time = (string) ($item['time'] ?? ''); - $label = (string) ($item['label'] ?? ''); - echo '
  1. ' . esc_html($time) . ' ' . esc_html($label) . '
  2. '; - } - echo '
'; - } - private function render_pagination(int $pages, int $page_num): void { if ($pages <= 1) { return; diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-shipping.php b/inc/classes/admin/native/class-aksrt-admin-pay-shipping.php new file mode 100644 index 00000000..405e4649 --- /dev/null +++ b/inc/classes/admin/native/class-aksrt-admin-pay-shipping.php @@ -0,0 +1,509 @@ +ensure_cap(); + $this->render_header(__('发货及物流', 'aksrt')); + + if (!class_exists('AKSRT_Shipping')) { + echo '

' . esc_html__('物流模块未初始化。', 'aksrt') . '

'; + $this->render_footer(); + return; + } + + $view = isset($_GET['view']) ? sanitize_key((string) wp_unslash($_GET['view'])) : 'records'; + if (!in_array($view, ['records', 'pending'], true)) { + $view = 'records'; + } + $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; + $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; + $page_num = max(1, (int) ($_GET['paged'] ?? 1)); + $per_page = 20; + $svc = AKSRT_Shipping::get_instance(); + $stats = $svc->get_stats(); + $status_options = $this->shipping_status_options(true); + + $pending_total = 0; + if (class_exists('AKSRT_Order_Repository')) { + $pending_total = (int) (AKSRT_Order_Repository::get_instance()->get_pending_shipment_orders(['limit' => 1, 'search' => ''])['total'] ?? 0); + } + + echo '

' . esc_html(sprintf( + /* translators: 1: shipping records 2: pending shipment orders */ + __('物流记录 %1$d 条 · 待发货订单 %2$d', 'aksrt'), + (int) ($stats['total'] ?? 0), + $pending_total + )) . '

'; + + echo ''; + + if ($view === 'pending') { + $this->render_pending_shipment_board($search, $page_num, $per_page); + $this->render_footer(); + return; + } + + echo '

' . esc_html__('新建发货', 'aksrt') . '

'; + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_create_shipping'); + echo '

'; + echo '

'; + echo '

'; + echo '

'; + submit_button(__('创建发货', 'aksrt'), 'primary', '', false); + echo '
'; + + echo '
'; + echo ''; + echo ''; + echo ' '; + echo ' '; + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo '
'; + + $args = [ + 'status' => $status, + 'limit' => $per_page, + 'offset' => ($page_num - 1) * $per_page, + ]; + if ($search !== '') { + if (preg_match('/^[A-Za-z0-9_-]+$/', $search)) { + $args['order_number'] = $search; + } else { + $args['tracking_no'] = $search; + } + } + $result = $svc->get_list($args); + $items = $result['items'] ?? []; + $total = (int) ($result['total'] ?? 0); + $pages = max(1, (int) ceil(max(1, $total) / $per_page)); + $row_status_options = $this->shipping_status_options(false); + $show_express_id = isset($_GET['express_id']) ? (int) $_GET['express_id'] : 0; + + echo ''; + echo ''; + echo ''; + if (!$items) { + echo ''; + } + foreach ($items as $row) { + $id = (int) ($row['id'] ?? 0); + $st = (string) ($row['status'] ?? ''); + $order_number = (string) ($row['order_number'] ?? ''); + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + if ($show_express_id === $id) { + echo ''; + } + } + echo '
ID' . esc_html__('订单号', 'aksrt') . '' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无物流记录', 'aksrt') . '
' . esc_html((string) $id) . '' . esc_html($order_number) . '' . esc_html((string) ($row['carrier'] ?? '')) . '' . esc_html((string) ($row['tracking_no'] ?? '')) . '' . esc_html($row_status_options[$st] ?? $st) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_update_shipping_' . $id); + echo ''; + echo ''; + submit_button(__('更新', 'aksrt'), 'small', '', false); + echo '
'; + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_query_express_' . $id); + submit_button(__('查物流', 'aksrt'), 'small', '', false); + echo '
'; + echo '' . esc_html__('轨迹', 'aksrt') . ''; + echo '
'; + $this->render_express_timeline_panel($id); + echo '
'; + $this->render_pagination($pages, $page_num); + $this->render_footer(); + } + + private function render_pending_shipment_board(string $search, int $page_num, int $per_page): void { + if (!class_exists('AKSRT_Order_Repository')) { + echo '

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

'; + return; + } + + echo '
'; + echo ''; + echo ''; + echo ' '; + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo '
'; + + $result = AKSRT_Order_Repository::get_instance()->get_pending_shipment_orders([ + 'search' => $search, + 'limit' => $per_page, + 'offset' => ($page_num - 1) * $per_page, + ]); + $items = $result['items'] ?? []; + $total = (int) ($result['total'] ?? 0); + $pages = max(1, (int) ($result['max_pages'] ?? 1)); + $labels = class_exists('AKSRT_Domain_Model') + ? AKSRT_Domain_Model::order_status_labels() + : []; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if (!$items) { + echo ''; + } + + foreach ($items as $row) { + $order_number = (string) ($row['order_number'] ?? ''); + $user_id = (int) ($row['user_id'] ?? 0); + $user = $user_id > 0 ? get_userdata($user_id) : null; + $status_key = (string) ($row['status'] ?? ''); + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
' . esc_html__('订单号', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('金额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('支付时间', 'aksrt') . '' . esc_html__('立即发货', 'aksrt') . '
' . esc_html__('暂无待发货订单', 'aksrt') . '
' . esc_html($order_number) . '' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '' . esc_html((string) ($row['product_type'] ?? '')) . '' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '' . esc_html($labels[$status_key] ?? $status_key) . '' . esc_html((string) ($row['paid_at'] ?? $row['created_at'] ?? '')) . ''; + echo '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); + $this->render_carrier_field(''); + echo ''; + echo ''; + submit_button(__('发货', 'aksrt'), 'primary small', '', false); + echo '
'; + echo '
'; + $this->render_pagination($pages, $page_num); + } + + public function handle_update_shipping(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; + $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : ''; + $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; + check_admin_referer('aksrt_pay_update_shipping_' . $id); + + $ok = false; + if ($id > 0 && $new_status !== '' && class_exists('AKSRT_Shipping')) { + $ok = AKSRT_Shipping::get_instance()->update_status($id, $new_status, $note, get_current_user_id()); + } + + $redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : ''; + if ($redirect_order !== '') { + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'order' => $redirect_order, + 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', + ])); + exit; + } + + wp_safe_redirect($this->page_url('aksrt_shipping', [ + 'view' => 'records', + 'aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed', + ])); + exit; + } + + public function handle_query_express(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $id = isset($_POST['shipping_id']) ? (int) $_POST['shipping_id'] : 0; + $force = !empty($_POST['force']); + check_admin_referer('aksrt_pay_query_express_' . $id); + + $ok = false; + $order_number = ''; + if ($id > 0 && class_exists('AKSRT_Shipping')) { + $svc = AKSRT_Shipping::get_instance(); + $record = $svc->get($id); + $order_number = is_array($record) ? (string) ($record['order_number'] ?? '') : ''; + $result = $svc->query_express($id, $force); + $ok = !empty($result['success']); + } + + wp_safe_redirect($this->page_url('aksrt_shipping', [ + 'view' => 'records', + 'express_id' => $id, + 's' => $order_number, + 'aksrt_pay_msg' => $ok ? 'express_ok' : 'express_failed', + ])); + exit; + } + + public function handle_create_shipping(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; + $redirect_page = isset($_POST['redirect_page']) ? sanitize_key((string) wp_unslash($_POST['redirect_page'])) : 'order'; + $nonce_action = $redirect_page === 'shipping' ? 'aksrt_pay_create_shipping' : ('aksrt_pay_create_shipping_' . $order_number); + check_admin_referer($nonce_action); + + $ok = false; + if ($order_number !== '' && class_exists('AKSRT_Shipping')) { + if (class_exists('AKSRT_Order_Repository')) { + $order = AKSRT_Order_Repository::get_instance()->get($order_number); + if (!$order) { + $this->redirect_shipping_create_result(false, $order_number, $redirect_page); + } + } + $id = AKSRT_Shipping::get_instance()->create([ + 'order_number' => $order_number, + 'carrier' => sanitize_text_field((string) wp_unslash($_POST['carrier'] ?? '')), + 'tracking_no' => sanitize_text_field((string) wp_unslash($_POST['tracking_no'] ?? '')), + 'note' => sanitize_textarea_field((string) wp_unslash($_POST['note'] ?? '')), + 'operator_id' => get_current_user_id(), + ]); + $ok = $id > 0; + } + + $this->redirect_shipping_create_result($ok, $order_number, $redirect_page); + } + + private function redirect_shipping_create_result(bool $ok, string $order_number, string $redirect_page): void { + $msg = $ok ? 'shipping_created' : 'shipping_create_failed'; + if ($redirect_page === 'shipping') { + wp_safe_redirect($this->page_url('aksrt_shipping', [ + 'view' => 'records', + 'aksrt_pay_msg' => $msg, + 's' => $order_number, + ])); + exit; + } + if ($redirect_page === 'pending') { + wp_safe_redirect($this->page_url('aksrt_shipping', [ + 'view' => 'pending', + 'aksrt_pay_msg' => $msg, + ])); + exit; + } + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'order' => $order_number, + 'aksrt_pay_msg' => $msg, + ])); + exit; + } + + + public function shipping_status_options_public(bool $include_all = false): array { + return $this->shipping_status_options($include_all); + } + + private function shipping_status_options(bool $include_all = false): array { + $options = []; + if ($include_all) { + $options[''] = __('全部状态', 'aksrt'); + } + if (class_exists('AKSRT_Shipping')) { + foreach (AKSRT_Shipping::get_status_options() as $item) { + $value = (string) ($item['value'] ?? ''); + if ($value === '') { + continue; + } + $options[$value] = (string) ($item['label'] ?? $value); + } + } + if (count($options) === (int) $include_all) { + $options = array_merge($options, [ + 'shipped' => __('已发货', 'aksrt'), + 'in_transit' => __('运输中', 'aksrt'), + 'delivered' => __('已签收', 'aksrt'), + 'returned' => __('已退回', 'aksrt'), + ]); + } + return $options; + } + public function render_carrier_field_public(string $selected = ''): void { + $this->render_carrier_field($selected); + } + + private function render_carrier_field(string $selected = ''): void { + $options = []; + if (class_exists('AKSRT_Shipping')) { + foreach (AKSRT_Shipping::get_carrier_options() as $item) { + $value = (string) ($item['value'] ?? ''); + $label = (string) ($item['label'] ?? $value); + if ($value !== '') { + $options[$value] = $label; + } + } + } + if ($options) { + echo ''; + return; + } + echo ''; + } + private function render_express_timeline_panel(int $shipping_id): void { + if ($shipping_id <= 0 || !class_exists('AKSRT_Shipping')) { + echo '

' . esc_html__('无法加载物流轨迹。', 'aksrt') . '

'; + return; + } + $svc = AKSRT_Shipping::get_instance(); + // Prefer cached data; do not force remote query on pure view. + $timeline = $svc->build_express_timeline($shipping_id); + $record = $svc->get($shipping_id); + echo '
'; + echo '

' . esc_html__('物流轨迹', 'aksrt') . ' · '; + echo esc_html((string) ($record['carrier'] ?? '')) . ' / ' . esc_html((string) ($record['tracking_no'] ?? '')); + echo '

'; + if (!$timeline) { + echo '

' . esc_html__('暂无轨迹,可点击「查物流」拉取快递 API。', 'aksrt') . '

'; + echo '
'; + return; + } + echo '
    '; + // newest first for operators + $timeline = array_reverse($timeline); + foreach ($timeline as $item) { + $time = (string) ($item['time'] ?? ''); + $label = (string) ($item['label'] ?? ''); + echo '
  1. ' . esc_html($time) . ' ' . esc_html($label) . '
  2. '; + } + echo '
'; + } + + + private function ensure_cap(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); + } + } + + private function render_header(string $title): void { + echo '
'; + echo '

' . esc_html($title) . '

'; + $this->render_flash(); + } + + private function render_footer(): void { + echo '
'; + } + + private function render_flash(): void { + if (empty($_GET['aksrt_pay_msg'])) { + return; + } + $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); + $map = [ + 'shipping_updated' => __('物流状态已更新。', 'aksrt'), + 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), + 'shipping_created' => __('发货记录已创建。', 'aksrt'), + 'shipping_create_failed' => __('发货记录创建失败。', 'aksrt'), + 'express_ok' => __('物流轨迹已更新。', 'aksrt'), + 'express_failed' => __('物流轨迹查询失败。', 'aksrt'), + ]; + if (!isset($map[$code])) { + return; + } + $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; + echo '

' . esc_html($map[$code]) . '

'; + } + + private function page_url(string $slug, array $args = []): string { + return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); + } + + private function render_pagination(int $pages, int $page_num): void { + if ($pages <= 1) { + return; + } + echo '
'; + echo paginate_links([ + 'base' => add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page_num, + ]); + echo '
'; + } +} diff --git a/inc/classes/forum/class-aksrt-plate-meta-box.php b/inc/classes/forum/class-aksrt-plate-meta-box.php index b649d081..336e131c 100644 --- a/inc/classes/forum/class-aksrt-plate-meta-box.php +++ b/inc/classes/forum/class-aksrt-plate-meta-box.php @@ -87,8 +87,11 @@ class AKSRT_Plate_Meta_Box { $add_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); $add_roles = is_array($add_roles) ? array_map('sanitize_key', $add_roles) : []; $wp_roles = $this->wp_role_options(); + $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); + $sort_order = (int) get_post_meta($plate_id, '_aksrt_sort_order', true); $this->render_access_fields($allow_view, $add_limit, $view_roles, $add_roles, $wp_roles); + $this->render_display_fields($icon, $sort_order); $this->render_moderator_fields($moderator_ids, $roles_map, $author_id); $this->render_applications($applications); } @@ -148,6 +151,22 @@ class AKSRT_Plate_Meta_Box { +

+ + + +

+

+ + + +

+
+ $moderator_ids * @param array $roles_map @@ -261,10 +280,23 @@ class AKSRT_Plate_Meta_Box { } $this->save_access_meta($post_id); + $this->save_display_meta($post_id); $this->save_moderators($post_id, (int) $post->post_author); $this->save_applications(); } + private function save_display_meta(int $post_id): void { + $icon = isset($_POST['aksrt_plate_icon']) + ? sanitize_text_field((string) wp_unslash($_POST['aksrt_plate_icon'])) + : ''; + update_post_meta($post_id, '_aksrt_icon', $icon); + + $sort = isset($_POST['aksrt_plate_sort_order']) + ? (int) wp_unslash($_POST['aksrt_plate_sort_order']) + : 0; + update_post_meta($post_id, '_aksrt_sort_order', $sort); + } + private function save_access_meta(int $post_id): void { $allow_view = isset($_POST['aksrt_plate_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_allow_view'])) : 'public'; if (!in_array($allow_view, self::ALLOW_VIEW, true)) {