Files
wordpress-theme-aksrt/src/AKSRT/Forum/BoardServicePart2.php
T
kate522 aaf458af1c refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
2026-07-17 13:29:22 +08:00

790 lines
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait BoardServicePart2 {
/**
* Get board page view (统一入口 CPT/taxonomy).
*/
public function get_board_view(int $board_id, array $args = []): array {
$user_id = (int) ($args['user_id'] ?? get_current_user_id());
$page = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1));
$orderby = sanitize_key((string) ($args['orderby'] ?? $args['sort'] ?? ''));
$board_page = class_exists('\AKSRT\Settings\Settings')
? (\AKSRT\Settings\Settings::get_forum_settings()['board_page'] ?? [])
: [];
$tabs = is_array($board_page['tabs'] ?? null) ? $board_page['tabs'] : [
['label' => '全部', 'sort' => 'date'],
['label' => '推荐', 'sort' => 'featured'],
['label' => '最新回复', 'sort' => 'last_reply'],
['label' => '热门', 'sort' => 'hot'],
['label' => '精华', 'sort' => 'essence'],
];
$allowed_sorts = [];
foreach ($tabs as $tab) {
$sort = sanitize_key((string) ($tab['sort'] ?? ''));
if ($sort !== '') {
$allowed_sorts[] = $sort;
}
}
if ($orderby === '' || !in_array($orderby, $allowed_sorts, true)) {
$active_index = max(0, (int) ($board_page['tab_active_index'] ?? 0));
$orderby = sanitize_key((string) ($tabs[$active_index]['sort'] ?? 'date'));
if ($orderby === '') {
$orderby = 'date';
}
}
$compose_url = function_exists('aksrt_get_forum_compose_url')
? aksrt_get_forum_compose_url(['board' => $board_id])
: home_url('/');
$top_info = !isset($board_page['top_info']) || !empty($board_page['top_info']);
if ($this->forum->is_plate_cpt_active()) {
$view = $this->get_plate_view_compat($board_id, $user_id, $page, $orderby);
$view['tabs'] = $tabs;
$view['active_sort'] = $orderby;
$view['top_info'] = $top_info;
$view['compose_url'] = $compose_url;
return $view;
}
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return [
'board' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'application' => null,
'board_options' => [],
'tabs' => $tabs,
'active_sort' => $orderby,
'top_info' => $top_info,
'compose_url' => function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : home_url('/'),
];
}
$topics = $this->forum->get_topic_list([
'board_id' => $board_id,
'page' => $page,
'limit' => 20,
'status' => 'publish',
'viewer_user_id' => $user_id,
'orderby' => $orderby,
]);
$sticky_topics = $this->forum->get_sticky_topics_for_board($board_id);
$board_link = get_term_link($board);
return [
'board' => [
'id' => $board_id,
'name' => (string) $board->name,
'description' => (string) $board->description,
'content' => '',
'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true),
'topic_count' => (int) $board->count,
'reply_count' => 0,
'today_reply_count' => 0,
'views' => 0,
'follow_count' => 0,
'last_reply' => '',
'last_post' => '',
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
'thumbnail_url' => '',
'author_id' => 0,
'allow_view' => 'public',
],
'topics' => $topics['items'],
'sticky_topics' => array_slice($sticky_topics, 0, 5),
'moderators' => $this->forum->get_board_moderator_profiles($board_id),
'pagination' => [
'page' => $topics['page'],
'max_pages' => $topics['max_pages'],
'total' => $topics['total'],
],
'permissions' => [
'can_compose' => $user_id > 0,
'can_apply_moderator' => $user_id > 0 && !$this->forum->is_board_moderator($user_id, $board_id),
'can_moderate' => $this->forum->is_board_moderator($user_id, $board_id),
],
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $board_id) : null,
'board_options' => $this->get_boards(),
'tabs' => $tabs,
'active_sort' => $orderby,
'top_info' => $top_info,
'compose_url' => $compose_url,
];
}
/**
* Plate CPT board view (backwards-compatible with get_board_view return shape).
*/
private function get_plate_view_compat(int $plate_id, int $user_id, int $page, string $orderby = 'date'): array {
$plate = $this->get_plate($plate_id);
if (!$plate) {
return [
'board' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'application' => null,
'board_options' => [],
];
}
// Check access permission
$allow_view = $plate['allow_view'] ?? 'public';
if (!$this->can_view_plate($user_id, $plate_id, $allow_view)) {
return [
'board' => $plate,
'topics' => [],
'sticky_topics' => [],
'moderators' => $this->get_plate_moderator_profiles($plate_id),
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [
'can_compose' => false,
'can_apply_moderator' => false,
'can_moderate' => false,
'access_denied' => true,
'access_reason' => $allow_view,
],
'application' => null,
'board_options' => $this->get_boards(),
];
}
$topics = $this->forum->get_topic_list([
'plate_id' => $plate_id,
'page' => $page,
'limit' => 20,
'status' => 'publish',
'viewer_user_id' => $user_id,
'orderby' => $orderby,
]);
$sticky_topics = $this->get_sticky_topics_for_plate($plate_id);
return [
'board' => $plate,
'topics' => $topics['items'],
'sticky_topics' => array_slice($sticky_topics, 0, 5),
'moderators' => $this->get_plate_moderator_profiles($plate_id),
'pagination' => [
'page' => $topics['page'],
'max_pages' => $topics['max_pages'],
'total' => $topics['total'],
],
'permissions' => [
'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id),
'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'),
'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id),
'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id),
'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id),
'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id),
],
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $plate_id) : null,
'board_options' => $this->get_boards(),
];
}
/**
* Check if a user can view a plate (§7.2 板块阅读权限).
*/
public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool {
if ($allow_view === '') {
$allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public');
}
if ($allow_view === 'public') {
return true;
}
if ($user_id <= 0) {
// Guest cash follow unlock via signed cookie (money follow pay only).
if ($allow_view === 'follow'
&& function_exists('aksrt_guest_has_plate_follow_access')
&& aksrt_guest_has_plate_follow_access($plate_id)
) {
return true;
}
return false;
}
if ($this->is_plate_moderator($user_id, $plate_id)) {
return true;
}
if ($allow_view === 'login') {
return true;
}
if ($allow_view === 'follow') {
return $this->is_user_following_plate($user_id, $plate_id);
}
if ($allow_view === 'role') {
$allowed_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true);
if (!is_array($allowed_roles) || empty($allowed_roles)) {
return true;
}
$user = get_userdata($user_id);
if (!$user) {
return false;
}
return !empty(array_intersect($user->roles, $allowed_roles));
}
return true;
}
/**
* Check if a user can compose in a plate (§7.2 板块发帖限制).
*/
public function can_compose_in_plate(int $user_id, int $plate_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_plate_moderator($user_id, $plate_id)) {
return true;
}
$add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all');
if ($add_limit === 'all' || $add_limit === 'login') {
return true;
}
// Moderators already returned true above.
if ($add_limit === 'moderator') {
return false;
}
$user = get_userdata($user_id);
if (!$user) {
return false;
}
if ($add_limit === 'role') {
$allowed_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true);
if (!is_array($allowed_roles) || empty($allowed_roles)) {
return true;
}
return !empty(array_intersect($user->roles, $allowed_roles));
}
// Unknown contract → deny (no legacy fallthrough).
return false;
}
/**
* Check if a user is following a plate (§7.2 关注).
*/
public function is_user_following_plate(int $user_id, int $plate_id): bool {
if ($plate_id <= 0) {
return false;
}
if ($user_id <= 0) {
return function_exists('aksrt_guest_has_plate_follow_access')
&& aksrt_guest_has_plate_follow_access($plate_id);
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
return false;
}
return in_array($plate_id, $follow_list, true);
}
/**
* Toggle user follow on a plate (§7.2 关注/取消关注).
*
* When plate has paid follow enabled and user is not following, returns
* require_pay payload instead of free-following.
*
* @return array{followed?:bool,follow_count?:int,require_pay?:bool,pay?:array,message?:string,success?:bool}
*/
public function toggle_follow_plate(int $user_id, int $plate_id): array {
if ($user_id <= 0) {
return ['followed' => false, 'follow_count' => 0, 'success' => false, 'message' => __('请先登录', 'aksrt')];
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
$follow_list = [];
}
$followed = in_array($plate_id, $follow_list, true);
if ($followed) {
// Unfollow is always free (zibll warns but still allows cancel).
$follow_list = array_values(array_diff($follow_list, [$plate_id]));
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
$count = $this->count_plate_followers($plate_id);
update_post_meta($plate_id, '_aksrt_follow_count', $count);
return [
'followed' => false,
'follow_count' => $count,
'success' => true,
];
}
// Follow path — paid gate.
if ($this->is_plate_follow_pay_required($plate_id, $user_id)) {
$price = $this->resolve_plate_follow_pay_price($plate_id, $user_id);
return [
'followed' => false,
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'require_pay' => true,
'success' => false,
'message' => __('关注该版块需要付费', 'aksrt'),
'pay' => $price,
];
}
$follow_list[] = $plate_id;
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
$count = $this->count_plate_followers($plate_id);
update_post_meta($plate_id, '_aksrt_follow_count', $count);
return [
'followed' => true,
'follow_count' => $count,
'success' => true,
];
}
/**
* Whether global+plate paid follow is on and user still needs to pay.
*/
public function is_plate_follow_pay_required(int $plate_id, int $user_id): bool {
if ($user_id <= 0 || $plate_id <= 0) {
return false;
}
if ($this->is_user_following_plate($user_id, $plate_id)) {
return false;
}
if ($this->is_plate_moderator($user_id, $plate_id) || user_can($user_id, 'manage_options')) {
return false;
}
if (!$this->is_plate_follow_pay_enabled($plate_id)) {
return false;
}
// Already purchased plate_follow order.
if (class_exists('\AKSRT\Payment\OrderRepository') && class_exists('\AKSRT\Core\DomainModel')) {
if (\AKSRT\Payment\OrderRepository::get_instance()->has_paid_access(
$user_id,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW,
$plate_id
)) {
return false;
}
}
return true;
}
public function is_plate_follow_pay_enabled(int $plate_id): bool {
if ($plate_id <= 0) {
return false;
}
// Global switch from forum settings (SPA).
$global_on = true;
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$cfg = \AKSRT\Settings\Settings::get_forum_settings();
// Nested permissions.plate_follow_pay or flat.
if (isset($cfg['permissions']['plate_follow_pay'])) {
$global_on = !empty($cfg['permissions']['plate_follow_pay']);
} elseif (isset($cfg['plate_follow_pay'])) {
$global_on = !empty($cfg['plate_follow_pay']);
}
}
if (!$global_on) {
return false;
}
if ((int) get_post_meta($plate_id, '_aksrt_follow_pay', true) !== 1) {
return false;
}
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
if ($modo === 'points') {
return (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true) > 0;
}
return (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true) > 0;
}
/**
* @return array{amount:float|int,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float|int,currency:string,vip_level:int,vip_set:bool}
*/
public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array {
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
if (!in_array($modo, ['money', 'points'], true)) {
$modo = 'money';
}
$is_points = $modo === 'points';
$base = $is_points
? max(0, (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true))
: max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', 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;
}
}
$pick = static function ($raw) use ($is_points) {
if ($raw === '' || $raw === null || $raw === false) {
return null;
}
if (!is_numeric($raw)) {
return null;
}
return $is_points ? max(0, (int) $raw) : max(0.0, (float) $raw);
};
$vip2 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true));
$vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true));
if ($vip1 === null) {
// Legacy single VIP key → vip1.
$vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true));
}
$amount = $base;
$is_vip_price = false;
$vip_set = ($vip1 !== null || $vip2 !== null);
if ($vip_level >= 2 && $vip2 !== null) {
$amount = $vip2;
$is_vip_price = true;
} elseif ($vip_level >= 1 && $vip1 !== null) {
$amount = $vip1;
$is_vip_price = true;
} elseif ($vip_level >= 2 && $vip1 !== null) {
// VIP2 user falls back to VIP1 when VIP2 unset.
$amount = $vip1;
$is_vip_price = true;
}
if ($is_vip_price) {
return [
'amount' => $amount,
'modo' => $modo,
'is_vip_price' => true,
'is_free' => $amount <= 0,
'label' => $amount <= 0
? __('VIP 免费关注', 'aksrt')
: ($is_points
? sprintf(__('VIP %d 积分', 'aksrt'), (int) $amount)
: sprintf(__('VIP ¥%s', 'aksrt'), number_format((float) $amount, 2, '.', ''))),
'base' => $base,
'currency' => $is_points ? 'points' : 'CNY',
'vip_level' => $vip_level,
'vip_set' => true,
];
}
return [
'amount' => $base,
'modo' => $modo,
'is_vip_price' => false,
'is_free' => false,
'label' => $is_points
? sprintf(__('%d 积分', 'aksrt'), (int) $base)
: sprintf(__('¥%s', 'aksrt'), number_format((float) $base, 2, '.', '')),
'base' => $base,
'currency' => $is_points ? 'points' : 'CNY',
'vip_level' => $vip_level,
'vip_set' => $vip_set,
];
}
/**
* Force-add plate to user follow list (after paid success).
*/
public function mark_plate_followed(int $user_id, int $plate_id): void {
if ($user_id <= 0 || $plate_id <= 0) {
return;
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
$follow_list = [];
}
$follow_list[] = $plate_id;
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
$count = $this->count_plate_followers($plate_id);
update_post_meta($plate_id, '_aksrt_follow_count', $count);
}
/**
* Purchase plate follow with points or balance (creates plate_follow order when cash).
*
* @return array{success:bool,message:string,followed?:bool,follow_count?:int,order_number?:string}
*/
public function purchase_plate_follow(int $user_id, int $plate_id): array {
if ($user_id <= 0) {
return $this->forum->error(__('请先登录', 'aksrt'));
}
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return $this->forum->error(__('版块不存在', 'aksrt'));
}
if ($this->is_user_following_plate($user_id, $plate_id)) {
return $this->forum->success(__('已关注', 'aksrt'), [
'followed' => true,
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
]);
}
if (!$this->is_plate_follow_pay_enabled($plate_id)) {
// Free follow fallback.
$this->mark_plate_followed($user_id, $plate_id);
return $this->forum->success(__('关注成功', 'aksrt'), [
'followed' => true,
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
]);
}
$price = $this->resolve_plate_follow_pay_price($plate_id, $user_id);
if (!empty($price['is_free'])) {
$this->mark_plate_followed($user_id, $plate_id);
return $this->forum->success(__('VIP 免费关注成功', 'aksrt'), [
'followed' => true,
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'free' => true,
]);
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return $this->forum->error(__('积分系统不可用', 'aksrt'));
}
$modo = (string) ($price['modo'] ?? 'money');
$amount = $price['amount'] ?? 0;
if ($modo === 'points') {
$cost = max(0, (int) $amount);
if ($cost <= 0) {
return $this->forum->error(__('积分价格未设置', 'aksrt'));
}
$balance = \AKSRT\Credits\Credits::get_balance($user_id);
if ($balance < $cost) {
return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost));
}
$deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'forum_plate_follow', [
'plate_id' => $plate_id,
], sprintf(__('付费关注版块「%s」', 'aksrt'), get_the_title($plate_id)));
if (is_wp_error($deducted) || $deducted === false) {
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt'));
}
$this->mark_plate_followed($user_id, $plate_id);
$this->grant_plate_follow_author_share($user_id, $plate_id, $cost, true);
return $this->forum->success(__('关注成功', 'aksrt'), [
'followed' => true,
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'credits_cost' => $cost,
]);
}
// Cash: multi-gateway cashier only (PlateFollowPay REST). No Credits×100 bypass.
return $this->forum->error(__('请通过收银台支付关注费用。', 'aksrt'), [
'require_cashier' => true,
'pay' => $price,
'modo' => 'money',
'plate_id' => $plate_id,
]);
}
private function grant_plate_follow_author_share(int $buyer_id, int $plate_id, int $cost, bool $is_points): void {
if (!$is_points || $cost <= 0 || !class_exists('\AKSRT\Credits\Credits')) {
return;
}
$post = get_post($plate_id);
if (!$post) {
return;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $buyer_id) {
return;
}
$author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70));
$author_amount = (int) floor($cost * $author_share / 100);
if ($author_amount <= 0) {
return;
}
\AKSRT\Credits\Credits::add($author_id, $author_amount, 'forum_plate_follow_income', [
'plate_id' => $plate_id,
'buyer_id' => $buyer_id,
], sprintf(__('版块「%s」被付费关注', 'aksrt'), get_the_title($plate_id)));
}
/**
* Count followers for a plate.
*/
private function count_plate_followers(int $plate_id): int {
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->usermeta}
WHERE meta_key = 'aksrt_follow_plate'
AND meta_value LIKE %s",
'%i:' . $plate_id . ';%'
));
}
/**
* Get plates a user follows.
*/
public function get_user_followed_plates(int $user_id): array {
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list) || empty($follow_list)) {
return [];
}
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'post__in' => array_map('intval', $follow_list),
'post_status' => 'publish',
'posts_per_page' => 100,
'orderby' => 'post__in',
]);
$result = [];
foreach ($plates as $plate) {
$result[] = $this->build_plate_payload($plate);
}
return $result;
}
/**
* Get a user's managed plates (plates where user is moderator/author).
*
* @return array{owned:array,moderating:array}
*/
public function get_user_managed_plates(int $user_id): array {
$owned = [];
$moderating = [];
if (!$this->forum->is_plate_cpt_active()) {
return ['owned' => $owned, 'moderating' => $moderating];
}
// Plates where user is author (超级版主)
$own_plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'author' => $user_id,
'post_status' => 'publish',
'posts_per_page' => 50,
'orderby' => 'title',
'order' => 'ASC',
]);
foreach ($own_plates as $plate) {
$owned[] = $this->build_plate_payload($plate);
}
// Plates where user is a moderator (in _aksrt_moderators list)
global $wpdb;
$mod_plate_ids = $wpdb->get_col($wpdb->prepare(
"SELECT pm.post_id FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'aksrt_forum_plate' AND p.post_status = 'publish'
WHERE pm.meta_key = %s",
'_aksrt_moderators'
));
foreach ($mod_plate_ids as $plate_id) {
$plate_id = (int) $plate_id;
// Skip plates already in owned
if (in_array($plate_id, wp_list_pluck($own_plates, 'ID'), true)) {
continue;
}
$mods = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
$plate = get_post($plate_id);
if ($plate) {
$moderating[] = $this->build_plate_payload($plate);
}
}
}
return ['owned' => $owned, 'moderating' => $moderating];
}
// ── Plate utilities (extracted from facade) ──
public function increment_plate_views(int $plate_id): void {
if ($plate_id <= 0 || !$this->forum->is_plate_cpt_active()) {
return;
}
$views = (int) get_post_meta($plate_id, '_aksrt_views', true);
update_post_meta($plate_id, '_aksrt_views', $views + 1);
}
/**
* Check if a plate qualifies as "hot" (§7.2 热门版块).
*/
public function is_hot_plate(int $plate_id): bool {
$config = $this->forum->get_config();
$threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100);
$posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true);
$follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true);
return ($posts_count + $follow_count * 2) >= $threshold;
}
/**
* Get plate categories (分区) with moderators.
*/
public function get_plate_categories(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_plate_cat',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$categories = [];
foreach ($terms as $term) {
$term_id = (int) $term->term_id;
$moderator_ids = get_term_meta($term_id, '_aksrt_cat_moderator', true);
if (!is_array($moderator_ids)) {
$moderator_ids = [];
}
$categories[] = [
'id' => $term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'slug' => (string) $term->slug,
'count' => (int) $term->count,
'parent_id' => (int) $term->parent,
'moderators' => array_values(array_unique(array_filter(array_map('intval', $moderator_ids)))),
'add_limit' => (string) (get_term_meta($term_id, '_aksrt_add_limit', true) ?: 'all'),
'url' => (string) get_term_link($term),
];
}
return $categories;
}
}