forum = $forum; } public function apply_moderator(int $user_id, int $board_id, string $reason) { if ($user_id <= 0) { return new WP_Error('not_logged_in', __('请先登录', 'aksrt')); } if ($this->forum->is_plate_cpt_active()) { $post = get_post($board_id); if (!$post || $post->post_type !== 'aksrt_forum_plate') { return new WP_Error('invalid_board', __('版块不存在', 'aksrt')); } } else { $board = get_term($board_id, 'aksrt_forum_board'); if (!$board || is_wp_error($board)) { return new WP_Error('invalid_board', __('版块不存在', 'aksrt')); } } if ($this->is_board_moderator($user_id, $board_id)) { return new WP_Error('already_moderator', __('你已经是该版块版主', 'aksrt')); } $applications = $this->get_board_moderator_applications($board_id); foreach ($applications as $application) { if ((int) ($application['user_id'] ?? 0) === $user_id && (string) ($application['status'] ?? '') === AKSRT_Forum::APPLICATION_PENDING) { return new WP_Error('application_exists', __('你已经提交过申请,等待处理即可', 'aksrt')); } } $reason = trim($reason); if ($reason === '') { return new WP_Error('empty_reason', __('请填写申请理由', 'aksrt')); } $application = [ 'id' => wp_generate_uuid4(), 'board_id' => $board_id, 'user_id' => $user_id, 'reason' => sanitize_textarea_field($reason), 'status' => AKSRT_Forum::APPLICATION_PENDING, 'created_at' => current_time('mysql'), 'reviewed_at' => '', 'review_note' => '', 'reviewer_id' => 0, ]; $applications[] = $application; if ($this->forum->is_plate_cpt_active()) { update_post_meta($board_id, '_aksrt_moderator_applications', $applications); } else { update_term_meta($board_id, '_aksrt_moderator_applications', $applications); } return [ 'message' => __('版主申请已提交', 'aksrt'), 'application' => $application, ]; } /** * Review a moderator application. * * @return array|WP_Error */ public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) { if ($reviewer_id <= 0 || !$this->forum->forum_user_can($reviewer_id, 'moderator_review')) { return new WP_Error('forbidden', __('权限不足', 'aksrt')); } if (!in_array($status, [AKSRT_Forum::APPLICATION_APPROVED, AKSRT_Forum::APPLICATION_REJECTED], true)) { return new WP_Error('invalid_status', __('申请处理状态无效', 'aksrt')); } $use_cpt = $this->forum->is_plate_cpt_active(); $boards = $this->forum->get_boards(); $flat_boards = $this->flatten_boards($boards); foreach ($flat_boards as $board) { $board_id = (int) ($board['id'] ?? 0); $applications = $this->get_board_moderator_applications($board_id); $updated = false; foreach ($applications as &$application) { if ((string) ($application['id'] ?? '') !== $application_id) { continue; } $application['status'] = $status; $application['review_note'] = sanitize_textarea_field($note); $application['reviewed_at'] = current_time('mysql'); $application['reviewer_id'] = $reviewer_id; $updated = true; if ($status === AKSRT_Forum::APPLICATION_APPROVED) { $this->add_moderator((int) ($application['user_id'] ?? 0), $board_id); } break; } unset($application); if ($updated) { if ($use_cpt) { update_post_meta($board_id, '_aksrt_moderator_applications', $applications); } else { update_term_meta($board_id, '_aksrt_moderator_applications', $applications); } $reviewed_application = $this->find_application_by_id($application_id); if ($reviewed_application) { $reviewed_user_id = (int) ($reviewed_application['user_id'] ?? 0); if ($reviewed_user_id > 0 && $reviewed_user_id !== $reviewer_id) { $board_name = (string) ($board['name'] ?? __('当前版块', 'aksrt')); $status_label = $status === AKSRT_Forum::APPLICATION_APPROVED ? __('已通过', 'aksrt') : __('已拒绝', 'aksrt'); $payload = [ 'title' => __('版主申请结果', 'aksrt'), 'target_url' => isset($board['url']) ? (string) $board['url'] : aksrt_get_forum_home_url(), ]; $clean_note = sanitize_textarea_field($note); if ($clean_note !== '') { $payload['reason'] = $clean_note; } $this->forum->notify( $reviewed_user_id, 'system', sprintf(__('你在「%1$s」提交的版主申请%2$s', 'aksrt'), $board_name, $status_label), $payload ); } } return [ 'message' => __('申请状态已更新', 'aksrt'), 'application' => $reviewed_application, ]; } } return new WP_Error('application_not_found', __('申请记录不存在', 'aksrt')); } /** * Flatten hierarchical boards array (collapse children into flat list). */ private function flatten_boards(array $boards): array { $flat = []; foreach ($boards as $board) { $children = $board['children'] ?? []; unset($board['children']); $flat[] = $board; if (!empty($children)) { $flat = array_merge($flat, $this->flatten_boards($children)); } } return $flat; } /** * Get flattened moderator application list. */ public function get_moderator_application_list(array $args = []): array { $status = sanitize_key((string) ($args['status'] ?? '')); $board_id = (int) ($args['board_id'] ?? 0); $keyword = sanitize_text_field((string) ($args['keyword'] ?? '')); $items = []; foreach ($this->forum->get_boards() as $board) { $current_board_id = (int) ($board['id'] ?? 0); if ($board_id > 0 && $current_board_id !== $board_id) { continue; } foreach ($this->get_board_moderator_applications($current_board_id) as $application) { if ($status !== '' && (string) ($application['status'] ?? '') !== $status) { continue; } $user = get_userdata((int) ($application['user_id'] ?? 0)); $row = [ 'id' => (string) ($application['id'] ?? ''), 'board_id' => $current_board_id, 'board_name' => (string) ($board['name'] ?? ''), 'user_id' => (int) ($application['user_id'] ?? 0), 'display_name' => $user ? $user->display_name : '', 'user_email' => $user ? $user->user_email : '', 'avatar_url' => $user ? $this->forum->get_user_avatar_url((int) $user->ID) : '', 'reason' => (string) ($application['reason'] ?? ''), 'status' => (string) ($application['status'] ?? ''), 'status_label' => $this->get_application_status_label((string) ($application['status'] ?? '')), 'created_at' => (string) ($application['created_at'] ?? ''), 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), 'review_note' => (string) ($application['review_note'] ?? ''), 'reviewer' => $this->forum->build_user_summary((int) ($application['reviewer_id'] ?? 0)), ]; if ($keyword !== '') { $haystack = strtolower(implode(' ', [ (string) $row['board_name'], (string) $row['display_name'], (string) $row['user_email'], (string) $row['reason'], ])); if (strpos($haystack, strtolower($keyword)) === false) { continue; } } $items[] = $row; } } usort($items, static function ($left, $right): int { return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); }); return $items; } /** * Sync board/plate topic count + reply count. */ public function sync_board_count(int $board_id): void { if ($board_id <= 0) { return; } if ($this->forum->is_plate_cpt_active()) { $this->sync_plate_counts($board_id); return; } $count = new WP_Query([ 'post_type' => 'aksrt_forum_topic', 'posts_per_page' => 1, 'fields' => 'ids', 'tax_query' => [[ 'taxonomy' => 'aksrt_forum_board', 'field' => 'term_id', 'terms' => $board_id, ]], 'post_status' => 'publish', ]); update_term_meta($board_id, '_aksrt_topic_count', (int) $count->found_posts); } /** * Sync plate multi-dimensional counters (§7.2 去规范化统计). */ public function sync_plate_counts(int $plate_id): void { global $wpdb; $posts_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'", (string) $plate_id )); $reply_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} c INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s AND c.comment_approved = '1'", (string) $plate_id )); $today_start = current_time('Y-m-d') . ' 00:00:00'; $today_reply_count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} c INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s AND c.comment_approved = '1' AND c.comment_date >= %s", (string) $plate_id, $today_start )); $last_post = $wpdb->get_var($wpdb->prepare( "SELECT p.post_date FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish' ORDER BY p.post_date DESC LIMIT 1", (string) $plate_id )); $last_reply = $wpdb->get_var($wpdb->prepare( "SELECT c.comment_date FROM {$wpdb->comments} c INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s AND c.comment_approved = '1' ORDER BY c.comment_date DESC LIMIT 1", (string) $plate_id )); update_post_meta($plate_id, '_aksrt_posts_count', $posts_count); update_post_meta($plate_id, '_aksrt_reply_count', $reply_count); update_post_meta($plate_id, '_aksrt_today_reply_count', $today_reply_count); update_post_meta($plate_id, '_aksrt_last_post', (string) ($last_post ?: '')); update_post_meta($plate_id, '_aksrt_last_reply', (string) ($last_reply ?: '')); } /** * Get board moderator IDs (统一入口,自动检测 CPT/taxonomy). */ public function get_board_moderators(int $board_id): array { if ($this->forum->is_plate_cpt_active()) { return $this->get_plate_moderators($board_id); } // Fallback: taxonomy $moderators = get_term_meta($board_id, '_aksrt_moderators', true); if (!is_array($moderators)) { return []; } return array_values(array_unique(array_filter(array_map('intval', $moderators)))); } /** * Get plate moderators from CPT meta. */ public function get_plate_moderators(int $plate_id): array { if (!post_type_exists('aksrt_forum_plate')) { return []; } $moderators = get_post_meta($plate_id, '_aksrt_moderators', true); if (!is_array($moderators)) { return []; } return array_values(array_unique(array_filter(array_map('intval', $moderators)))); } /** * Check if a user is a forum-level admin (bbs_admin_users setting). * * Forum admins have cross-board governance privileges and bypass bans. */ public function is_forum_admin(int $user_id): bool { if ($user_id <= 0) { return false; } if (user_can($user_id, 'manage_options')) { return true; } $forum = AKSRT_Settings::get_forum_settings(); $admin_users = $forum['admin_users'] ?? []; return in_array($user_id, $admin_users, true); } /** * Check if a user is a board moderator. */ public function is_board_moderator(int $user_id, int $board_id): bool { if ($user_id <= 0) { return false; } if ($this->is_forum_admin($user_id)) { return true; } return in_array($user_id, $this->get_board_moderators($board_id), true); } /** * Get board moderator profile rows. */ public function get_board_moderator_profiles(int $board_id): array { $profiles = []; foreach ($this->get_board_moderators($board_id) as $user_id) { $user = get_userdata($user_id); if (!$user) { continue; } $profiles[] = [ 'id' => $user_id, 'display_name' => $user->display_name, 'user_email' => $user->user_email, 'avatar_url' => $this->forum->get_user_avatar_url($user_id), ]; } return $profiles; } /** * Get board moderator profiles with role information. */ public function get_board_moderator_profiles_with_roles(int $board_id): array { $roles_map = $this->get_moderator_roles($board_id); $profiles = []; foreach ($this->get_board_moderators($board_id) as $user_id) { $user = get_userdata($user_id); if (!$user) continue; $profiles[] = [ 'id' => $user_id, 'display_name' => $user->display_name, 'user_email' => $user->user_email, 'avatar_url' => $this->forum->get_user_avatar_url($user_id), 'role' => $roles_map[$user_id] ?? 'moderator', ]; } return $profiles; } /** * Get moderator role map for a board: [user_id => role]. * * In CPT mode, also maps plate author → 'plate_author' (超级版主). */ public function get_moderator_roles(int $board_id): array { if ($this->forum->is_plate_cpt_active()) { $roles = get_post_meta($board_id, '_aksrt_moderator_roles', true); $roles = is_array($roles) ? $roles : []; $post = get_post($board_id); if ($post && $post->post_author > 0) { $roles[(int) $post->post_author] = 'plate_author'; } return $roles; } $roles = get_term_meta($board_id, '_aksrt_moderator_roles', true); return is_array($roles) ? $roles : []; } /** * Set a moderator's role on a board. */ public function set_moderator_role(int $user_id, int $board_id, string $role): bool { $allowed = ['super_moderator', 'moderator', 'junior_moderator', 'plate_author', 'cat_moderator']; if (!in_array($role, $allowed, true)) { return false; } if (!in_array($user_id, $this->get_board_moderators($board_id), true)) { return false; } $roles = $this->get_moderator_roles($board_id); $roles[$user_id] = $role; if ($this->forum->is_plate_cpt_active()) { return (bool) update_post_meta($board_id, '_aksrt_moderator_roles', $roles); } return (bool) update_term_meta($board_id, '_aksrt_moderator_roles', $roles); } /** * Add a moderator to a board with optional role. */ public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool { $current = $this->get_board_moderators($board_id); if (in_array($user_id, $current, true)) { $this->set_moderator_role($user_id, $board_id, $role); return true; } $current[] = $user_id; $new_list = array_values(array_unique(array_map('intval', $current))); if ($this->forum->is_plate_cpt_active()) { $result = update_post_meta($board_id, '_aksrt_moderators', $new_list); } else { $result = update_term_meta($board_id, '_aksrt_moderators', $new_list); } if ($result !== false) { $this->set_moderator_role($user_id, $board_id, $role); } return $result !== false; } /** * Remove a moderator from a board. */ public function remove_moderator(int $user_id, int $board_id): bool { $current = $this->get_board_moderators($board_id); $current = array_values(array_diff($current, [$user_id])); $roles = $this->get_moderator_roles($board_id); unset($roles[$user_id]); if ($this->forum->is_plate_cpt_active()) { $result = update_post_meta($board_id, '_aksrt_moderators', $current); update_post_meta($board_id, '_aksrt_moderator_roles', $roles); } else { $result = update_term_meta($board_id, '_aksrt_moderators', $current); update_term_meta($board_id, '_aksrt_moderator_roles', $roles); } return $result !== false; } /** * Get all boards a user moderates (with roles). */ public function get_user_moderator_boards(int $user_id): array { $boards = []; foreach ($this->forum->get_boards() as $board) { $board_id = (int) ($board['id'] ?? 0); if ($board_id <= 0) continue; $moderators = $this->get_board_moderators($board_id); if (in_array($user_id, $moderators, true)) { $roles = $this->get_moderator_roles($board_id); $boards[] = [ 'board_id' => $board_id, 'board_name' => (string) ($board['name'] ?? ''), 'role' => $roles[$user_id] ?? 'moderator', ]; } } return $boards; } /** * Check if a user has a specific minimum tier on a board/plate. * * Tier hierarchy (aligned with zibll 4-level): * forum_admin(4) > cat_moderator(3) > plate_author(2) > moderator(1) * * @param string $min_role 'moderator'|'plate_author'|'cat_moderator'|'forum_admin' */ public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool { $tier_map = [ 'junior_moderator' => AKSRT_Forum::TIER_MODERATOR, // backward-compat alias 'moderator' => AKSRT_Forum::TIER_MODERATOR, 'plate_author' => AKSRT_Forum::TIER_PLATE_AUTHOR, 'super_moderator' => AKSRT_Forum::TIER_PLATE_AUTHOR, // backward-compat alias 'cat_moderator' => AKSRT_Forum::TIER_CAT_MOD, 'forum_admin' => AKSRT_Forum::TIER_FORUM_ADMIN, ]; $required = $tier_map[$min_role] ?? AKSRT_Forum::TIER_MODERATOR; return $this->forum->get_user_plate_tier($user_id, $board_id) >= $required; } /** * Get board moderator applications (统一入口,自动检测 CPT/taxonomy). */ public function get_board_moderator_applications(int $board_id): array { if ($this->forum->is_plate_cpt_active()) { return $this->get_plate_moderator_applications($board_id); } // Fallback: taxonomy $applications = get_term_meta($board_id, '_aksrt_moderator_applications', true); if (!is_array($applications)) { return []; } return array_values(array_filter(array_map(function ($application) use ($board_id): array { if (!is_array($application)) { $application = []; } return [ 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), 'board_id' => $board_id, 'user_id' => (int) ($application['user_id'] ?? 0), 'reason' => (string) ($application['reason'] ?? ''), 'status' => (string) ($application['status'] ?? AKSRT_Forum::APPLICATION_PENDING), 'created_at' => (string) ($application['created_at'] ?? ''), 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), 'review_note' => (string) ($application['review_note'] ?? ''), 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), ]; }, $applications))); } /** * Get plate moderator applications from CPT meta. */ public function get_plate_moderator_applications(int $plate_id): array { if (!post_type_exists('aksrt_forum_plate')) { return []; } $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); if (!is_array($applications)) { return []; } return array_values(array_filter(array_map(function ($application) use ($plate_id): array { if (!is_array($application)) { $application = []; } return [ 'id' => (string) ($application['id'] ?? wp_generate_uuid4()), 'board_id' => $plate_id, 'user_id' => (int) ($application['user_id'] ?? 0), 'reason' => (string) ($application['reason'] ?? ''), 'status' => (string) ($application['status'] ?? AKSRT_Forum::APPLICATION_PENDING), 'created_at' => (string) ($application['created_at'] ?? ''), 'reviewed_at' => (string) ($application['reviewed_at'] ?? ''), 'review_note' => (string) ($application['review_note'] ?? ''), 'reviewer_id' => (int) ($application['reviewer_id'] ?? 0), ]; }, $applications))); } /** * Get current user's board application. */ public function get_board_application_for_user(int $user_id, int $board_id): ?array { $applications = $this->get_board_moderator_applications($board_id); $matched = array_values(array_filter($applications, static function ($application) use ($user_id): bool { return (int) ($application['user_id'] ?? 0) === $user_id; })); if (empty($matched)) { return null; } usort($matched, static function ($left, $right): int { return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? '')); }); $application = $matched[0]; $application['status_label'] = $this->get_application_status_label((string) ($application['status'] ?? '')); return $application; } public function find_application_by_id(string $application_id): ?array { foreach ($this->get_moderator_application_list() as $application) { if ((string) ($application['id'] ?? '') === $application_id) { return $application; } } return null; } public function get_application_status_label(string $status): string { $map = [ AKSRT_Forum::APPLICATION_PENDING => __('待处理', 'aksrt'), AKSRT_Forum::APPLICATION_APPROVED => __('已通过', 'aksrt'), AKSRT_Forum::APPLICATION_REJECTED => __('已拒绝', 'aksrt'), ]; return $map[$status] ?? $map[AKSRT_Forum::APPLICATION_PENDING]; } public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void { if (!$this->forum->is_plate_cpt_active()) { return; } $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); if ($plate_id > 0) { $this->sync_plate_counts($plate_id); } } public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void { if (!$this->forum->is_plate_cpt_active()) { return; } $post_id = (int) ($commentdata['comment_post_ID'] ?? 0); if ($post_id <= 0) { return; } $post = get_post($post_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return; } $plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true); if ($plate_id > 0) { $this->sync_plate_counts($plate_id); } } public function is_moderator(int $user_id, int $topic_id): bool { if ($user_id <= 0) { return false; } if ($this->is_forum_admin($user_id)) { return true; } // CPT mode: resolve plate from topic meta if ($this->forum->is_plate_cpt_active()) { $plate_id = $this->forum->get_topic_plate_id($topic_id); if ($plate_id > 0) { return $this->forum->get_user_plate_tier($user_id, $plate_id) >= self::TIER_MODERATOR; } } // Fallback: taxonomy-based boards $boards = get_the_terms($topic_id, 'aksrt_forum_board'); if ($boards && !is_wp_error($boards)) { foreach ($boards as $board) { if ($this->is_board_moderator($user_id, (int) $board->term_id)) { return true; } } } return false; } }