forum = $forum; } // ================================================================ // Favorite (§7.3) // ================================================================ public function is_favorited(int $user_id, int $topic_id): bool { if ($user_id <= 0) { return false; } $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); $favorites = is_array($favorites) ? $favorites : []; return in_array($topic_id, $favorites, true); } public function toggle_favorite(int $user_id, int $topic_id) { if ($user_id <= 0) { return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); } $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); } $favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true); $favorites = is_array($favorites) ? $favorites : []; $was_favorited = in_array($topic_id, $favorites, true); if ($was_favorited) { $favorites = array_values(array_diff($favorites, [$topic_id])); } else { $favorites[] = $topic_id; } update_user_meta($user_id, 'aksrt_favorite_forum_posts', $favorites); $count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); $count = $was_favorited ? max(0, $count - 1) : $count + 1; update_post_meta($topic_id, '_aksrt_favorite_count', $count); $this->refresh_recommend_score($topic_id); return [ 'favorited' => !$was_favorited, 'count' => $count, 'message' => $was_favorited ? __('已取消收藏', 'aksrt') : __('收藏成功', 'aksrt'), ]; } // ================================================================ // Score (§7.3) // ================================================================ public function score_topic(int $user_id, int $topic_id, int $amount) { if ($user_id <= 0) { return new \WP_Error('not_logged_in', __('请先登录', 'aksrt')); } $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); } if ($amount === 0) { return new \WP_Error('invalid_amount', __('评分值不能为零', 'aksrt')); } $author_id = (int) $post->post_author; if ($author_id === $user_id) { return new \WP_Error('self_score', __('不能给自己的帖子评分', 'aksrt')); } $config = $this->forum->get_config(); $max_extra = (int) ($config['post_page']['score_extra_max'] ?? 5); $max_deduct = (int) ($config['post_page']['score_deduct_max'] ?? 3); if ($amount > 0 && $amount > $max_extra) { $amount = $max_extra; } if ($amount < 0 && abs($amount) > $max_deduct) { $amount = -$max_deduct; } $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); $detail = is_array($detail) ? $detail : []; $user_key = (string) $user_id; if (isset($detail[$user_key])) { return new \WP_Error('already_scored', __('你已经对该帖评过分', 'aksrt')); } $detail[$user_key] = [ 'amount' => $amount, 'time' => current_time('mysql'), ]; update_post_meta($topic_id, '_aksrt_score_detail', $detail); $total = (int) get_post_meta($topic_id, '_aksrt_score', true); $total += $amount; update_post_meta($topic_id, '_aksrt_score', $total); do_action('aksrt_forum_topic_scored', $topic_id, (int) $author_id, (float) $amount); $reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true); $reputation += $amount; update_user_meta($author_id, 'aksrt_reputation', $reputation); if ($amount > 0) { $credits_amount = (int) ($config['credits']['score_extra'] ?? 3); if ($credits_amount > 0) { if (class_exists('\AKSRT\Credits\PointsFree')) { \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_score_extra', $credits_amount, [ 'type' => 'post', 'id' => $topic_id, ]); } elseif (class_exists('\AKSRT\Credits\Credits')) { $daily = $this->today_credits_count($author_id, 'forum_score_extra'); $daily_max = (int) ($config['credits']['daily_max'] ?? 50); if ($daily < $daily_max) { \AKSRT\Credits\Credits::add($author_id, $credits_amount, 'forum_score_extra', sprintf( __('帖子「%s」被加分', 'aksrt'), get_the_title($topic_id) )); } } } } $this->refresh_recommend_score($topic_id); $this->notify_topic_scored($topic_id, $user_id, $amount); return [ 'score' => $total, 'message' => $amount > 0 ? __('加分成功', 'aksrt') : __('减分成功', 'aksrt'), ]; } public function get_score_detail(int $topic_id): array { $detail = get_post_meta($topic_id, '_aksrt_score_detail', true); $detail = is_array($detail) ? $detail : []; $entries = []; foreach ($detail as $uid => $entry) { $user = get_userdata((int) $uid); $entries[] = [ 'user_id' => (int) $uid, 'display_name' => $user ? $user->display_name : '', 'amount' => (int) ($entry['amount'] ?? 0), 'time' => (string) ($entry['time'] ?? ''), ]; } return $entries; } // ================================================================ // Recommend score (§7.3) // ================================================================ public function calculate_recommend_score(int $topic_id): float { $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return 0.0; } $views = (int) get_post_meta($topic_id, '_aksrt_views', true); $favorites = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); $comments = (int) get_comments([ 'post_id' => $topic_id, 'count' => true, 'status' => 'approve', ]); $score = (int) get_post_meta($topic_id, '_aksrt_score', true); $w_views = 1.0; $w_favorites = 5.0; $w_comments = 3.0; $w_score = 2.0; $raw = ($views * $w_views) + ($favorites * $w_favorites) + ($comments * $w_comments) + (max(0, $score) * $w_score); $post_time = strtotime($post->post_date_gmt); $age_hours = max(1, (time() - $post_time) / 3600); $decay = pow($age_hours, 1.5); $recommend = ($raw + 1) / $decay * 10000; return round($recommend, 2); } public function refresh_recommend_score(int $topic_id): float { $score = $this->calculate_recommend_score($topic_id); update_post_meta($topic_id, '_aksrt_recommend_score', $score); $this->maybe_reward_hot_topic($topic_id, $score); return $score; } private function maybe_reward_hot_topic(int $topic_id, float $score): void { $config = $this->forum->get_config(); $hot_threshold = (float) ($config['post_page']['hot_recommend_threshold'] ?? $config['credits']['hot_recommend_threshold'] ?? $config['other']['hot_topic_threshold'] ?? 500); if ($hot_threshold <= 0) { $hot_threshold = 500.0; } if ($score < $hot_threshold) { return; } if (get_post_meta($topic_id, '_aksrt_hot_rewarded', true)) { return; } $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return; } $author_id = (int) $post->post_author; if ($author_id <= 0) { return; } do_action('aksrt_forum_topic_hot', $topic_id, (int) $author_id); update_post_meta($topic_id, '_aksrt_hot_rewarded', '1'); $amount = (int) ($config['credits']['posts_hot'] ?? 5); if ($amount > 0) { if (class_exists('\AKSRT\Credits\PointsFree')) { \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_posts_hot', $amount, [ 'type' => 'post', 'id' => $topic_id, ]); } elseif (class_exists('\AKSRT\Credits\Credits')) { \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_topic', [ 'type' => 'post', 'id' => $topic_id, ]); } } $this->notify($author_id, 'hot', sprintf( __('你的帖子「%s」成为热帖!', 'aksrt'), get_the_title($topic_id) ), [ 'title' => __('热帖通知', 'aksrt'), 'target_url' => get_permalink($topic_id), ]); } public function batch_refresh_recommend_scores(int $limit = 100): int { $query = new \WP_Query([ 'post_type' => 'aksrt_forum_topic', 'post_status' => 'publish', 'posts_per_page' => $limit, 'orderby' => 'modified', 'order' => 'DESC', 'fields' => 'ids', 'no_found_rows' => true, ]); $count = 0; foreach ($query->posts as $post_id) { $this->refresh_recommend_score((int) $post_id); $count++; } return $count; } public function maybe_refresh_recommend_on_view(): void { if (!is_singular('aksrt_forum_topic')) { return; } $topic_id = get_the_ID(); if (!$topic_id) { return; } $last = (int) get_post_meta($topic_id, '_aksrt_recommend_refreshed', true); if ((time() - $last) > 300) { $this->refresh_recommend_score($topic_id); update_post_meta($topic_id, '_aksrt_recommend_refreshed', time()); } } private function get_bbs_type_label(string $bbs_type): string { $config = $this->forum->get_config(); $names = $config['names'] ?? []; $map = [ Forum::BBS_TYPE_STANDARD => $names['posts'] ?? __('帖子', 'aksrt'), Forum::BBS_TYPE_QUESTION => $names['question_badge'] ?? __('提问', 'aksrt'), ]; return $map[$bbs_type] ?? $map[Forum::BBS_TYPE_STANDARD]; } private function get_question_status_label(int $status): string { $config = $this->forum->get_config(); $names = $config['names'] ?? []; return $status === Forum::QUESTION_SOLVED ? ($names['question_ok_badge'] ?? __('已解决', 'aksrt')) : ($names['question_badge'] ?? __('提问', 'aksrt')); } // ================================================================ // Notifications (§7.8) // ================================================================ public function notify(int $user_id, string $type, string $content, array $data = []): void { if ($user_id <= 0 || !class_exists('\AKSRT\Messages\Message')) { return; } \AKSRT\Messages\Message::get_instance()->send($user_id, $type, $content, $data); } public function notify_topic_pending(int $topic_id, int $author_id): void { $title = get_the_title($topic_id); $config = $this->forum->get_config(); $admin_users = $config['admin_users'] ?? []; foreach ($admin_users as $admin_id) { $this->notify((int) $admin_id, 'posts', sprintf( __('帖子「%s」待审核', 'aksrt'), $title ), [ 'title' => __('待审核帖子', 'aksrt'), 'target_url' => get_permalink($topic_id), ]); } } public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void { $post = get_post($topic_id); if (!$post) return; $author_id = (int) $post->post_author; if ($author_id <= 0 || $author_id === $operator_id) return; $title = get_the_title($topic_id); $action_labels = [ 'review' => __('审核状态已变更', 'aksrt'), 'sticky' => __('置顶状态已变更', 'aksrt'), 'featured' => __('精华状态已变更', 'aksrt'), 'locked' => __('锁定状态已变更', 'aksrt'), 'close' => __('帖子已关闭', 'aksrt'), 'hide' => __('帖子已隐藏', 'aksrt'), 'delete' => __('帖子已删除', 'aksrt'), 'undelete' => __('帖子已恢复', 'aksrt'), ]; if ($action === 'review') { $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); if ($review_status === Forum::REVIEW_REJECTED) { $action_labels['review'] = __('审核未通过', 'aksrt'); } elseif ($review_status === Forum::REVIEW_PENDING) { $action_labels['review'] = __('已转为待审核', 'aksrt'); } elseif ($review_status === Forum::REVIEW_APPROVED) { $action_labels['review'] = __('审核已通过', 'aksrt'); } } $label = $action_labels[$action] ?? __('治理动作已执行', 'aksrt'); $target_url = $action === 'delete' ? aksrt_get_forum_home_url() : get_permalink($topic_id); $payload = [ 'title' => $label, 'target_url' => $target_url, ]; $note = (string) get_post_meta($topic_id, '_aksrt_moderation_note', true); if ($note !== '') { $payload['reason'] = $note; } $this->notify($author_id, 'system', sprintf( __('你的帖子「%1$s」%2$s', 'aksrt'), $title, $label ), $payload); } public function notify_topic_reply(int $topic_id, int $commenter_id): void { $post = get_post($topic_id); if (!$post) return; $author_id = (int) $post->post_author; if ($author_id <= 0 || $author_id === $commenter_id) return; $commenter = get_userdata($commenter_id); $name = $commenter ? $commenter->display_name : __('有人', 'aksrt'); $title = get_the_title($topic_id); $this->notify($author_id, 'comment', sprintf( __('%1$s 回复了你的帖子「%2$s」', 'aksrt'), $name, $title ), [ 'title' => __('新回复', 'aksrt'), 'target_url' => get_permalink($topic_id), ]); } public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void { $post = get_post($topic_id); if (!$post) return; $author_id = (int) $post->post_author; if ($author_id <= 0 || $author_id === $scorer_id) return; $title = get_the_title($topic_id); $this->notify($author_id, 'system', sprintf( __('你的帖子「%1$s」被%2$s %3$d 分', 'aksrt'), $title, $amount > 0 ? __('加', 'aksrt') : __('减', 'aksrt'), abs($amount) ), [ 'title' => $amount > 0 ? __('帖子被加分', 'aksrt') : __('帖子被减分', 'aksrt'), 'target_url' => get_permalink($topic_id), ]); } // ================================================================ // Private helpers // ================================================================ private function today_credits_count(int $user_id, string $reason): int { if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) { return 0; } $table = \AKSRT\Core\DBMigrator::table('credit_log'); if (!$table) { return 0; } global $wpdb; $today = current_time('Y-m-d'); return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s", $user_id, $reason, $today )); } }