diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md index 72ed3a70..472164bf 100644 --- a/docs/zibll-gap-matrix.md +++ b/docs/zibll-gap-matrix.md @@ -99,7 +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 | backlog | +| 5+ behavior parity | in progress — shipping list create + order links; after-sale `s`/type filters + list create + merchant approve/reject/confirm + order links; order admin note (JSON-safe) + complete/refund/close quick actions | +| 5 remaining | coupons/cards UX depth; product/forum runtime field parity; pending-to-ship board; express timeline | ## Dual-UI rule 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 34f523b9..26d453e7 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php @@ -31,6 +31,7 @@ class AKSRT_Admin_Pay_Menus { add_action('admin_menu', [$this, 'register_menus']); add_action('admin_notices', [$this, 'pending_notices']); 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']); @@ -38,6 +39,7 @@ class AKSRT_Admin_Pay_Menus { 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']); add_action('admin_post_aksrt_pay_create_coupon', [$this, 'handle_create_coupon']); add_action('admin_post_aksrt_pay_disable_coupon', [$this, 'handle_disable_coupon']); add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']); @@ -196,6 +198,10 @@ class AKSRT_Admin_Pay_Menus { $map = [ 'order_updated' => __('订单状态已更新。', 'aksrt'), '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'), @@ -368,6 +374,7 @@ class AKSRT_Admin_Pay_Menus { echo '' . esc_html((string) ($row['created_at'] ?? '')) . ''; echo ''; echo '' . esc_html__('详情', 'aksrt') . ''; + $this->render_order_quick_status_buttons($order_number, $status_key); echo '
'; echo ''; echo ''; @@ -422,6 +429,12 @@ class AKSRT_Admin_Pay_Menus { $user_id = (int) ($order['user_id'] ?? 0); $user = $user_id > 0 ? get_userdata($user_id) : null; $status = (string) ($order['status'] ?? ''); + $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; + $customer_note = (string) ($context['customer_note'] ?? $order['note_text'] ?? ''); + $admin_note = is_array($meta) ? (string) ($meta['admin_note'] ?? '') : ''; + if ($admin_note === '' && $customer_note === '' && !empty($order['note']) && !is_array(json_decode((string) $order['note'], true))) { + $admin_note = (string) $order['note']; + } $shipping_items = class_exists('AKSRT_Shipping') ? AKSRT_Shipping::get_instance()->get_by_order($order_number) @@ -444,7 +457,7 @@ class AKSRT_Admin_Pay_Menus { __('交易号', 'aksrt') => (string) ($order['payment_transaction_id'] ?? ''), __('创建时间', 'aksrt') => (string) ($order['created_at'] ?? ''), __('支付时间', 'aksrt') => (string) ($order['paid_at'] ?? ''), - __('备注', 'aksrt') => (string) ($order['note'] ?? ''), + __('买家备注', 'aksrt') => $customer_note, ]; foreach ($rows as $label => $value) { echo '' . esc_html($label) . '' . esc_html((string) $value) . ''; @@ -454,7 +467,19 @@ class AKSRT_Admin_Pay_Menus { } echo ''; + echo '

' . esc_html__('运营备注', 'aksrt') . '

'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_update_order_note_' . $order_number); + echo '

'; + submit_button(__('保存备注', 'aksrt'), 'secondary', '', false); + echo '
'; + echo '

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

'; + echo '
'; + $this->render_order_quick_status_buttons($order_number, $status, true); + echo '
'; echo '
'; echo ''; echo ''; @@ -472,24 +497,43 @@ class AKSRT_Admin_Pay_Menus { echo ''; echo ''; echo ''; + echo ''; wp_nonce_field('aksrt_pay_create_shipping_' . $order_number); - echo '

'; + echo '

'; echo '

'; echo '

'; submit_button(__('创建发货', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('物流管理', 'aksrt') . ''; + echo ' ' . esc_html__('物流管理', 'aksrt') . ''; echo '
'; if (!$shipping_items) { echo '

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

'; } else { - echo ''; + $status_options = $this->shipping_status_options(false); + echo '
ID' . esc_html__('承运商', 'aksrt') . '' . esc_html__('运单号', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '
'; foreach ($shipping_items as $item) { + $sid = (int) ($item['id'] ?? 0); + $st = (string) ($item['status'] ?? ''); 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((string) ($item['id'] ?? '')) . '' . esc_html((string) $sid) . '' . esc_html((string) ($item['carrier'] ?? '')) . '' . esc_html((string) ($item['tracking_no'] ?? '')) . '' . esc_html((string) ($item['status'] ?? '')) . '' . esc_html($status_options[$st] ?? $st) . '' . esc_html((string) ($item['created_at'] ?? '')) . '
'; + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_update_shipping_' . $sid); + echo ''; + echo ''; + submit_button(__('更新', 'aksrt'), 'small', '', false); + echo '
'; @@ -500,14 +544,10 @@ class AKSRT_Admin_Pay_Menus { echo ''; echo ''; echo ''; + echo ''; wp_nonce_field('aksrt_pay_create_after_sale_' . $order_number); echo '

'; @@ -520,13 +560,15 @@ class AKSRT_Admin_Pay_Menus { if (!$after_sale) { echo '

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

'; } else { - echo ''; + echo '
ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '
'; foreach ($after_sale as $item) { + $tid = (int) ($item['id'] ?? 0); 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((string) ($item['id'] ?? '')) . '' . esc_html((string) ($item['type'] ?? '')) . '' . esc_html((string) ($item['status'] ?? '')) . '' . esc_html((string) $tid) . '' . esc_html((string) ($item['type_label'] ?? $item['type'] ?? '')) . '' . esc_html((string) ($item['status_label'] ?? $item['status'] ?? '')) . '' . esc_html((string) ($item['created_at'] ?? '')) . '' . esc_html__('在售后页处理', 'aksrt') . '
'; @@ -557,9 +599,26 @@ class AKSRT_Admin_Pay_Menus { exit; } - /* ------------------------------------------------------------------ - * Withdrawals - * ----------------------------------------------------------------*/ + public function handle_update_order_note(): 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'])) : ''; + $admin_note = isset($_POST['admin_note']) ? sanitize_textarea_field((string) wp_unslash($_POST['admin_note'])) : ''; + check_admin_referer('aksrt_pay_update_order_note_' . $order_number); + + $ok = false; + if ($order_number !== '' && class_exists('AKSRT_Order_Repository')) { + $ok = AKSRT_Order_Repository::get_instance()->update_admin_note($order_number, $admin_note); + } + + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'order' => $order_number, + 'aksrt_pay_msg' => $ok ? 'order_note_updated' : 'order_note_failed', + ])); + exit; + } public function render_withdrawals_page(): void { $this->ensure_cap(); @@ -960,6 +1019,7 @@ class AKSRT_Admin_Pay_Menus { $per_page = 20; $svc = AKSRT_Shipping::get_instance(); $stats = $svc->get_stats(); + $status_options = $this->shipping_status_options(true); echo '

' . esc_html(sprintf( /* translators: 1: total */ @@ -967,16 +1027,23 @@ class AKSRT_Admin_Pay_Menus { (int) ($stats['total'] ?? 0) )) . '

'; + 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 ''; wp_nonce_field('aksrt_pay_update_shipping_' . $id); echo ''; @@ -1052,6 +1118,16 @@ class AKSRT_Admin_Pay_Menus { 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', ['aksrt_pay_msg' => $ok ? 'shipping_updated' : 'shipping_failed'])); exit; } @@ -1061,10 +1137,18 @@ class AKSRT_Admin_Pay_Menus { wp_die(esc_html__('权限不足', 'aksrt')); } $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - check_admin_referer('aksrt_pay_create_shipping_' . $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'] ?? '')), @@ -1075,9 +1159,18 @@ class AKSRT_Admin_Pay_Menus { $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', ['aksrt_pay_msg' => $msg, 's' => $order_number])); + exit; + } wp_safe_redirect($this->page_url('aksrt_orders', [ 'order' => $order_number, - 'aksrt_pay_msg' => $ok ? 'shipping_created' : 'shipping_create_failed', + 'aksrt_pay_msg' => $msg, ])); exit; } @@ -1087,7 +1180,9 @@ class AKSRT_Admin_Pay_Menus { wp_die(esc_html__('权限不足', 'aksrt')); } $order_number = isset($_POST['order_number']) ? sanitize_text_field((string) wp_unslash($_POST['order_number'])) : ''; - check_admin_referer('aksrt_pay_create_after_sale_' . $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')) { @@ -1107,17 +1202,18 @@ class AKSRT_Admin_Pay_Menus { $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' => $ok ? 'after_sale_created' : 'after_sale_create_failed', + 'aksrt_pay_msg' => $msg, ])); exit; } - /* ------------------------------------------------------------------ - * After-sale - * ----------------------------------------------------------------*/ - public function render_after_sale_page(): void { $this->ensure_cap(); $this->render_header(__('售后管理', 'aksrt')); @@ -1129,10 +1225,17 @@ class AKSRT_Admin_Pay_Menus { } $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 */ @@ -1141,15 +1244,22 @@ class AKSRT_Admin_Pay_Menus { (int) ($stats['pending'] ?? 0) )) . '

'; - $status_options = [ - '' => __('全部状态', 'aksrt'), - 'pending' => __('待处理', 'aksrt'), - 'processing' => __('处理中', 'aksrt'), - 'approved' => __('已通过', 'aksrt'), - 'rejected' => __('已拒绝', 'aksrt'), - 'completed' => __('已完成', 'aksrt'), - 'cancelled' => __('已取消', 'aksrt'), - ]; + 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 ''; @@ -1158,17 +1268,29 @@ class AKSRT_Admin_Pay_Menus { printf('', esc_attr($key), selected($status, $key, false), esc_html($label)); } echo ' '; + echo ' '; + echo ' '; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); echo '
'; - $result = $svc->get_list([ + $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 ''; @@ -1181,28 +1303,53 @@ class AKSRT_Admin_Pay_Menus { $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 ''; 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((string) $id) . '' . esc_html((string) ($row['order_number'] ?? '')) . '' . esc_html((string) ($row['type'] ?? '')) . '' . 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($status_options[$st] ?? $st) . '' . esc_html((string) ($row['status_label'] ?? $row_status_options[$st] ?? $st)) . '' . esc_html((string) ($row['created_at'] ?? '')) . '
'; + echo '
'; + 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 '
'; $this->render_pagination($pages, $page_num); @@ -1226,9 +1373,32 @@ class AKSRT_Admin_Pay_Menus { exit; } - /* ------------------------------------------------------------------ - * Coupons - * ----------------------------------------------------------------*/ + 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; + } public function render_coupons_page(): void { $this->ensure_cap(); @@ -1553,6 +1723,143 @@ class AKSRT_Admin_Pay_Menus { exit; } + /** + * @return array + */ + 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; + } + + /** + * @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'); + } + // 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 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_order_quick_status_buttons(string $order_number, string $current_status, bool $redirect_detail = false): void { + $actions = [ + 'completed' => __('完成', 'aksrt'), + 'refunded' => __('退款', 'aksrt'), + 'cancelled' => __('关闭', 'aksrt'), + ]; + foreach ($actions as $status => $label) { + if ($status === $current_status) { + continue; + } + $confirm = ''; + if ($status === 'refunded') { + $confirm = __('确认将该订单标记为已退款?', 'aksrt'); + } elseif ($status === 'cancelled') { + $confirm = __('确认关闭该订单?', 'aksrt'); + } elseif ($status === 'completed') { + $confirm = __('确认完成该订单?', 'aksrt'); + } + echo '
'; + echo ''; + echo ''; + echo ''; + if ($redirect_detail) { + echo ''; + } + wp_nonce_field('aksrt_pay_update_order_status_' . $order_number); + printf( + '', + $confirm !== '' ? ' onclick="return confirm(\'' . esc_js($confirm) . '\');"' : '', + esc_html($label) + ); + echo '
'; + } + } + private function render_pagination(int $pages, int $page_num): void { if ($pages <= 1) { return; diff --git a/inc/classes/class-aksrt-order-repository.php b/inc/classes/class-aksrt-order-repository.php index 1e1f4eb4..4828070f 100644 --- a/inc/classes/class-aksrt-order-repository.php +++ b/inc/classes/class-aksrt-order-repository.php @@ -232,6 +232,46 @@ class AKSRT_Order_Repository { /** * 更新订单状态(带状态机验证) */ + /** + * Merge operator admin note into order note meta without destroying JSON payload. + */ + public function update_admin_note(string $order_number, string $admin_note): bool { + global $wpdb; + + $order = $this->get($order_number); + if (!$order) { + return false; + } + + $raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : ''; + $decoded = $raw !== '' ? json_decode($raw, true) : null; + $admin_note = sanitize_textarea_field($admin_note); + + if (is_array($decoded)) { + $decoded['admin_note'] = $admin_note; + $new_note = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } elseif ($raw !== '') { + $new_note = wp_json_encode([ + 'customer_note' => $raw, + 'admin_note' => $admin_note, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } else { + $new_note = wp_json_encode([ + 'admin_note' => $admin_note, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $result = $wpdb->update( + $this->table(), + ['note' => $new_note], + ['order_number' => $order_number], + ['%s'], + ['%s'] + ); + + return $result !== false; + } + public function update_status(string $order_number, string $new_status, array $extra = []): bool { global $wpdb;