diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md
index 9f042571..48496f4e 100644
--- a/docs/zibll-gap-matrix.md
+++ b/docs/zibll-gap-matrix.md
@@ -48,7 +48,7 @@ Focus: admin **entry & page shape**. Runtime field parity is Phase 5+.
| `zibpay_order_page` | Orders (legacy) | SPA | same native list | |
| `zibpay_page#/shipping` | Shipping | SPA | `aksrt_shipping` | `AKSRT_Shipping` |
| `zibpay_page#/after-sale` | After-sale | SPA | `aksrt_after_sale` | `AKSRT_After_Sale` |
-| `zibpay_product_page` | Paid-post sales report | partial | optional later `aksrt_pay_products` | content pay |
+| `zibpay_product_page` | Paid-post sales report | done | `aksrt_pay_products` | content pay (`AKSRT_Admin_Pay_Products` + order aggregation) |
| `zibpay_income_page` | Creator income | `PaymentRevenue` | `aksrt_income` | `AKSRT_Revenue` |
| `zibpay_rebate_page` | Rebate | `PaymentRebate` | `aksrt_rebate` | `AKSRT_Rebate` |
| `zibpay_withdraw` | Withdraw | `ShopWithdrawals` | `aksrt_withdraw` | `AKSRT_Withdrawal` |
@@ -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 |
-| 5 remaining | zibll pay-hide/points plate options out of scope |
+| 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 |
+| 5 remaining | plate follow-pay (zibll paid follow) still optional; topic-level pay/points/hide-part done |
## Dual-UI rule
diff --git a/inc/classes/forum/class-aksrt-topic-meta-box.php b/inc/classes/forum/class-aksrt-topic-meta-box.php
index 800088bc..f82a9ef7 100644
--- a/inc/classes/forum/class-aksrt-topic-meta-box.php
+++ b/inc/classes/forum/class-aksrt-topic-meta-box.php
@@ -2,6 +2,16 @@
/**
* Native topic governance meta box for aksrt_forum_topic.
*
+ * Visibility / pay / credits contract (runtime source of truth):
+ * - `_aksrt_visibility` public|login|comment|role|credits|pay|moderator
+ * - `_aksrt_credits_cost` points price when visibility=credits
+ * - `_aksrt_pay_price` cash price when visibility=pay
+ * - `_aksrt_pay_vip_price` optional VIP cash (0 = free)
+ * - `_aksrt_visibility_roles` roles when visibility=role
+ * - `_aksrt_pay_hide_part` partial hide flag (full hide still default)
+ *
+ * Legacy `_aksrt_pay_credits` / topic `_aksrt_allow_view` are rewritten on save.
+ *
* @package AKSRT
*/
@@ -9,6 +19,16 @@ defined('ABSPATH') || exit;
class AKSRT_Topic_Meta_Box {
+ private const VISIBILITY = [
+ 'public',
+ 'login',
+ 'comment',
+ 'role',
+ 'credits',
+ 'pay',
+ 'moderator',
+ ];
+
private static $instance = null;
public static function get_instance(): self {
@@ -49,10 +69,27 @@ class AKSRT_Topic_Meta_Box {
$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);
+ $visibility = sanitize_key((string) (get_post_meta($post->ID, '_aksrt_visibility', true) ?: 'public'));
+ if (!in_array($visibility, self::VISIBILITY, true)) {
+ $visibility = 'public';
+ }
+
+ $credits_cost = (int) get_post_meta($post->ID, '_aksrt_credits_cost', true);
+ if ($credits_cost <= 0) {
+ // Legacy dual key.
+ $credits_cost = max(0, (int) get_post_meta($post->ID, '_aksrt_pay_credits', true));
+ }
+ $pay_price = (float) get_post_meta($post->ID, '_aksrt_pay_price', true);
+ $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_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)) {
+ $view_roles = [];
+ }
+ $view_roles = array_values(array_filter(array_map('sanitize_key', $view_roles)));
+ $wp_roles = $this->wp_role_options();
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
@@ -61,6 +98,16 @@ class AKSRT_Topic_Meta_Box {
'orderby' => 'title',
'order' => 'ASC',
]);
+
+ $visibility_labels = [
+ 'public' => __('公开', 'aksrt'),
+ 'login' => __('登录可见', 'aksrt'),
+ 'comment' => __('评论可见', 'aksrt'),
+ 'role' => __('指定角色', 'aksrt'),
+ 'credits' => __('支付积分后可见', 'aksrt'),
+ 'pay' => __('付费后可见', 'aksrt'),
+ 'moderator' => __('仅版主', 'aksrt'),
+ ];
?>
@@ -104,26 +151,42 @@ class AKSRT_Topic_Meta_Box {
-
-
-