diff --git a/functions.php b/functions.php index f2bffc70..e5e06080 100644 --- a/functions.php +++ b/functions.php @@ -307,6 +307,16 @@ if (is_admin() && class_exists('AKSRT_Product_Variants_Meta_Box')) { if (is_admin() && class_exists('AKSRT_Plate_Meta_Box')) { AKSRT_Plate_Meta_Box::get_instance()->init(); } +// 论坛帖子原生列表治理列/筛选/批量 +if (is_admin() && class_exists('AKSRT_Topic_Admin_List')) { + AKSRT_Topic_Admin_List::get_instance()->init(); +} +// 论坛帖子治理 meta box +if (is_admin() && class_exists('AKSRT_Topic_Meta_Box')) { + AKSRT_Topic_Meta_Box::get_instance()->init(); +} + + // 商品评价/评分系统 diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 0cb88453..21fcc75a 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -323,6 +323,8 @@ require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-shop-product-service.php require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-product-meta-box.php'; require_once AKSRT_DIR . '/inc/classes/shop/class-aksrt-product-variants-meta-box.php'; require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-plate-meta-box.php'; +require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-topic-admin-list.php'; +require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-topic-meta-box.php'; require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-1.php'; require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-2.php'; require_once AKSRT_DIR . '/inc/classes/shop/shop-traits/trait-aksrt-shop-part-3.php'; 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 e0fe26c4..16a9cf10 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php @@ -33,6 +33,7 @@ class AKSRT_Admin_Pay_Menus { add_action('admin_post_aksrt_pay_update_order_status', [$this, 'handle_update_order_status']); 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']); add_action('admin_post_aksrt_pay_update_shipping', [$this, 'handle_update_shipping']); add_action('admin_post_aksrt_pay_update_after_sale', [$this, 'handle_update_after_sale']); add_action('admin_post_aksrt_pay_create_coupon', [$this, 'handle_create_coupon']); @@ -197,6 +198,8 @@ class AKSRT_Admin_Pay_Menus { 'withdraw_failed' => __('提现状态更新失败。', 'aksrt'), 'card_disabled' => __('充值卡已禁用。', 'aksrt'), 'card_failed' => __('充值卡禁用失败。', 'aksrt'), + 'card_batch_ok' => __('卡密批量生成完成。', 'aksrt'), + 'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'), 'shipping_updated' => __('物流状态已更新。', 'aksrt'), 'shipping_failed' => __('物流状态更新失败。', 'aksrt'), 'after_sale_updated' => __('售后状态已更新。', 'aksrt'), @@ -592,6 +595,34 @@ class AKSRT_Admin_Pay_Menus { (int) ($stats['used'] ?? 0) )) . '

'; + echo '

' . esc_html__('批量生成卡密', 'aksrt') . '

'; + echo '
'; + echo ''; + wp_nonce_field('aksrt_pay_batch_charge_cards'); + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

'; + echo '

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

' . esc_html(sprintf( + /* translators: %d count */ + __('成功生成 %d 张卡密。', 'aksrt'), + $generated + )) . '

'; + } + echo '
'; echo ''; echo ' '; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo ' ' . esc_html__('SPA 批量生成(过渡)', 'aksrt') . ''; echo '
'; $result = $svc->get_list([ @@ -720,6 +751,50 @@ class AKSRT_Admin_Pay_Menus { exit; } + public function handle_batch_charge_cards(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('权限不足', 'aksrt')); + } + check_admin_referer('aksrt_pay_batch_charge_cards'); + + if (!class_exists('AKSRT_Charge_Card')) { + wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); + exit; + } + + $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'balance'; + $count = isset($_POST['count']) ? (int) $_POST['count'] : 10; + $length = isset($_POST['length']) ? (int) $_POST['length'] : 16; + $prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : ''; + $amount = isset($_POST['amount']) ? (float) wp_unslash($_POST['amount']) : 0; + $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; + $level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1; + $days = isset($_POST['vip_days']) ? max(1, (int) $_POST['vip_days']) : 30; + + if ($type === 'points') { + $type = 'credits'; + } + + $meta = []; + if ($type === 'balance') { + $meta['amount'] = max(0, $amount); + } elseif ($type === 'credits') { + $meta['points'] = max(0, (int) $amount); + } elseif ($type === 'vip') { + $meta['level'] = $level; + $meta['days'] = $days; + } + + $codes = AKSRT_Charge_Card::get_instance()->batch_create($type, $meta, $count, $length, $prefix, '', $note); + $ok = !empty($codes); + + wp_safe_redirect($this->page_url('aksrt_charge_cards', [ + 'aksrt_pay_msg' => $ok ? 'card_batch_ok' : 'card_batch_failed', + 'aksrt_generated' => count($codes), + ])); + exit; + } + /* ------------------------------------------------------------------ * Shipping * ----------------------------------------------------------------*/ diff --git a/inc/classes/class-aksrt-shop-term-meta.php b/inc/classes/class-aksrt-shop-term-meta.php index 3f1a7568..b6873863 100644 --- a/inc/classes/class-aksrt-shop-term-meta.php +++ b/inc/classes/class-aksrt-shop-term-meta.php @@ -39,6 +39,12 @@ class AKSRT_Shop_Term_Meta { add_action('shop_discount_edit_form_fields', [$this, 'render_discount_edit_fields'], 10, 1); add_action('created_shop_discount', [$this, 'save_discount_term_fields']); add_action('edited_shop_discount', [$this, 'save_discount_term_fields']); + + // Product category inheritance fields. + add_action('aksrt_product_cat_add_form_fields', [$this, 'render_cat_add_fields']); + 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']); } // ─── Taxonomy Registration ─────────────────────────────────── @@ -565,4 +571,110 @@ class AKSRT_Shop_Term_Meta { update_term_meta($term_id, $key, $value); } } + + public function render_cat_add_fields(): void { + foreach ($this->get_cat_field_definitions() as $key => $def) { + echo '
'; + echo ''; + $this->render_cat_field_control($key, $def['default'] ?? '', false); + echo '
'; + } + } + + public function render_cat_edit_fields($term): void { + if (!($term instanceof WP_Term)) { + return; + } + foreach ($this->get_cat_field_definitions() as $key => $def) { + $value = get_term_meta($term->term_id, $key, true); + if ($value === '' || $value === false) { + $value = $def['default'] ?? ''; + } + echo ''; + $this->render_cat_field_control($key, $value, true); + echo ''; + } + } + + /** + * @param mixed $value + */ + private function render_cat_field_control(string $key, $value, bool $edit): void { + $val = (string) $value; + if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) { + echo ''; + return; + } + if (in_array($key, ['shop_cat_service', 'shop_cat_single_tab', 'shop_cat_content_after'], true)) { + echo ''; + return; + } + if ($key === 'shop_cat_shipping_fee') { + $opts = ['' => __('继承', 'aksrt'), 'free' => __('包邮', 'aksrt'), 'fixed' => __('固定运费', 'aksrt'), 'amount' => __('满额包邮', 'aksrt')]; + echo ''; + return; + } + if ($key === 'shop_cat_after_sale') { + $opts = ['' => __('继承', 'aksrt'), 'refund' => __('仅退款', 'aksrt'), 'refund_return' => __('退货退款', 'aksrt'), 'none' => __('不支持', 'aksrt')]; + echo ''; + return; + } + if ($key === 'shop_cat_content_layout') { + $opts = ['' => __('继承', 'aksrt'), 'adapt' => __('自适应', 'aksrt'), 'full' => __('通栏', 'aksrt'), 'sidebar' => __('侧栏', 'aksrt')]; + echo ''; + return; + } + if ($key === 'shop_cat_list_style') { + $opts = ['' => __('继承', 'aksrt'), 'default' => __('默认', 'aksrt'), 'card-lg' => __('大卡片', 'aksrt'), 'card-sm' => __('小卡片', 'aksrt')]; + echo ''; + return; + } + $type = $key === 'shop_cat_limit_buy' ? 'number' : 'text'; + printf( + '', + esc_attr($type), + $edit ? 'regular-text' : '', + esc_attr($key), + esc_attr($key), + esc_attr($val) + ); + } + + public function save_cat_term_fields(int $term_id): void { + if (!current_user_can('manage_categories')) { + return; + } + foreach ($this->get_cat_field_definitions() as $key => $def) { + if (in_array($key, ['shop_cat_guest_buy', 'shop_cat_email_fill', 'shop_cat_content_show_bg'], true)) { + $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); + } + } } diff --git a/inc/classes/forum/class-aksrt-topic-admin-list.php b/inc/classes/forum/class-aksrt-topic-admin-list.php new file mode 100644 index 00000000..f0d584f1 --- /dev/null +++ b/inc/classes/forum/class-aksrt-topic-admin-list.php @@ -0,0 +1,409 @@ + $label) { + $new[$key] = $label; + if ($key === 'title') { + $new['aksrt_plate'] = __('版块', 'aksrt'); + $new['aksrt_sticky'] = __('置顶', 'aksrt'); + $new['aksrt_locked'] = __('锁定', 'aksrt'); + $new['aksrt_review'] = __('审核', 'aksrt'); + $new['aksrt_bbs_type']= __('类型', 'aksrt'); + $new['aksrt_views'] = __('阅读', 'aksrt'); + } + } + return $new; + } + + public function sortable_columns(array $columns): array { + $columns['aksrt_sticky'] = 'aksrt_sticky'; + $columns['aksrt_views'] = 'aksrt_views'; + return $columns; + } + + public function render_column(string $column, int $post_id): void { + switch ($column) { + case 'aksrt_plate': + $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); + if ($plate_id > 0) { + $plate = get_post($plate_id); + echo $plate ? esc_html(get_the_title($plate)) : esc_html('#' . $plate_id); + } else { + // fallback board taxonomy + $terms = get_the_terms($post_id, 'aksrt_forum_board'); + if (is_array($terms) && $terms) { + echo esc_html($terms[0]->name); + } else { + echo '—'; + } + } + break; + + case 'aksrt_sticky': + $sticky = (int) get_post_meta($post_id, '_aksrt_sticky', true); + $map = [ + 0 => __('否', 'aksrt'), + 1 => __('版块', 'aksrt'), + 2 => __('超级', 'aksrt'), + 3 => __('全局', 'aksrt'), + ]; + echo esc_html($map[$sticky] ?? (string) $sticky); + echo ''; + break; + + case 'aksrt_locked': + $locked = (int) get_post_meta($post_id, '_aksrt_locked', true); + echo $locked ? esc_html__('是', 'aksrt') : esc_html__('否', 'aksrt'); + break; + + case 'aksrt_review': + $status = (string) get_post_meta($post_id, '_aksrt_review_status', true); + if ($status === '') { + $status = 'approved'; + } + $map = [ + 'approved' => __('已通过', 'aksrt'), + 'pending' => __('待审核', 'aksrt'), + 'rejected' => __('已拒绝', 'aksrt'), + ]; + echo esc_html($map[$status] ?? $status); + break; + + case 'aksrt_bbs_type': + $type = (string) (get_post_meta($post_id, '_aksrt_bbs_type', true) ?: 'standard'); + echo esc_html($type === 'question' ? __('提问', 'aksrt') : __('标准', 'aksrt')); + break; + + case 'aksrt_views': + echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_views', true)); + break; + } + } + + public function filters(string $post_type): void { + if ($post_type !== 'aksrt_forum_topic') { + return; + } + + $sticky = isset($_GET['aksrt_sticky']) ? sanitize_key((string) wp_unslash($_GET['aksrt_sticky'])) : ''; + $locked = isset($_GET['aksrt_locked']) ? sanitize_key((string) wp_unslash($_GET['aksrt_locked'])) : ''; + $review = isset($_GET['aksrt_review']) ? sanitize_key((string) wp_unslash($_GET['aksrt_review'])) : ''; + $plate = isset($_GET['aksrt_plate']) ? (int) $_GET['aksrt_plate'] : 0; + + echo ' '; + + echo ' '; + + echo ' '; + + $plates = get_posts([ + 'post_type' => 'aksrt_forum_plate', + 'post_status' => 'publish', + 'posts_per_page' => 200, + 'orderby' => 'title', + 'order' => 'ASC', + ]); + if ($plates) { + echo ' '; + } + } + + public function apply_filters(WP_Query $query): void { + if (!is_admin() || !$query->is_main_query()) { + return; + } + $post_type = $query->get('post_type'); + if ($post_type !== 'aksrt_forum_topic') { + return; + } + + $meta_query = (array) $query->get('meta_query'); + + if (isset($_GET['aksrt_sticky']) && $_GET['aksrt_sticky'] !== '') { + $meta_query[] = [ + 'key' => '_aksrt_sticky', + 'value' => (int) $_GET['aksrt_sticky'], + ]; + } + if (isset($_GET['aksrt_locked']) && $_GET['aksrt_locked'] !== '') { + $meta_query[] = [ + 'key' => '_aksrt_locked', + 'value' => (int) $_GET['aksrt_locked'], + ]; + } + if (!empty($_GET['aksrt_review'])) { + $review = sanitize_key((string) wp_unslash($_GET['aksrt_review'])); + if ($review === 'approved') { + $meta_query[] = [ + 'relation' => 'OR', + [ + 'key' => '_aksrt_review_status', + 'value' => 'approved', + ], + [ + 'key' => '_aksrt_review_status', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => '_aksrt_review_status', + 'value' => '', + ], + ]; + } else { + $meta_query[] = [ + 'key' => '_aksrt_review_status', + 'value' => $review, + ]; + } + } + if (!empty($_GET['aksrt_plate'])) { + $meta_query[] = [ + 'key' => '_aksrt_plate_id', + 'value' => (int) $_GET['aksrt_plate'], + ]; + } + + if ($meta_query) { + $query->set('meta_query', $meta_query); + } + + $orderby = $query->get('orderby'); + if ($orderby === 'aksrt_sticky') { + $query->set('meta_key', '_aksrt_sticky'); + $query->set('orderby', 'meta_value_num'); + } elseif ($orderby === 'aksrt_views') { + $query->set('meta_key', '_aksrt_views'); + $query->set('orderby', 'meta_value_num'); + } + } + + public function bulk_actions(array $actions): array { + $actions['aksrt_sticky_1'] = __('设为版块置顶', 'aksrt'); + $actions['aksrt_sticky_0'] = __('取消置顶', 'aksrt'); + $actions['aksrt_lock'] = __('锁定', 'aksrt'); + $actions['aksrt_unlock'] = __('解锁', 'aksrt'); + $actions['aksrt_approve'] = __('审核通过', 'aksrt'); + $actions['aksrt_reject'] = __('审核拒绝', 'aksrt'); + return $actions; + } + + public function handle_bulk_actions(string $redirect_to, string $action, array $post_ids): string { + $count = 0; + foreach ($post_ids as $post_id) { + $post_id = (int) $post_id; + if ($post_id <= 0 || get_post_type($post_id) !== 'aksrt_forum_topic') { + continue; + } + switch ($action) { + case 'aksrt_sticky_1': + update_post_meta($post_id, '_aksrt_sticky', 1); + $count++; + break; + case 'aksrt_sticky_0': + update_post_meta($post_id, '_aksrt_sticky', 0); + $count++; + break; + case 'aksrt_lock': + update_post_meta($post_id, '_aksrt_locked', 1); + $count++; + break; + case 'aksrt_unlock': + update_post_meta($post_id, '_aksrt_locked', 0); + $count++; + break; + case 'aksrt_approve': + update_post_meta($post_id, '_aksrt_review_status', 'approved'); + $count++; + break; + case 'aksrt_reject': + update_post_meta($post_id, '_aksrt_review_status', 'rejected'); + $count++; + break; + } + } + + return add_query_arg('aksrt_topic_bulk', $count, $redirect_to); + } + + public function bulk_notices(): void { + if (empty($_GET['aksrt_topic_bulk'])) { + return; + } + $count = (int) $_GET['aksrt_topic_bulk']; + echo '

' . esc_html(sprintf( + /* translators: %d count */ + __('已更新 %d 个帖子。', 'aksrt'), + $count + )) . '

'; + } + + public function quick_edit_box(string $column_name, string $post_type): void { + if ($post_type !== 'aksrt_forum_topic' || $column_name !== 'aksrt_sticky') { + return; + } + ?> +
+
+ + + + + +
+
+ post_type !== 'aksrt_forum_topic') { + return; + } + ?> + + ID, '_aksrt_plate_id', true); + $sticky = (int) get_post_meta($post->ID, '_aksrt_sticky', true); + $locked = (int) get_post_meta($post->ID, '_aksrt_locked', true); + $featured = (int) get_post_meta($post->ID, '_aksrt_featured', true); + $review = (string) get_post_meta($post->ID, '_aksrt_review_status', true); + if ($review === '') { + $review = 'approved'; + } + $bbs_type = (string) (get_post_meta($post->ID, '_aksrt_bbs_type', true) ?: 'standard'); + $visibility = (string) (get_post_meta($post->ID, '_aksrt_visibility', true) ?: 'public'); + $allow_view = (string) (get_post_meta($post->ID, '_aksrt_allow_view', true) ?: 'public'); + $pay_price = (float) get_post_meta($post->ID, '_aksrt_pay_price', true); + $pay_credits= (int) get_post_meta($post->ID, '_aksrt_pay_credits', true); + + $plates = get_posts([ + 'post_type' => 'aksrt_forum_plate', + 'post_status' => 'publish', + 'posts_per_page' => 200, + 'orderby' => 'title', + 'order' => 'ASC', + ]); + ?> +

+ + +

+

+ + +

+

+
+ +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+ post_type !== 'aksrt_forum_topic' || !current_user_can('edit_post', $post_id)) { + return; + } + + $plate_id = isset($_POST['aksrt_plate_id']) ? max(0, (int) $_POST['aksrt_plate_id']) : 0; + update_post_meta($post_id, '_aksrt_plate_id', $plate_id); + + $sticky = isset($_POST['aksrt_sticky']) ? max(0, min(3, (int) $_POST['aksrt_sticky'])) : 0; + update_post_meta($post_id, '_aksrt_sticky', $sticky); + update_post_meta($post_id, '_aksrt_locked', !empty($_POST['aksrt_locked']) ? 1 : 0); + update_post_meta($post_id, '_aksrt_featured', !empty($_POST['aksrt_featured']) ? 1 : 0); + + $review = isset($_POST['aksrt_review_status']) ? sanitize_key((string) wp_unslash($_POST['aksrt_review_status'])) : 'approved'; + if (!in_array($review, ['approved', 'pending', 'rejected'], true)) { + $review = 'approved'; + } + update_post_meta($post_id, '_aksrt_review_status', $review); + + $bbs_type = isset($_POST['aksrt_bbs_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_bbs_type'])) : 'standard'; + if (!in_array($bbs_type, ['standard', 'question'], true)) { + $bbs_type = 'standard'; + } + update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type); + + $visibility = isset($_POST['aksrt_visibility']) ? sanitize_key((string) wp_unslash($_POST['aksrt_visibility'])) : 'public'; + update_post_meta($post_id, '_aksrt_visibility', $visibility); + + $allow_view = isset($_POST['aksrt_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_allow_view'])) : 'public'; + update_post_meta($post_id, '_aksrt_allow_view', $allow_view); + + update_post_meta($post_id, '_aksrt_pay_price', isset($_POST['aksrt_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_pay_price'])) : 0); + update_post_meta($post_id, '_aksrt_pay_credits', isset($_POST['aksrt_pay_credits']) ? max(0, (int) wp_unslash($_POST['aksrt_pay_credits'])) : 0); + + // Keep board taxonomy bridge roughly in sync when plate selected. + if ($plate_id > 0 && taxonomy_exists('aksrt_forum_board')) { + $slug = 'plate-' . $plate_id; + $term = get_term_by('slug', $slug, 'aksrt_forum_board'); + if (!$term) { + $title = get_the_title($plate_id) ?: $slug; + $created = wp_insert_term($title, 'aksrt_forum_board', ['slug' => $slug]); + if (!is_wp_error($created) && !empty($created['term_id'])) { + $term_id = (int) $created['term_id']; + } else { + $term_id = 0; + } + } else { + $term_id = (int) $term->term_id; + } + if ($term_id > 0) { + wp_set_object_terms($post_id, [$term_id], 'aksrt_forum_board', false); + } + } + } +}