feat(admin): clear closed orders and bulk content-pay edit
Add zibll-style clear/delete for closed orders and bulk/quick fields for post pay_type, modo, prices, and virtual_sales.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 '</form>';
|
||||
|
||||
// Hygiene actions (zibll clear_order / delete).
|
||||
echo '<p style="margin:8px 0 12px;">';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('将删除 14 天前所有已关闭/已过期订单,不可恢复。确认清理?', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_clear_closed_orders" />';
|
||||
echo '<input type="hidden" name="days" value="14" />';
|
||||
wp_nonce_field('aksrt_pay_clear_closed_orders');
|
||||
submit_button(__('清理 14 天前关闭订单', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
echo '</p>';
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th>' . esc_html__('订单号', 'aksrt') . '</th>';
|
||||
@@ -226,6 +238,14 @@ class AKSRT_Admin_Pay_Orders {
|
||||
echo '</select>';
|
||||
submit_button(__('更新', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
if (in_array($status_key, ['cancelled', 'expired'], true)) {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline;" onsubmit="return confirm(\'' . esc_js(__('确认永久删除此订单?不可恢复。', 'aksrt')) . '\');">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_delete_order" />';
|
||||
echo '<input type="hidden" name="order_number" value="' . esc_attr($order_number) . '" />';
|
||||
wp_nonce_field('aksrt_pay_delete_order_' . $order_number);
|
||||
submit_button(__('删除', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
}
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单统计
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
<input type="number" name="aksrt_qe_maxheight" min="0" step="100" value="" class="aksrt-qe-maxheight" placeholder="<?php esc_attr_e('0=不限', 'aksrt'); ?>">
|
||||
</label>
|
||||
<br class="clear">
|
||||
<h4 style="margin-top:10px;"><?php esc_html_e('内容付费(批量)', 'aksrt'); ?></h4>
|
||||
<input type="hidden" name="aksrt_bulk_pay_flag" value="1">
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('付费类型', 'aksrt'); ?></span>
|
||||
<select name="aksrt_bulk_pay_type" class="aksrt-bulk-pay-type">
|
||||
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
|
||||
<?php
|
||||
$pay_labels = class_exists('AKSRT_Domain_Model') ? AKSRT_Domain_Model::pay_type_labels() : [];
|
||||
foreach ($pay_labels as $k => $lab) :
|
||||
?>
|
||||
<option value="<?php echo esc_attr($k); ?>"><?php echo esc_html($lab); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('支付方式', 'aksrt'); ?></span>
|
||||
<select name="aksrt_bulk_pay_modo" class="aksrt-bulk-pay-modo">
|
||||
<option value="-1"><?php esc_html_e('— 不更改 —', 'aksrt'); ?></option>
|
||||
<option value="0"><?php esc_html_e('现金', 'aksrt'); ?></option>
|
||||
<option value="points"><?php esc_html_e('积分', 'aksrt'); ?></option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('现金价', 'aksrt'); ?></span>
|
||||
<input type="number" min="0" step="0.01" name="aksrt_bulk_pay_price" value="" class="aksrt-bulk-pay-price" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
|
||||
</label>
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('积分价', 'aksrt'); ?></span>
|
||||
<input type="number" min="0" step="1" name="aksrt_bulk_points_price" value="" class="aksrt-bulk-points-price" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
|
||||
</label>
|
||||
<label class="alignleft">
|
||||
<span class="title"><?php esc_html_e('虚拟销量', 'aksrt'); ?></span>
|
||||
<input type="number" min="0" step="1" name="aksrt_bulk_virtual_sales" value="" class="aksrt-bulk-virtual-sales" placeholder="<?php esc_attr_e('留空=不改', 'aksrt'); ?>">
|
||||
</label>
|
||||
<br class="clear">
|
||||
</div>
|
||||
</fieldset>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk/quick merge update for content pay meta (_aksrt_pay + denormalized keys).
|
||||
* Only non-sentinel fields are written; omit to keep existing.
|
||||
*/
|
||||
private function save_bulk_pay_fields(int $post_id): void {
|
||||
if (!class_exists('AKSRT_Domain_Model') || !function_exists('aksrt_get_pay_meta')) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type_raw = isset($_POST['aksrt_bulk_pay_type']) ? (string) wp_unslash($_POST['aksrt_bulk_pay_type']) : '-1';
|
||||
$modo_raw = isset($_POST['aksrt_bulk_pay_modo']) ? (string) wp_unslash($_POST['aksrt_bulk_pay_modo']) : '-1';
|
||||
$price_raw = isset($_POST['aksrt_bulk_pay_price']) ? trim((string) wp_unslash($_POST['aksrt_bulk_pay_price'])) : '';
|
||||
$points_raw = isset($_POST['aksrt_bulk_points_price']) ? trim((string) wp_unslash($_POST['aksrt_bulk_points_price'])) : '';
|
||||
$virtual_raw = isset($_POST['aksrt_bulk_virtual_sales']) ? trim((string) wp_unslash($_POST['aksrt_bulk_virtual_sales'])) : '';
|
||||
|
||||
$has_change = ($type_raw !== '' && $type_raw !== '-1')
|
||||
|| ($modo_raw !== '' && $modo_raw !== '-1')
|
||||
|| $price_raw !== ''
|
||||
|| $points_raw !== ''
|
||||
|| $virtual_raw !== '';
|
||||
if (!$has_change) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = aksrt_get_pay_meta($post_id);
|
||||
|
||||
if ($type_raw !== '' && $type_raw !== '-1') {
|
||||
$meta['pay_type'] = AKSRT_Domain_Model::normalize_pay_type(sanitize_key($type_raw));
|
||||
}
|
||||
if ($modo_raw !== '' && $modo_raw !== '-1') {
|
||||
$modo = sanitize_key($modo_raw);
|
||||
$meta['pay_modo'] = ($modo === 'points') ? 'points' : '0';
|
||||
}
|
||||
if ($price_raw !== '' && is_numeric($price_raw)) {
|
||||
$meta['price'] = max(0, (float) $price_raw);
|
||||
}
|
||||
if ($points_raw !== '' && is_numeric($points_raw)) {
|
||||
$meta['points_price'] = max(0, (int) $points_raw);
|
||||
}
|
||||
if ($virtual_raw !== '' && is_numeric($virtual_raw)) {
|
||||
$meta['virtual_sales'] = max(0, (int) $virtual_raw);
|
||||
}
|
||||
|
||||
$meta = AKSRT_Domain_Model::sanitize_pay_meta($meta);
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_META_KEY, $meta);
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_TYPE_KEY, (string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
|
||||
update_post_meta($post_id, AKSRT_Domain_Model::PAY_PRICE_KEY, (float) ($meta['price'] ?? 0));
|
||||
if (class_exists('AKSRT_Domain_Model') && method_exists('AKSRT_Domain_Model', 'is_resource_pay_type')) {
|
||||
update_post_meta(
|
||||
$post_id,
|
||||
AKSRT_Domain_Model::RESOURCE_MARKER_KEY,
|
||||
AKSRT_Domain_Model::is_resource_pay_type((string) ($meta['pay_type'] ?? 'no')) ? 1 : 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* §3.8 Quick / Bulk edit merge-mode save.
|
||||
* Only updates fields that were actually submitted (non-sentinel, non-empty).
|
||||
|
||||
Reference in New Issue
Block a user