diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md index 1d51d372..bafe9f1c 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 | done — native ops shell + plate follow-pay + topic pay/points/VIP1-2/partial-hide + sales report + download logs + hidecontent + users commerce + order user/method filters + post pay_type filter + income/rebate user filter + topic virtual_sales/coupon meta | -| 5 remaining | optional: multi-gateway cashier for forum topics; full bulk-edit of content pay fields | +| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter | +| 5 remaining | optional: multi-gateway cashier for forum topics (alipay/wechat) | ## Dual-UI rule diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-orders.php b/inc/classes/admin/native/class-aksrt-admin-pay-orders.php index dc697982..13436196 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-orders.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-orders.php @@ -29,6 +29,8 @@ class AKSRT_Admin_Pay_Orders { } 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_clear_closed_orders', [$this, 'handle_clear_closed_orders']); + add_action('admin_post_aksrt_pay_delete_order', [$this, 'handle_delete_order']); } public function render_orders_page(): void { @@ -178,6 +180,16 @@ class AKSRT_Admin_Pay_Orders { } echo ''; + // Hygiene actions (zibll clear_order / delete). + echo '

'; + echo '

'; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_clear_closed_orders'); + submit_button(__('清理 14 天前关闭订单', 'aksrt'), 'secondary', '', false); + echo '
'; + echo '

'; + echo ''; echo ''; echo ''; @@ -226,6 +238,14 @@ class AKSRT_Admin_Pay_Orders { echo ''; submit_button(__('更新', 'aksrt'), 'small', '', false); echo ''; + if (in_array($status_key, ['cancelled', 'expired'], true)) { + echo ''; + echo ''; + echo ''; + wp_nonce_field('aksrt_pay_delete_order_' . $order_number); + submit_button(__('删除', 'aksrt'), 'small', '', false); + echo ''; + } echo ''; echo ''; } @@ -455,7 +475,43 @@ class AKSRT_Admin_Pay_Orders { exit; } + public function handle_clear_closed_orders(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + check_admin_referer('aksrt_pay_clear_closed_orders'); + $days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14; + $deleted = 0; + if (class_exists('AKSRT_Order_Repository')) { + $deleted = AKSRT_Order_Repository::get_instance()->clear_closed_orders($days); + } + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'aksrt_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty', + 'cleared' => $deleted, + ])); + exit; + } + public function handle_delete_order(): 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'])) : ''; + check_admin_referer('aksrt_pay_delete_order_' . $order_number); + $ok = false; + if ($order_number !== '' && class_exists('AKSRT_Order_Repository')) { + $order = AKSRT_Order_Repository::get_instance()->get($order_number); + $status = is_array($order) ? (string) ($order['status'] ?? '') : ''; + // Only allow hard-delete of closed/expired rows (safety). + if (in_array($status, ['cancelled', 'expired'], true)) { + $ok = AKSRT_Order_Repository::get_instance()->delete_order($order_number); + } + } + wp_safe_redirect($this->page_url('aksrt_orders', [ + 'aksrt_pay_msg' => $ok ? 'order_deleted' : 'order_delete_failed', + ])); + exit; + } private function shipping_status_options(bool $include_all = false): array { if (class_exists('AKSRT_Admin_Pay_Shipping')) { @@ -544,6 +600,14 @@ class AKSRT_Admin_Pay_Orders { 'order_failed' => __('订单状态更新失败。', 'aksrt'), 'order_note_updated' => __('订单备注已保存。', 'aksrt'), 'order_note_failed' => __('订单备注保存失败。', 'aksrt'), + 'orders_cleared' => sprintf( + /* translators: %d deleted count */ + __('已清理 %d 条关闭订单。', 'aksrt'), + max(0, (int) ($_GET['cleared'] ?? 0)) + ), + 'orders_clear_empty' => __('没有可清理的关闭订单。', 'aksrt'), + 'order_deleted' => __('订单已删除。', 'aksrt'), + 'order_delete_failed' => __('订单删除失败(仅允许删除已关闭/过期订单)。', 'aksrt'), 'shipping_updated' => __('物流状态已更新。', 'aksrt'), 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), 'shipping_created' => __('发货记录已创建。', 'aksrt'), diff --git a/inc/classes/class-aksrt-order-repository.php b/inc/classes/class-aksrt-order-repository.php index 4f43073e..9fa4b06c 100644 --- a/inc/classes/class-aksrt-order-repository.php +++ b/inc/classes/class-aksrt-order-repository.php @@ -648,6 +648,39 @@ class AKSRT_Order_Repository { return [$where, $params]; } + /** + * Delete closed (cancelled/expired) orders older than $days days. + * Mirrors zibll ZibPay::clear_order(14). + * + * @return int Number of rows deleted. + */ + public function clear_closed_orders(int $days = 14): int { + global $wpdb; + + $days = max(1, min(3650, $days)); + $table = $this->table(); + $cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS)); + // Prefer cancelled_at when set; fall back to created_at for older rows. + $sql = "DELETE FROM {$table} + WHERE status IN ('cancelled', 'expired') + AND COALESCE(NULLIF(cancelled_at, ''), created_at) < %s"; + $deleted = $wpdb->query($wpdb->prepare($sql, $cutoff)); + return $deleted === false ? 0 : (int) $deleted; + } + + /** + * Hard-delete a single order by order_number (admin hygiene). + */ + public function delete_order(string $order_number): bool { + global $wpdb; + $order_number = sanitize_text_field($order_number); + if ($order_number === '') { + return false; + } + $ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']); + return $ok !== false && $ok > 0; + } + /** * 获取订单统计 */ diff --git a/inc/classes/class-aksrt-post-meta-box.php b/inc/classes/class-aksrt-post-meta-box.php index 9a1b7cce..f1e79b84 100644 --- a/inc/classes/class-aksrt-post-meta-box.php +++ b/inc/classes/class-aksrt-post-meta-box.php @@ -381,9 +381,20 @@ class AKSRT_Post_Meta_Box { // §3.8 Quick / Bulk edit — separate code path (merge mode) if (!empty($_POST['aksrt_qe_flag'])) { $this->save_quick_edit($post_id); + // Bulk pay fields may be submitted together with QE flag on bulk form. + $this->save_bulk_pay_fields($post_id); return; } + // Bulk-only pay fields without aksrt_qe_flag (when only pay row used). + if (!empty($_POST['aksrt_bulk_pay_flag'])) { + $this->save_bulk_pay_fields($post_id); + // Continue only if full meta also posted; otherwise done. + if (!isset($_POST['aksrt_post_meta'])) { + return; + } + } + $raw = $_POST['aksrt_post_meta'] ?? []; $sanitized = [ 'cover_images' => isset($raw['cover_images']) ? sanitize_text_field($raw['cover_images']) : '', @@ -653,11 +664,105 @@ class AKSRT_Post_Meta_Box {
+

+ + + + + + +
' . esc_html__('订单号', 'aksrt') . '