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.
This commit is contained in:
kate522 committed 2026-07-15 11:13:48 +08:00
1 parent cf6ad0e972
commit 8d31e65ccd
14 files changed
+611 -32

No files matched your search

@@ -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 {
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_cost" id="aksrt_credits_cost" value="<?php echo esc_attr((string) $credits_cost); ?>">
<span class="description"><?php esc_html_e('可见性为「支付积分后可见」时使用。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-credits-fields">
<label for="aksrt_credits_vip1"><strong><?php esc_html_e('VIP1 积分价', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_vip1" id="aksrt_credits_vip1" value="<?php echo esc_attr($credits_vip1_set ? (string) (int) $credits_vip1 : ''); ?>" placeholder="<?php esc_attr_e('留空=不设', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-credits-fields">
<label for="aksrt_credits_vip2"><strong><?php esc_html_e('VIP2 积分价', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_vip2" id="aksrt_credits_vip2" value="<?php echo esc_attr($credits_vip2_set ? (string) (int) $credits_vip2 : ''); ?>" placeholder="<?php esc_attr_e('留空=不设;0=免费', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_price"><strong><?php esc_html_e('付费价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_price" id="aksrt_pay_price" value="<?php echo esc_attr((string) $pay_price); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_vip_price"><strong><?php esc_html_e('VIP 价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_vip_price" id="aksrt_pay_vip_price" value="<?php echo esc_attr($vip_set ? (string) $vip_price_val : ''); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP 价', 'aksrt'); ?>">
<span class="description"><?php esc_html_e('填 0 表示 VIP 免费;留空则 VIP 与普通同价。', 'aksrt'); ?></span>
<label for="aksrt_pay_vip1_price"><strong><?php esc_html_e('VIP1 价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_vip1_price" id="aksrt_pay_vip1_price" value="<?php echo esc_attr($vip1_set ? (string) (float) $pay_vip1_price : ''); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP1 价', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_vip2_price"><strong><?php esc_html_e('VIP2 价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_vip2_price" id="aksrt_pay_vip2_price" value="<?php echo esc_attr($vip2_set ? (string) (float) $pay_vip2_price : ''); ?>" placeholder="<?php esc_attr_e('留空=不设;0=VIP2 免费', 'aksrt'); ?>">
<span class="description"><?php esc_html_e('兼容旧「VIP 价格」字段:未填 VIP1 时仍可读 _aksrt_pay_vip_price。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-pay-hide-fields">
<label>
<input type="checkbox" name="aksrt_pay_hide_part" value="1" <?php checked($pay_hide_part); ?>>
<?php esc_html_e('仅隐藏部分内容', 'aksrt'); ?>
</label>
<span class="description" style="display:block;"><?php esc_html_e('开启后仍以全文门控为默认;部分隐藏依赖正文隐藏块。未解锁用户正文不会下发。', 'aksrt'); ?></span>
<span class="description" style="display:block;"><?php esc_html_e('开启后正文仍输出,付费块用 [payshow] 或 [hidecontent type="payshow"] 包裹。未开启则全文门控。', 'aksrt'); ?></span>
</p>
<?php
}
@@ -281,9 +306,22 @@ class AKSRT_Topic_Meta_Box {
}
update_post_meta($post_id, '_aksrt_credits_cost', $cost);
update_post_meta($post_id, '_aksrt_pay_credits', $cost);
if (isset($_POST['aksrt_credits_vip1']) && (string) wp_unslash($_POST['aksrt_credits_vip1']) !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) wp_unslash($_POST['aksrt_credits_vip1'])));
} else {
delete_post_meta($post_id, '_aksrt_credits_vip1');
}
if (isset($_POST['aksrt_credits_vip2']) && (string) wp_unslash($_POST['aksrt_credits_vip2']) !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) wp_unslash($_POST['aksrt_credits_vip2'])));
} else {
delete_post_meta($post_id, '_aksrt_credits_vip2');
}
} else {
delete_post_meta($post_id, '_aksrt_credits_cost');
delete_post_meta($post_id, '_aksrt_pay_credits');
delete_post_meta($post_id, '_aksrt_credits_vip1');
delete_post_meta($post_id, '_aksrt_credits_vip2');
}
// Cash pay.
@@ -291,15 +329,34 @@ class AKSRT_Topic_Meta_Box {
$price = isset($_POST['aksrt_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_pay_price'])) : 0;
update_post_meta($post_id, '_aksrt_pay_price', $price);
if (isset($_POST['aksrt_pay_vip_price']) && (string) wp_unslash($_POST['aksrt_pay_vip_price']) !== '') {
$vip1_written = false;
if (isset($_POST['aksrt_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_pay_vip1_price']) !== '') {
$vip1 = max(0, (float) wp_unslash($_POST['aksrt_pay_vip1_price']));
update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip1);
// Keep legacy single VIP key in sync with VIP1 for older readers.
update_post_meta($post_id, '_aksrt_pay_vip_price', $vip1);
$vip1_written = true;
} else {
delete_post_meta($post_id, '_aksrt_pay_vip1_price');
}
if (isset($_POST['aksrt_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_pay_vip2_price']) !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_pay_vip2_price'])));
} else {
delete_post_meta($post_id, '_aksrt_pay_vip2_price');
}
// Legacy field if posted without vip1.
if (!$vip1_written && isset($_POST['aksrt_pay_vip_price']) && (string) wp_unslash($_POST['aksrt_pay_vip_price']) !== '') {
$vip = max(0, (float) wp_unslash($_POST['aksrt_pay_vip_price']));
update_post_meta($post_id, '_aksrt_pay_vip_price', $vip);
} else {
update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip);
} elseif (!$vip1_written) {
delete_post_meta($post_id, '_aksrt_pay_vip_price');
}
} else {
delete_post_meta($post_id, '_aksrt_pay_price');
delete_post_meta($post_id, '_aksrt_pay_vip_price');
delete_post_meta($post_id, '_aksrt_pay_vip1_price');
delete_post_meta($post_id, '_aksrt_pay_vip2_price');
}
// Partial hide only meaningful for pay/credits; otherwise clear.
@@ -68,11 +68,18 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
$pay_hide_part = (int) get_post_meta($topic_id, '_aksrt_pay_hide_part', true) === 1;
$content = (string) $post->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,
];
}