From e2f7fcad894f7db66623e4422800f8be41a2af4b Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Jul 2026 02:32:25 +0800 Subject: [PATCH] feat(admin): order detail view, invite-code batching, product tag term fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add native order detail page under 支付中心订单, standalone invite-code batch generate/disable on users.php, and aksrt_product_tag taxonomy form fields. Source only; no SPA build artifacts. --- .../native/class-aksrt-admin-pay-menus.php | 116 ++++++++++- .../native/class-aksrt-admin-users-menus.php | 184 +++++++++++++++++- inc/classes/class-aksrt-shop-term-meta.php | 71 +++++++ 3 files changed, 357 insertions(+), 14 deletions(-) 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 16a9cf10..a49e4336 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php @@ -286,6 +286,13 @@ class AKSRT_Admin_Pay_Menus { return; } + $detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : ''; + if ($detail_order !== '') { + $this->render_order_detail_page($detail_order); + $this->render_footer(); + return; + } + $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)); @@ -353,7 +360,8 @@ class AKSRT_Admin_Pay_Menus { echo '' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . ''; echo '' . esc_html($status_label) . ''; echo '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - echo ''; + echo ''; + echo '' . esc_html__('详情', 'aksrt') . ''; echo '
'; echo ''; echo ''; @@ -392,6 +400,102 @@ class AKSRT_Admin_Pay_Menus { $this->render_footer(); } + private function render_order_detail_page(string $order_number): void { + $repo = AKSRT_Order_Repository::get_instance(); + $order = $repo->get($order_number); + if (!$order) { + echo '

' . esc_html__('订单不存在。', 'aksrt') . '

'; + echo '

' . esc_html__('返回订单列表', 'aksrt') . '

'; + return; + } + + $labels = class_exists('AKSRT_Domain_Model') + ? AKSRT_Domain_Model::order_status_labels() + : []; + $context = $repo->get_admin_order_context($order_number); + $user_id = (int) ($order['user_id'] ?? 0); + $user = $user_id > 0 ? get_userdata($user_id) : null; + $status = (string) ($order['status'] ?? ''); + + $shipping_items = class_exists('AKSRT_Shipping') + ? AKSRT_Shipping::get_instance()->get_by_order($order_number) + : []; + $after_sale = class_exists('AKSRT_After_Sale') + ? (AKSRT_After_Sale::get_instance()->get_list(['order_number' => $order_number, 'limit' => 20])['items'] ?? []) + : []; + + echo '

← ' . esc_html__('返回订单列表', 'aksrt') . '

'; + + echo ''; + $rows = [ + __('订单号', 'aksrt') => $order_number, + __('用户', 'aksrt') => $user ? $user->display_name . ' (#' . $user_id . ')' : (string) $user_id, + __('商品类型', 'aksrt') => (string) ($order['product_type'] ?? ''), + __('商品 ID', 'aksrt') => (string) ($order['product_id'] ?? ''), + __('金额', 'aksrt') => number_format((float) ($order['amount'] ?? 0), 2) . ' ' . (string) ($order['currency'] ?? ''), + __('状态', 'aksrt') => $labels[$status] ?? $status, + __('支付方式', 'aksrt') => (string) ($order['payment_method'] ?? ''), + __('交易号', 'aksrt') => (string) ($order['payment_transaction_id'] ?? ''), + __('创建时间', 'aksrt') => (string) ($order['created_at'] ?? ''), + __('支付时间', 'aksrt') => (string) ($order['paid_at'] ?? ''), + __('备注', 'aksrt') => (string) ($order['note'] ?? ''), + ]; + foreach ($rows as $label => $value) { + echo ''; + } + if (!empty($context)) { + echo ''; + } + echo '
' . esc_html($label) . '' . esc_html((string) $value) . '
' . esc_html__('附加上下文', 'aksrt') . '
' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '
'; + + echo '

' . esc_html__('更新状态', 'aksrt') . '

'; + echo ''; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_update_order_status_' . $order_number); + echo ' '; + submit_button(__('保存状态', 'aksrt'), 'primary', '', false); + echo '
'; + + echo '

' . esc_html__('物流记录', 'aksrt') . '

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

' . esc_html__('暂无物流记录。', 'aksrt') . ' ' . esc_html__('去物流管理', 'aksrt') . '

'; + } else { + echo ''; + foreach ($shipping_items as $item) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
ID' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '
' . esc_html((string) ($item['id'] ?? '')) . '' . esc_html((string) ($item['carrier'] ?? '')) . '' . esc_html((string) ($item['tracking_no'] ?? '')) . '' . esc_html((string) ($item['status'] ?? '')) . '' . esc_html((string) ($item['created_at'] ?? '')) . '
'; + } + + echo '

' . esc_html__('售后工单', 'aksrt') . '

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

' . esc_html__('暂无售后工单。', 'aksrt') . ' ' . esc_html__('去售后管理', 'aksrt') . '

'; + } else { + echo ''; + foreach ($after_sale as $item) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '
' . esc_html((string) ($item['id'] ?? '')) . '' . esc_html((string) ($item['type'] ?? '')) . '' . esc_html((string) ($item['status'] ?? '')) . '' . esc_html((string) ($item['created_at'] ?? '')) . '
'; + } + } + public function handle_update_order_status(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('权限不足', 'aksrt')); @@ -406,9 +510,13 @@ class AKSRT_Admin_Pay_Menus { $ok = AKSRT_Order_Repository::get_instance()->update_status($order_number, $new_status, ['force' => true]); } - wp_safe_redirect($this->page_url('aksrt_orders', [ - 'aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed', - ])); + $args = ['aksrt_pay_msg' => $ok ? 'order_updated' : 'order_failed']; + $redirect_order = isset($_POST['redirect_order']) ? sanitize_text_field((string) wp_unslash($_POST['redirect_order'])) : ''; + if ($redirect_order !== '') { + $args['order'] = $redirect_order; + } + + wp_safe_redirect($this->page_url('aksrt_orders', $args)); exit; } diff --git a/inc/classes/admin/native/class-aksrt-admin-users-menus.php b/inc/classes/admin/native/class-aksrt-admin-users-menus.php index e0b63f6f..5d7150c1 100644 --- a/inc/classes/admin/native/class-aksrt-admin-users-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-users-menus.php @@ -32,6 +32,8 @@ class AKSRT_Admin_Users_Menus { add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']); add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']); add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']); + add_action('admin_post_aksrt_users_batch_invite_codes', [$this, 'handle_batch_invite_codes']); + add_action('admin_post_aksrt_users_disable_invite_code', [$this, 'handle_disable_invite_code']); } public function register_menus(): void { @@ -120,15 +122,19 @@ class AKSRT_Admin_Users_Menus { } $code = sanitize_key((string) wp_unslash($_GET['aksrt_users_msg'])); $map = [ - 'message_sent' => __('消息已发送。', 'aksrt'), - 'message_failed' => __('消息发送失败。', 'aksrt'), - 'verify_updated' => __('认证状态已更新。', 'aksrt'), - 'verify_failed' => __('认证审核失败。', 'aksrt'), - 'report_updated' => __('举报状态已更新。', 'aksrt'), - 'report_failed' => __('举报更新失败。', 'aksrt'), - 'restriction_added' => __('限制已添加。', 'aksrt'), + 'message_sent' => __('消息已发送。', 'aksrt'), + 'message_failed' => __('消息发送失败。', 'aksrt'), + 'verify_updated' => __('认证状态已更新。', 'aksrt'), + 'verify_failed' => __('认证审核失败。', 'aksrt'), + 'report_updated' => __('举报状态已更新。', 'aksrt'), + 'report_failed' => __('举报更新失败。', 'aksrt'), + 'restriction_added' => __('限制已添加。', 'aksrt'), 'restriction_removed' => __('限制已移除。', 'aksrt'), - 'restriction_failed'=> __('限制操作失败。', 'aksrt'), + 'restriction_failed' => __('限制操作失败。', 'aksrt'), + 'invite_batch_ok' => __('邀请码批量生成完成。', 'aksrt'), + 'invite_batch_failed' => __('邀请码批量生成失败。', 'aksrt'), + 'invite_disabled' => __('邀请码已禁用。', 'aksrt'), + 'invite_failed' => __('邀请码操作失败。', 'aksrt'), ]; if (!isset($map[$code])) { return; @@ -233,8 +239,68 @@ class AKSRT_Admin_Users_Menus { echo '

' . esc_html__('邀请码管理', 'aksrt') . '

'; $this->flash(); - $spa = admin_url('admin.php?page=aksrt-settings#/users/invite-codes'); - echo '

' . esc_html__('SPA 批量生成(过渡)', 'aksrt') . '

'; + echo '

' . esc_html__('批量生成独立邀请码', 'aksrt') . '

'; + echo '
'; + echo ''; + wp_nonce_field('aksrt_users_batch_invite_codes'); + echo '

'; + echo '

'; + echo '

'; + echo '

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

' . esc_html(sprintf( + /* translators: %d count */ + __('成功生成 %d 个邀请码。', 'aksrt'), + (int) $_GET['aksrt_generated'] + )) . '

'; + } + + $standalone = get_option('aksrt_standalone_invite_codes', []); + $standalone = is_array($standalone) ? array_reverse(array_values($standalone)) : []; + + echo '

' . esc_html__('独立邀请码', 'aksrt') . '

'; + echo ''; + echo ''; + echo ''; + if (!$standalone) { + echo ''; + } + $shown = 0; + foreach ($standalone as $entry) { + if ($shown >= 200) { + break; + } + $code = sanitize_key((string) ($entry['code'] ?? '')); + if ($code === '') { + continue; + } + $disabled = !empty($entry['disabled']); + $used = !empty($entry['used']) || !empty($entry['used_by']); + $status = $disabled ? __('已禁用', 'aksrt') : ($used ? __('已使用', 'aksrt') : __('可用', 'aksrt')); + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + $shown++; + } + echo '
' . esc_html__('邀请码', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('批次', 'aksrt') . '' . esc_html__('备注', 'aksrt') . '' . esc_html__('创建时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
' . esc_html__('暂无独立邀请码', 'aksrt') . '
' . esc_html($code) . '' . esc_html($status) . '' . esc_html((string) ($entry['batch_id'] ?? '')) . '' . esc_html((string) ($entry['note'] ?? '')) . '' . esc_html((string) ($entry['created_at'] ?? '')) . ''; + if (!$disabled && !$used) { + echo '
'; + echo ''; + echo ''; + wp_nonce_field('aksrt_users_disable_invite_code_' . $code); + submit_button(__('禁用', 'aksrt'), 'small', '', false); + echo '
'; + } else { + echo '—'; + } + echo '
'; global $wpdb; $rows = $wpdb->get_results( @@ -269,6 +335,104 @@ class AKSRT_Admin_Users_Menus { echo '
'; } + public function handle_batch_invite_codes(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + check_admin_referer('aksrt_users_batch_invite_codes'); + + $count = isset($_POST['count']) ? max(1, min(100, (int) $_POST['count'])) : 10; + $length = isset($_POST['length']) ? max(4, min(12, (int) $_POST['length'])) : 6; + $prefix = isset($_POST['prefix']) ? sanitize_key((string) wp_unslash($_POST['prefix'])) : ''; + $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; + $batch = 'INV-' . gmdate('YmdHis') . '-' . wp_generate_password(4, false, false); + $alphabet = 'abcdefghjkmnpqrstuvwxyz23456789'; + + $codes = get_option('aksrt_standalone_invite_codes', []); + $codes = is_array($codes) ? array_values($codes) : []; + $existing = []; + foreach ($codes as $entry) { + $c = sanitize_key((string) ($entry['code'] ?? '')); + if ($c !== '') { + $existing[$c] = true; + } + } + + global $wpdb; + $generated = []; + for ($i = 0; $i < $count; $i++) { + $code = ''; + for ($attempt = 0; $attempt < 20; $attempt++) { + $candidate = $prefix; + for ($j = 0; $j < $length; $j++) { + $candidate .= $alphabet[random_int(0, strlen($alphabet) - 1)]; + } + $candidate = sanitize_key($candidate); + if ($candidate === '' || isset($existing[$candidate])) { + continue; + } + $user_hit = $wpdb->get_var($wpdb->prepare( + "SELECT umeta_id FROM {$wpdb->usermeta} WHERE meta_key = 'aksrt_invite_code' AND meta_value = %s LIMIT 1", + $candidate + )); + if ($user_hit) { + continue; + } + $code = $candidate; + break; + } + if ($code === '') { + continue; + } + $existing[$code] = true; + $codes[] = [ + 'code' => $code, + 'created_at' => current_time('mysql'), + 'used' => false, + 'used_by' => 0, + 'used_at' => '', + 'disabled' => false, + 'note' => $note, + 'batch_id' => $batch, + 'created_by' => get_current_user_id(), + ]; + $generated[] = $code; + } + + update_option('aksrt_standalone_invite_codes', array_values($codes), false); + $ok = !empty($generated); + $this->redirect('aksrt_invite_codes', $ok ? 'invite_batch_ok' : 'invite_batch_failed', [ + 'aksrt_generated' => count($generated), + ]); + } + + public function handle_disable_invite_code(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + $code = isset($_POST['code']) ? sanitize_key((string) wp_unslash($_POST['code'])) : ''; + check_admin_referer('aksrt_users_disable_invite_code_' . $code); + + $ok = false; + if ($code !== '') { + $codes = get_option('aksrt_standalone_invite_codes', []); + $codes = is_array($codes) ? $codes : []; + foreach ($codes as $i => $entry) { + if (sanitize_key((string) ($entry['code'] ?? '')) !== $code) { + continue; + } + $entry['disabled'] = true; + $codes[$i] = $entry; + $ok = true; + break; + } + if ($ok) { + update_option('aksrt_standalone_invite_codes', array_values($codes), false); + } + } + $this->redirect('aksrt_invite_codes', $ok ? 'invite_disabled' : 'invite_failed'); + } + /* ------------------------------------------------------------------ * Verify * ----------------------------------------------------------------*/ diff --git a/inc/classes/class-aksrt-shop-term-meta.php b/inc/classes/class-aksrt-shop-term-meta.php index b6873863..d8f13c57 100644 --- a/inc/classes/class-aksrt-shop-term-meta.php +++ b/inc/classes/class-aksrt-shop-term-meta.php @@ -45,6 +45,12 @@ class AKSRT_Shop_Term_Meta { add_action('aksrt_product_cat_edit_form_fields', [$this, 'render_cat_edit_fields'], 10, 1); add_action('created_aksrt_product_cat', [$this, 'save_cat_term_fields']); add_action('edited_aksrt_product_cat', [$this, 'save_cat_term_fields']); + + // Product tag display fields. + add_action('aksrt_product_tag_add_form_fields', [$this, 'render_tag_add_fields']); + add_action('aksrt_product_tag_edit_form_fields', [$this, 'render_tag_edit_fields'], 10, 1); + add_action('created_aksrt_product_tag', [$this, 'save_tag_term_fields']); + add_action('edited_aksrt_product_tag', [$this, 'save_tag_term_fields']); } // ─── Taxonomy Registration ─────────────────────────────────── @@ -677,4 +683,69 @@ class AKSRT_Shop_Term_Meta { update_term_meta($term_id, $key, $value); } } + + public function render_tag_add_fields(): void { + foreach ($this->get_tag_field_definitions() as $key => $def) { + echo '
'; + echo ''; + $this->render_tag_field_control($key, $def['default'] ?? ''); + echo '
'; + } + } + + public function render_tag_edit_fields($term): void { + if (!($term instanceof WP_Term)) { + return; + } + foreach ($this->get_tag_field_definitions() as $key => $def) { + $value = get_term_meta($term->term_id, $key, true); + if ($value === '' || $value === false) { + $value = $def['default'] ?? ''; + } + echo ''; + $this->render_tag_field_control($key, $value); + echo ''; + } + } + + /** + * @param mixed $value + */ + private function render_tag_field_control(string $key, $value): void { + $val = (string) $value; + if ($key === 'shop_tag_is_important') { + echo ''; + return; + } + $type = $key === 'shop_tag_priority' ? 'number' : 'text'; + printf( + '', + esc_attr($type), + esc_attr($key), + esc_attr($key), + esc_attr($val) + ); + } + + public function save_tag_term_fields(int $term_id): void { + if (!current_user_can('manage_categories')) { + return; + } + foreach ($this->get_tag_field_definitions() as $key => $def) { + if ($key === 'shop_tag_is_important') { + $raw = isset($_POST[$key]) ? '1' : ''; + } else { + if (!isset($_POST[$key])) { + continue; + } + $raw = wp_unslash($_POST[$key]); + } + if (isset($def['sanitize']) && is_callable($def['sanitize'])) { + $value = call_user_func($def['sanitize'], $raw); + } else { + $value = sanitize_text_field((string) $raw); + } + update_term_meta($term_id, $key, $value); + } + } }