From 8d31e65ccd3455caac9bad315f80afe62c8132b4 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 15 Jul 2026 11:13:48 +0800 Subject: [PATCH] feat(admin,forum): download logs, hidecontent, VIP tiers, users commerce Add aksrt_download_logs workbench, zibll hidecontent alias + topic partial-hide path, VIP1/VIP2 price matrix for topics, and users list commerce snapshot column with deep links. --- docs/admin-ia-map.md | 1 + docs/zibll-gap-matrix.md | 4 +- functions.php | 3 + inc/bootstrap.php | 1 + .../class-aksrt-admin-pay-download-logs.php | 202 ++++++++++++++++++ .../native/class-aksrt-admin-pay-menus.php | 11 + .../native/class-aksrt-admin-pay-products.php | 4 + inc/classes/class-aksrt-post-meta-box.php | 4 + .../class-aksrt-user-credits-admin.php | 58 +++++ .../forum/class-aksrt-topic-meta-box.php | 69 +++++- ...trait-aksrt-forum-topic-service-part-3.php | 110 ++++++++-- inc/core-hooks/part-1.php | 6 +- inc/helpers/aksrt-shortcodes.php | 93 +++++++- templates/forum/forum-topic.php | 77 +++++++ 14 files changed, 611 insertions(+), 32 deletions(-) create mode 100644 inc/classes/admin/native/class-aksrt-admin-pay-download-logs.php diff --git a/docs/admin-ia-map.md b/docs/admin-ia-map.md index 7b074bbe..9ca7ea06 100644 --- a/docs/admin-ia-map.md +++ b/docs/admin-ia-map.md @@ -28,6 +28,7 @@ Do **not** reintroduce operational workbenches into the SPA once native pages sh ├─ 概览 aksrt_pay ├─ 订单 aksrt_orders ├─ 付费文章 aksrt_pay_products +├─ 下载记录 aksrt_download_logs ├─ 发货及物流 aksrt_shipping ├─ 售后管理 aksrt_after_sale ├─ 优惠码 aksrt_coupons diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md index 4a8822e6..f52696c0 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 modularized (orders/shipping/after-sale/coupons/charge-cards/income/rebate/withdraw) + plate moderator roles/advanced limits/icon/sort + coupon product_scope create/edit/batch + paid-post sales report (`aksrt_pay_products`) + topic pay/points visibility admin/runtime alignment + plate follow-pay MVP + post list sales column + order type tabs + topic plate re-check | -| 5 remaining | download-log admin page; topic VIP1/2 full matrix; hidecontent shortcode partial-hide; users commerce columns | +| 5+ behavior parity | done — native ops shell modularized + plate follow-pay MVP + topic pay/points + sales report + post sales column + order type tabs + download logs page + hidecontent alias + topic VIP1/2 matrix + users commerce column + topic partial hide | +| 5 remaining | optional: multi-gateway for forum/topics; full zibll download modal UX; topic virtual_sales/coupon fields | ## Dual-UI rule diff --git a/functions.php b/functions.php index 4c793d43..c0d7c035 100644 --- a/functions.php +++ b/functions.php @@ -374,6 +374,9 @@ if (is_admin() && class_exists('AKSRT_Admin_Pay_Orders')) { if (is_admin() && class_exists('AKSRT_Admin_Pay_Products')) { AKSRT_Admin_Pay_Products::get_instance()->init(); } +if (is_admin() && class_exists('AKSRT_Admin_Pay_Download_Logs')) { + AKSRT_Admin_Pay_Download_Logs::get_instance()->init(); +} if (is_admin() && class_exists('AKSRT_Admin_Pay_Withdrawals')) { AKSRT_Admin_Pay_Withdrawals::get_instance()->init(); } diff --git a/inc/bootstrap.php b/inc/bootstrap.php index db167446..3dddba83 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -179,6 +179,7 @@ require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-shippi require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-after-sale.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-orders.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-products.php'; +require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-download-logs.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-withdrawals.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-income.php'; require_once AKSRT_DIR . '/inc/classes/admin/native/class-aksrt-admin-pay-rebate.php'; diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-download-logs.php b/inc/classes/admin/native/class-aksrt-admin-pay-download-logs.php new file mode 100644 index 00000000..038243dc --- /dev/null +++ b/inc/classes/admin/native/class-aksrt-admin-pay-download-logs.php @@ -0,0 +1,202 @@ +ensure_cap(); + echo '

' . esc_html__('下载记录', 'aksrt') . '

'; + + if (!class_exists('AKSRT_DB_Migrator')) { + echo '

' . esc_html__('数据库服务不可用。', 'aksrt') . '

'; + return; + } + + $table = AKSRT_DB_Migrator::table('dl_token'); + if ($table === '') { + echo '

' . esc_html__('下载令牌表未初始化。', 'aksrt') . '

'; + return; + } + + $post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0; + $user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0; + $only_used = !isset($_GET['used']) || (string) $_GET['used'] !== '0'; + $page_num = max(1, (int) ($_GET['paged'] ?? 1)); + $per_page = 30; + + $result = $this->query_logs($table, [ + 'post_id' => $post_id, + 'user_id' => $user_id, + 'only_used' => $only_used, + 'limit' => $per_page, + 'offset' => ($page_num - 1) * $per_page, + ]); + $items = $result['items']; + $total = $result['total']; + $pages = max(1, (int) ceil($total / $per_page)); + + echo '

' . esc_html__('基于下载令牌表(aksrt_dl_token)。已使用记录会保留一段时间后清理;未使用且过期的令牌会被定期删除。', 'aksrt') . '

'; + + echo '
'; + echo ''; + echo ' '; + echo ' '; + echo ' '; + // When unchecked, pass used=0 explicitly. + if (!$only_used) { + // no-op; checkbox not checked + } + submit_button(__('筛选', 'aksrt'), 'secondary', '', false); + echo ' ' . esc_html__('重置', 'aksrt') . ''; + echo '
'; + + // Fix: unchecked checkbox means used filter off — re-read without default if form submitted with no used. + // Already handled via isset($_GET['used']). + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if (!$items) { + echo ''; + } + + foreach ($items as $row) { + $uid = (int) ($row['user_id'] ?? 0); + $pid = (int) ($row['post_id'] ?? 0); + $user = $uid > 0 ? get_userdata($uid) : null; + $title = $pid > 0 ? get_the_title($pid) : ''; + if ($title === '') { + $title = $pid > 0 ? sprintf(__('(已删除 #%d)', 'aksrt'), $pid) : '—'; + } + $edit = $pid > 0 ? get_edit_post_link($pid, 'raw') : ''; + $used = (int) ($row['used_count'] ?? 0); + $max = (int) ($row['max_uses'] ?? 1); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
' . esc_html__('时间', 'aksrt') . '' . esc_html__('用户', 'aksrt') . '' . esc_html__('文章', 'aksrt') . '' . esc_html__('源序号', 'aksrt') . '' . esc_html__('积分消耗', 'aksrt') . '' . esc_html__('使用', 'aksrt') . '' . esc_html__('过期', 'aksrt') . '' . esc_html__('IP', 'aksrt') . '
' . esc_html__('暂无下载记录。', 'aksrt') . '
' . esc_html((string) ($row['created_at'] ?? '')) . ''; + if ($user) { + echo esc_html($user->display_name . ' (#' . $uid . ')'); + echo '
' . esc_html__('仅此用户', 'aksrt') . ''; + } else { + echo esc_html((string) $uid); + } + echo '
'; + if ($edit) { + echo '' . esc_html($title) . ''; + } else { + echo esc_html($title); + } + if ($pid > 0) { + echo '
' . esc_html__('仅此文章', 'aksrt') . ''; + } + echo '
' . esc_html((string) (int) ($row['source_index'] ?? 0)) . '' . esc_html((string) (int) ($row['cost'] ?? 0)) . '' . esc_html($used . ' / ' . $max) . ($used > 0 ? ' ' : '') . '' . esc_html((string) ($row['expires_at'] ?? '')) . '' . esc_html((string) ($row['ip_bind'] ?? '')) . '
'; + + if ($pages > 1) { + echo '
'; + echo paginate_links([ + 'base' => add_query_arg('paged', '%#%'), + 'format' => '', + 'prev_text' => '«', + 'next_text' => '»', + 'total' => $pages, + 'current' => $page_num, + ]); + echo '
'; + } + echo ''; + } + + /** + * @param array{post_id?:int,user_id?:int,only_used?:bool,limit?:int,offset?:int} $args + * @return array{items: array>, total:int} + */ + private function query_logs(string $table, array $args): array { + global $wpdb; + + $where = ['1=1']; + $params = []; + + $post_id = (int) ($args['post_id'] ?? 0); + if ($post_id > 0) { + $where[] = 'post_id = %d'; + $params[] = $post_id; + } + $user_id = (int) ($args['user_id'] ?? 0); + if ($user_id > 0) { + $where[] = 'user_id = %d'; + $params[] = $user_id; + } + if (!empty($args['only_used'])) { + $where[] = 'used_count > 0'; + } + + $where_sql = implode(' AND ', $where); + $limit = max(1, min(100, (int) ($args['limit'] ?? 30))); + $offset = max(0, (int) ($args['offset'] ?? 0)); + + $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; + $total = $params + ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) + : (int) $wpdb->get_var($count_sql); + + $sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; + $list_params = $params; + $list_params[] = $limit; + $list_params[] = $offset; + $rows = $wpdb->get_results($wpdb->prepare($sql, $list_params), ARRAY_A); + + return [ + 'items' => is_array($rows) ? $rows : [], + 'total' => $total, + ]; + } + + private function ensure_cap(): void { + if (!current_user_can(self::CAPABILITY)) { + wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); + } + } +} 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 f3189845..244b50b4 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-menus.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-menus.php @@ -65,6 +65,17 @@ class AKSRT_Admin_Pay_Menus { wp_die(esc_html__('付费文章模块未初始化。', 'aksrt')); }, ], + 'download_logs' => [ + 'slug' => 'aksrt_download_logs', + 'title' => __('下载记录', 'aksrt'), + 'callback' => static function (): void { + if (class_exists('AKSRT_Admin_Pay_Download_Logs')) { + AKSRT_Admin_Pay_Download_Logs::get_instance()->render_page(); + return; + } + wp_die(esc_html__('下载记录模块未初始化。', 'aksrt')); + }, + ], 'shipping' => [ 'slug' => 'aksrt_shipping', 'title' => __('发货及物流', 'aksrt'), diff --git a/inc/classes/admin/native/class-aksrt-admin-pay-products.php b/inc/classes/admin/native/class-aksrt-admin-pay-products.php index b3c4420a..9de6ccb7 100644 --- a/inc/classes/admin/native/class-aksrt-admin-pay-products.php +++ b/inc/classes/admin/native/class-aksrt-admin-pay-products.php @@ -141,6 +141,10 @@ class AKSRT_Admin_Pay_Products { echo '' . esc_html__('编辑', 'aksrt') . ''; } echo '' . esc_html__('订单', 'aksrt') . ''; + if ($pt === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) { + $dl_url = $this->page_url('aksrt_download_logs', ['post_id' => $post_id]); + echo '' . esc_html__('下载记录', 'aksrt') . ''; + } $permalink = get_permalink($post_id); if ($permalink) { echo '' . esc_html__('查看', 'aksrt') . ''; diff --git a/inc/classes/class-aksrt-post-meta-box.php b/inc/classes/class-aksrt-post-meta-box.php index 355abd98..c3bc7dfa 100644 --- a/inc/classes/class-aksrt-post-meta-box.php +++ b/inc/classes/class-aksrt-post-meta-box.php @@ -532,6 +532,10 @@ class AKSRT_Post_Meta_Box { echo '
' . esc_html__('暂无订单', 'aksrt') . ''; } echo '
' . esc_html__('付费文章', 'aksrt') . ''; + if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) { + $dl_url = admin_url('admin.php?page=aksrt_download_logs&post_id=' . $post_id); + echo ' · ' . esc_html__('下载记录', 'aksrt') . ''; + } echo ''; } diff --git a/inc/classes/class-aksrt-user-credits-admin.php b/inc/classes/class-aksrt-user-credits-admin.php index f28f2887..c964f265 100644 --- a/inc/classes/class-aksrt-user-credits-admin.php +++ b/inc/classes/class-aksrt-user-credits-admin.php @@ -44,10 +44,14 @@ class AKSRT_User_Credits_Admin { public function add_balance_column(array $columns): array { $currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); $columns['aksrt_credits_balance'] = sprintf(__('%s余额', 'aksrt'), $currency_name); + $columns['aksrt_user_commerce'] = __('资产/分成', 'aksrt'); return $columns; } public function render_balance_column(string $output, string $column_name, int $user_id): string { + if ($column_name === 'aksrt_user_commerce') { + return $this->render_commerce_column($user_id); + } if ($column_name !== 'aksrt_credits_balance') { return $output; } @@ -59,6 +63,55 @@ class AKSRT_User_Credits_Admin { return '' . esc_html((string) $balance) . ''; } + /** + * Compact commerce snapshot: VIP, revenue, rebate, download logs link. + */ + private function render_commerce_column(int $user_id): string { + $bits = []; + if (function_exists('aksrt_get_user_vip_level')) { + $lv = (int) aksrt_get_user_vip_level($user_id); + if ($lv > 0) { + $bits[] = 'VIP' . esc_html((string) $lv) . ''; + } + } + if (class_exists('AKSRT_Revenue')) { + $stats = AKSRT_Revenue::get_instance()->get_stats($user_id); + $pending = (float) ($stats['pending'] ?? 0); + $total = (float) ($stats['total'] ?? 0); + if ($total > 0 || $pending > 0) { + $bits[] = sprintf( + /* translators: 1 total 2 pending */ + esc_html__('分成 ¥%1$s(待结 ¥%2$s)', 'aksrt'), + number_format($total, 2), + number_format($pending, 2) + ); + $bits[] = '' . esc_html__('明细', 'aksrt') . ''; + } + } + if (class_exists('AKSRT_Rebate')) { + $list = AKSRT_Rebate::get_instance()->get_list(['user_id' => $user_id, 'limit' => 1]); + $total_r = (int) ($list['total'] ?? 0); + if ($total_r > 0) { + $sum = 0.0; + // Lightweight sum via stats if available; else show count. + if (method_exists(AKSRT_Rebate::get_instance(), 'get_stats')) { + // global stats only — show link with count + $bits[] = sprintf(esc_html__('返佣 %d 笔', 'aksrt'), $total_r); + } else { + $bits[] = sprintf(esc_html__('返佣 %d 笔', 'aksrt'), $total_r); + } + $bits[] = '' . esc_html__('返佣', 'aksrt') . ''; + } + } + $bits[] = '' . esc_html__('下载记录', 'aksrt') . ''; + $bits[] = '' . esc_html__('订单', 'aksrt') . ''; + + if (!$bits) { + return '—'; + } + return '
' . implode('
', $bits) . '
'; + } + public function make_balance_sortable(array $columns): array { $columns['aksrt_credits_balance'] = 'aksrt_credits_balance'; return $columns; @@ -91,6 +144,11 @@ class AKSRT_User_Credits_Admin { esc_html(sprintf(__('调整%s', 'aksrt'), $currency_name)) ); } + $actions['aksrt_user_dl_logs'] = sprintf( + '%s', + esc_url(admin_url('admin.php?page=aksrt_download_logs&user_id=' . (int) $user->ID)), + esc_html__('下载记录', 'aksrt') + ); return $actions; } diff --git a/inc/classes/forum/class-aksrt-topic-meta-box.php b/inc/classes/forum/class-aksrt-topic-meta-box.php index f82a9ef7..0e154d0b 100644 --- a/inc/classes/forum/class-aksrt-topic-meta-box.php +++ b/inc/classes/forum/class-aksrt-topic-meta-box.php @@ -83,6 +83,19 @@ class AKSRT_Topic_Meta_Box { $pay_vip_price = get_post_meta($post->ID, '_aksrt_pay_vip_price', true); $vip_set = ($pay_vip_price !== '' && $pay_vip_price !== null && $pay_vip_price !== false); $vip_price_val = $vip_set ? (float) $pay_vip_price : ''; + $pay_vip1_price = get_post_meta($post->ID, '_aksrt_pay_vip1_price', true); + $pay_vip2_price = get_post_meta($post->ID, '_aksrt_pay_vip2_price', true); + $vip1_set = ($pay_vip1_price !== '' && $pay_vip1_price !== null && $pay_vip1_price !== false); + $vip2_set = ($pay_vip2_price !== '' && $pay_vip2_price !== null && $pay_vip2_price !== false); + // Migrate single VIP price into vip1 display if vip1 empty. + if (!$vip1_set && $vip_set) { + $vip1_set = true; + $pay_vip1_price = $pay_vip_price; + } + $credits_vip1 = get_post_meta($post->ID, '_aksrt_credits_vip1', true); + $credits_vip2 = get_post_meta($post->ID, '_aksrt_credits_vip2', true); + $credits_vip1_set = ($credits_vip1 !== '' && $credits_vip1 !== null && $credits_vip1 !== false); + $credits_vip2_set = ($credits_vip2 !== '' && $credits_vip2 !== null && $credits_vip2 !== false); $pay_hide_part = (int) get_post_meta($post->ID, '_aksrt_pay_hide_part', true) === 1; $view_roles = get_post_meta($post->ID, '_aksrt_visibility_roles', true); if (!is_array($view_roles)) { @@ -172,21 +185,33 @@ class AKSRT_Topic_Meta_Box {

+

+ + +

+

+ + +

- - - + + +

+

+ + +

- +

post_content; if (!$can_view) { - // Prevent content leak via payload/API when template forgets can_view check. - // Partial-hide flag only applies after unlock; denied users get empty body. - $content = ''; - if (in_array($visibility, [AKSRT_Forum::VISIBILITY_CREDITS, AKSRT_Forum::VISIBILITY_PAY, AKSRT_Forum::VISIBILITY_LOGIN, AKSRT_Forum::VISIBILITY_COMMENT, AKSRT_Forum::VISIBILITY_ROLE, AKSRT_Forum::VISIBILITY_MODERATOR], true)) { - $excerpt = ''; + // Partial hide: keep body for shortcode processing; strip locked blocks at render. + // Full hide (default): blank content to avoid API/template leaks. + if ($pay_hide_part && in_array($visibility, [AKSRT_Forum::VISIBILITY_CREDITS, AKSRT_Forum::VISIBILITY_PAY], true)) { + // leave content; payshow/hidecontent shortcodes gate paid parts + $excerpt = wp_trim_words(wp_strip_all_tags( + preg_replace('/\[(payshow|hidecontent)[^\]]*\].*?\[\/\1\]/is', '', $content) ?? $content + ), 40); + } else { + $content = ''; + if (in_array($visibility, [AKSRT_Forum::VISIBILITY_CREDITS, AKSRT_Forum::VISIBILITY_PAY, AKSRT_Forum::VISIBILITY_LOGIN, AKSRT_Forum::VISIBILITY_COMMENT, AKSRT_Forum::VISIBILITY_ROLE, AKSRT_Forum::VISIBILITY_MODERATOR], true)) { + $excerpt = ''; + } } } @@ -334,6 +341,34 @@ trait AKSRT_Forum_Topic_Service_Part_3 { } $cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true)); + if ($cost <= 0) { + $cost = max(0, (int) get_post_meta($topic_id, '_aksrt_pay_credits', true)); + } + // VIP tier discount for credits. + $vip_level = 0; + if (function_exists('aksrt_get_user_vip_level')) { + $vip_level = (int) aksrt_get_user_vip_level($user_id); + } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { + $vip_level = 2; + } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { + $vip_level = 1; + } + if ($vip_level >= 2) { + $v2 = get_post_meta($topic_id, '_aksrt_credits_vip2', true); + if ($v2 !== '' && $v2 !== null && $v2 !== false) { + $cost = max(0, (int) $v2); + } + } elseif ($vip_level >= 1) { + $v1 = get_post_meta($topic_id, '_aksrt_credits_vip1', true); + if ($v1 !== '' && $v1 !== null && $v1 !== false) { + $cost = max(0, (int) $v1); + } + } + if ($cost <= 0 && $vip_level > 0) { + // VIP free credits unlock. + $this->mark_topic_unlocked($user_id, $topic_id); + return $this->forum->success(__('VIP 免费解锁成功', 'aksrt'), ['free' => true]); + } if ($cost <= 0) { return $this->forum->error(__('积分价格未设置', 'aksrt')); } @@ -491,31 +526,69 @@ trait AKSRT_Forum_Topic_Service_Part_3 { /** * Resolve cash price for a pay-visibility topic. * - * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool} + * VIP resolution: VIP2 price → VIP1/legacy VIP price → base. + * + * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int} */ public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array { $base_raw = get_post_meta($topic_id, '_aksrt_pay_price', true); $base = is_numeric($base_raw) ? (float) $base_raw : 0.0; - $vip_raw = get_post_meta($topic_id, '_aksrt_pay_vip_price', true); - $vip_set = ($vip_raw !== '' && $vip_raw !== null && $vip_raw !== false); - $vip_price = $vip_set && is_numeric($vip_raw) ? (float) $vip_raw : 0.0; - - $is_vip = $user_id > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($user_id); - if (!$is_vip && $user_id > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { - $is_vip = true; + $vip_level = 0; + if ($user_id > 0) { + if (function_exists('aksrt_get_user_vip_level')) { + $vip_level = (int) aksrt_get_user_vip_level($user_id); + } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { + $vip_level = 2; + } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { + $vip_level = 1; + } + if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { + $vip_level = 1; + } } - if ($is_vip && $vip_set) { + $pick = static function ($raw) { + if ($raw === '' || $raw === null || $raw === false) { + return null; + } + return is_numeric($raw) ? (float) $raw : null; + }; + + $vip2 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip2_price', true)); + $vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip1_price', true)); + if ($vip1 === null) { + // Legacy single VIP key. + $vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip_price', true)); + } + + $amount = $base; + $is_vip_price = false; + $vip_set = ($vip1 !== null || $vip2 !== null); + + if ($vip_level >= 2 && $vip2 !== null) { + $amount = max(0.0, $vip2); + $is_vip_price = true; + } elseif ($vip_level >= 1 && $vip1 !== null) { + $amount = max(0.0, $vip1); + $is_vip_price = true; + } elseif ($vip_level >= 2 && $vip1 !== null) { + // VIP2 user falls back to VIP1 price when VIP2 unset. + $amount = max(0.0, $vip1); + $is_vip_price = true; + } + + if ($is_vip_price) { return [ - 'amount' => max(0.0, $vip_price), + 'amount' => $amount, 'is_vip_price' => true, - 'is_free' => $vip_price <= 0, - 'label' => $vip_price <= 0 + 'is_free' => $amount <= 0, + 'label' => $amount <= 0 ? __('VIP 免费', 'aksrt') - : sprintf(__('VIP ¥%s', 'aksrt'), number_format($vip_price, 2, '.', '')), + : sprintf(__('VIP ¥%s', 'aksrt'), number_format($amount, 2, '.', '')), 'base' => $base, 'vip_set' => true, + 'vip_level' => $vip_level, ]; } @@ -528,6 +601,7 @@ trait AKSRT_Forum_Topic_Service_Part_3 { : __('价格未设置', 'aksrt'), 'base' => $base, 'vip_set' => $vip_set, + 'vip_level' => $vip_level, ]; } diff --git a/inc/core-hooks/part-1.php b/inc/core-hooks/part-1.php index 8acefd82..40acc36b 100644 --- a/inc/core-hooks/part-1.php +++ b/inc/core-hooks/part-1.php @@ -326,9 +326,13 @@ function aksrt_register_forum_topic_meta() { // 付费内容扩展位(论坛帖子付费门槛;前台 forum-topic 渲染) '_aksrt_allow_view' => ['type' => 'string', 'default' => 'public'], '_aksrt_pay_price' => ['type' => 'number', 'default' => 0], - '_aksrt_pay_vip_price' => ['type' => 'number', 'default' => 0], + '_aksrt_pay_vip_price' => ['type' => 'number', 'default' => 0], // legacy single VIP + '_aksrt_pay_vip1_price' => ['type' => 'number', 'default' => 0], + '_aksrt_pay_vip2_price' => ['type' => 'number', 'default' => 0], '_aksrt_pay_credits' => ['type' => 'integer', 'default' => 0], // legacy alias of credits_cost '_aksrt_credits_cost' => ['type' => 'integer', 'default' => 0], // runtime unlock price + '_aksrt_credits_vip1' => ['type' => 'integer', 'default' => 0], + '_aksrt_credits_vip2' => ['type' => 'integer', 'default' => 0], '_aksrt_pay_hide_part' => ['type' => 'integer', 'default' => 0], '_aksrt_visibility_roles' => ['type' => 'array', 'default' => []], diff --git a/inc/helpers/aksrt-shortcodes.php b/inc/helpers/aksrt-shortcodes.php index c994e3ad..d10bcacc 100644 --- a/inc/helpers/aksrt-shortcodes.php +++ b/inc/helpers/aksrt-shortcodes.php @@ -37,6 +37,8 @@ class AKSRT_Shortcodes { add_shortcode('reply', [$this, 'render_reply']); add_shortcode('vip', [$this, 'render_vip']); add_shortcode('payshow', [$this, 'render_payshow']); + // Zibll-compatible hidecontent alias (type=payshow|reply|login|vip|password|…) + add_shortcode('hidecontent', [$this, 'render_hidecontent']); add_shortcode('content', [$this, 'render_content_gate']); // 文章归档 / 个人资料卡 → AKSRT_Templates 模板 add_shortcode('aksrt_archive', [$this, 'render_archive']); @@ -101,16 +103,13 @@ class AKSRT_Shortcodes { } /** - * [payshow]...[/payshow] — 付费后可见 + * [payshow]...[/payshow] — 付费后可见(文章 content-pay 或论坛主题解锁) */ public function render_payshow(array $atts = [], string $content = ''): string { $atts = shortcode_atts(['message' => ''], $atts, 'payshow'); $pid = get_the_ID(); - $can_view = false; - if ($pid && class_exists('AKSRT_Content_Visibility')) { - $can_view = AKSRT_Content_Visibility::can_view_paid_content($pid); - } + $can_view = $this->viewer_can_see_paid_block($pid); if ($can_view) { return '
' @@ -132,6 +131,90 @@ class AKSRT_Shortcodes { return $html; } + /** + * Zibll [hidecontent type="payshow|reply|login|…"]…[/hidecontent] + */ + public function render_hidecontent(array $atts = [], string $content = ''): string { + $atts = shortcode_atts([ + 'type' => 'payshow', + 'message' => '', + 'pass' => '', + ], $atts, 'hidecontent'); + $type = sanitize_key((string) $atts['type']); + $msg = (string) $atts['message']; + + switch ($type) { + case 'reply': + case 'comment': + return $this->render_reply(['message' => $msg], $content); + case 'login': + case 'signin': + return $this->render_hide([ + 'mode' => 'login', + 'message' => $msg !== '' ? $msg : __('登录后可查看隐藏内容', 'aksrt'), + ], $content); + case 'password': + case 'pass': + return $this->render_hide([ + 'mode' => 'password', + 'message' => $msg, + 'pass' => (string) $atts['pass'], + ], $content); + case 'vip': + return $this->render_vip(['message' => $msg], $content); + case 'payshow': + case 'pay': + case 'points': + default: + return $this->render_payshow(['message' => $msg], $content); + } + } + + /** + * Whether current viewer may see a paid hide block on this post/topic. + */ + private function viewer_can_see_paid_block(int $pid): bool { + if ($pid <= 0) { + return false; + } + $post = get_post($pid); + if (!$post instanceof WP_Post) { + return false; + } + $uid = get_current_user_id(); + if ($uid > 0 && ((int) $post->post_author === $uid || user_can($uid, 'manage_options'))) { + return true; + } + + // Forum topics: paid/credits unlock list or orders. + if ($post->post_type === 'aksrt_forum_topic' && class_exists('AKSRT_Forum')) { + $forum = AKSRT_Forum::get_instance(); + $visibility = sanitize_key((string) get_post_meta($pid, '_aksrt_visibility', true)); + if ($visibility === 'public' || $visibility === '') { + return true; + } + if (method_exists($forum, 'has_user_unlocked_topic') && $forum->has_user_unlocked_topic($uid, $pid)) { + return true; + } + if (method_exists($forum, 'has_user_paid_topic') && $forum->has_user_paid_topic($uid, $pid)) { + return true; + } + // Free VIP price counts as paid for cash topics. + if ($visibility === 'pay' && method_exists($forum, 'resolve_topic_pay_price')) { + $price = $forum->resolve_topic_pay_price($pid, $uid); + if (!empty($price['is_free'])) { + return true; + } + } + return false; + } + + if (class_exists('AKSRT_Content_Visibility')) { + return AKSRT_Content_Visibility::can_view_paid_content($pid); + } + return false; + } + /** * [content type="admin|author|vip"]...[/content] — 按角色/身份控制可见性 */ diff --git a/templates/forum/forum-topic.php b/templates/forum/forum-topic.php index c3fa4be6..863ce4fd 100644 --- a/templates/forum/forum-topic.php +++ b/templates/forum/forum-topic.php @@ -59,6 +59,83 @@ if (empty($topic['can_view'])) : $deny_vis = $topic['visibility']['key'] ?? ''; $credits_cost = (int) get_post_meta($topic_id, '_aksrt_credits_cost', true); $pay_price = get_post_meta($topic_id, '_aksrt_pay_price', true); + $pay_hide_part = !empty($topic['pay_hide_part']) || ((int) get_post_meta($topic_id, '_aksrt_pay_hide_part', true) === 1); + $partial_ok = $pay_hide_part && in_array($deny_vis, ['pay', 'credits'], true); + + // Partial hide: show title + body with payshow/hidecontent locked blocks + pay CTA. + if ($partial_ok) : + ?> +
+
+
+
+
+
+

+
+
+ +

+ 0 && is_user_logged_in()) : ?> + + + + 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($viewer_id); + $is_free_vip = false; + if ($is_vip_user && $vip_set) { + $charge_amount = max(0.0, (float) $vip_price_raw); + $is_free_vip = $charge_amount <= 0; + } + ?> + + + + + +
+
+ +
+
+
+
+
+
+