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 '';
+
+ // 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 '' . esc_html__('时间', 'aksrt') . ' ';
+ echo '' . esc_html__('用户', 'aksrt') . ' ';
+ echo '' . esc_html__('文章', 'aksrt') . ' ';
+ echo '' . esc_html__('源序号', 'aksrt') . ' ';
+ echo '' . esc_html__('积分消耗', 'aksrt') . ' ';
+ echo '' . esc_html__('使用', 'aksrt') . ' ';
+ echo '' . esc_html__('过期', 'aksrt') . ' ';
+ echo '' . esc_html__('IP', 'aksrt') . ' ';
+ echo ' ';
+
+ if (!$items) {
+ echo '' . esc_html__('暂无下载记录。', 'aksrt') . ' ';
+ }
+
+ 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 '' . esc_html((string) ($row['created_at'] ?? '')) . ' ';
+ echo '';
+ if ($user) {
+ echo esc_html($user->display_name . ' (#' . $uid . ')');
+ echo '' . esc_html__('仅此用户', 'aksrt') . ' ';
+ } else {
+ echo esc_html((string) $uid);
+ }
+ echo ' ';
+ echo '';
+ if ($edit) {
+ echo '' . esc_html($title) . ' ';
+ } else {
+ echo esc_html($title);
+ }
+ if ($pid > 0) {
+ echo '' . esc_html__('仅此文章', 'aksrt') . ' ';
+ }
+ echo ' ';
+ echo '' . esc_html((string) (int) ($row['source_index'] ?? 0)) . ' ';
+ echo '' . esc_html((string) (int) ($row['cost'] ?? 0)) . ' ';
+ echo '' . esc_html($used . ' / ' . $max) . ($used > 0 ? ' ' : '') . ' ';
+ echo '' . esc_html((string) ($row['expires_at'] ?? '')) . ' ';
+ echo '' . esc_html((string) ($row['ip_bind'] ?? '')) . ' ';
+ echo ' ';
+ }
+ echo '
';
+
+ 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) :
+ ?>
+
+
+
+
+
+