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 '';
echo '';
echo '| ' . esc_html__('订单号', 'aksrt') . ' | ';
@@ -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 '
';
}
@@ -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 {
+
+
+
+
+
+
+
+