post_type !== 'aksrt_forum_topic') { return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); } if (!$this->forum->can_moderate_topic($operator_id, $topic_id)) { return new \WP_Error('forbidden', __('无权执行该治理动作', 'aksrt')); } $action = sanitize_key($action); $note = sanitize_textarea_field((string) ($payload['note'] ?? '')); switch ($action) { case 'move_board': $target_board_id = (int) ($payload['target_board_id'] ?? 0); $target_ok = $this->assert_board($target_board_id, __('目标版块不存在', 'aksrt')); if (is_wp_error($target_ok)) { return $target_ok; } if ($this->forum->is_plate_cpt_active()) { $previous_plate_id = (int) $this->forum->get_topic_plate_id($topic_id); $this->attach_topic_to_board($topic_id, $target_board_id); if ($previous_plate_id > 0 && $previous_plate_id !== $target_board_id) { $this->forum->sync_board_count($previous_plate_id); } $this->forum->sync_board_count($target_board_id); } else { $current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); if (is_wp_error($current_board_ids)) { $current_board_ids = []; } $this->attach_topic_to_board($topic_id, $target_board_id); foreach ($current_board_ids as $current_board_id) { $this->forum->sync_board_count((int) $current_board_id); } $this->forum->sync_board_count($target_board_id); } break; case 'visibility': $visibility = sanitize_key((string) ($payload['visibility'] ?? '')); if (!isset($this->get_visibility_map()[$visibility])) { return new \WP_Error('invalid_visibility', __('可见性参数无效', 'aksrt')); } update_post_meta($topic_id, '_aksrt_visibility', $visibility); if ($visibility === Forum::VISIBILITY_CREDITS) { update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($payload['credits_cost'] ?? 0))); delete_post_meta($topic_id, '_aksrt_pay_price'); delete_post_meta($topic_id, '_aksrt_visibility_roles'); } elseif ($visibility === Forum::VISIBILITY_PAY) { update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($payload['pay_price'] ?? ''))); delete_post_meta($topic_id, '_aksrt_credits_cost'); delete_post_meta($topic_id, '_aksrt_visibility_roles'); } elseif ($visibility === Forum::VISIBILITY_ROLE) { $roles = isset($payload['visibility_roles']) && is_array($payload['visibility_roles']) ? array_values(array_filter(array_map('sanitize_key', $payload['visibility_roles']))) : []; update_post_meta($topic_id, '_aksrt_visibility_roles', $roles); delete_post_meta($topic_id, '_aksrt_credits_cost'); delete_post_meta($topic_id, '_aksrt_pay_price'); } else { delete_post_meta($topic_id, '_aksrt_credits_cost'); delete_post_meta($topic_id, '_aksrt_pay_price'); delete_post_meta($topic_id, '_aksrt_visibility_roles'); } break; case 'review': $review_status = sanitize_key((string) ($payload['review_status'] ?? '')); if (!isset($this->get_review_map()[$review_status])) { return new \WP_Error('invalid_review_status', __('审核状态参数无效', 'aksrt')); } $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); update_post_meta($topic_id, '_aksrt_review_status', $review_status); if ($review_status === Forum::REVIEW_APPROVED) { wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); } elseif ($review_status === Forum::REVIEW_PENDING) { wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); } else { wp_update_post(['ID' => $topic_id, 'post_status' => 'draft']); } foreach ($board_ids as $board_id) { $this->forum->sync_board_count((int) $board_id); } break; case 'sticky': { $raw = isset($payload['value']) ? $payload['value'] : null; if ($raw === false || $raw === null) { $level = Forum::STICKY_NONE; } elseif (is_numeric($raw)) { $level = (int) $raw; } else { $level = !empty($raw) ? Forum::STICKY_PLATE : Forum::STICKY_NONE; } $level = max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, $level)); update_post_meta($topic_id, '_aksrt_sticky', $level); break; } case 'featured': { $prev = get_post_meta($topic_id, '_aksrt_featured', true) === '1'; $next = !empty($payload['value']); update_post_meta($topic_id, '_aksrt_featured', $next ? '1' : '0'); // 精华联动:从「未设精华」→「设为精华」时触发积分奖励 + 站内信通知(§7.3/§7.7/§7.8)。 // 从「精华」→「取消」时不回收积分,仅不重复发放。 if ($next && !$prev) { $this->reward_essence($topic_id, $operator_id); } break; } case 'locked': { $locked = !empty($payload['value']); update_post_meta($topic_id, '_aksrt_locked', $locked ? '1' : '0'); wp_update_post([ 'ID' => $topic_id, 'comment_status' => $locked ? 'closed' : 'open', ]); break; } case 'question_status': { $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); if ($bbs_type !== Forum::BBS_TYPE_QUESTION) { return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); } $status = (int) ($payload['value'] ?? Forum::QUESTION_UNSOLVED); $status = in_array($status, [Forum::QUESTION_UNSOLVED, Forum::QUESTION_SOLVED], true) ? $status : Forum::QUESTION_UNSOLVED; update_post_meta($topic_id, '_aksrt_question_status', $status); break; } case 'close': { $closed = !empty($payload['value']); if ($closed) { update_post_meta($topic_id, '_aksrt_closed', '1'); wp_update_post(['ID' => $topic_id, 'comment_status' => 'closed']); } else { delete_post_meta($topic_id, '_aksrt_closed'); $locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1'; if (!$locked) { wp_update_post(['ID' => $topic_id, 'comment_status' => 'open']); } } break; } case 'hide': { $hidden = !empty($payload['value']); if ($hidden) { update_post_meta($topic_id, '_aksrt_hidden', '1'); $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); wp_update_post(['ID' => $topic_id, 'post_status' => 'private']); foreach ($board_ids as $bid) { $this->forum->sync_board_count((int) $bid); } } else { delete_post_meta($topic_id, '_aksrt_hidden'); $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); foreach ($board_ids as $bid) { $this->forum->sync_board_count((int) $bid); } } break; } case 'delete': { $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); wp_trash_post($topic_id); foreach ($board_ids as $bid) { $this->forum->sync_board_count((int) $bid); } $author_id = (int) $post->post_author; if ($author_id > 0 && $author_id !== $operator_id) { $this->forum->notify($author_id, 'system', sprintf( __('你的帖子「%s」已被删除', 'aksrt'), $post->post_title ), [ 'title' => __('帖子已删除', 'aksrt'), 'target_url' => aksrt_get_forum_home_url(), 'reason' => $note, ]); } break; } case 'undelete': { if ($post->post_status !== 'trash') { return new \WP_Error('not_trashed', __('该帖不在回收站中', 'aksrt')); } wp_untrash_post($topic_id); $review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true); if ($review_status === Forum::REVIEW_APPROVED) { wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']); } else { wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']); } $board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']); foreach ($board_ids as $bid) { $this->forum->sync_board_count((int) $bid); } break; } default: return new \WP_Error('invalid_action', __('不支持的治理动作', 'aksrt')); } if ($note !== '') { update_post_meta($topic_id, '_aksrt_moderation_note', $note); } update_post_meta($topic_id, '_aksrt_moderated_by', $operator_id); update_post_meta($topic_id, '_aksrt_moderated_at', current_time('mysql')); $this->forum->notify_moderation_result($topic_id, $action, $operator_id); return [ 'message' => __('治理动作已执行', 'aksrt'), 'topic' => $this->build_topic_payload(get_post($topic_id), $operator_id), ]; } /** * 精华联动奖励(§7.3 联动 + §7.7 被设精华奖励 + §7.8 精华通知)。 * * - 触发积分奖励:读 `credits.essence` 配置,有每日上限与去重 meta(防止重复发放)。 * - 触发站内信通知作者(依赖 \AKSRT\Messages\Message inbox 子系统)。 * - 缺失依赖(积分模块未启用/未配置)时静默跳过,不阻塞治理动作。 */ public function reward_essence(int $topic_id, int $operator_id): void { $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; } // 去重:同一帖已发过精华奖励则跳过(取消再加不重复发)。 if (get_post_meta($topic_id, '_aksrt_essence_rewarded', true) === '1') { return; } // 积分奖励(§7.7):统一走免费积分日上限。 if (class_exists('\AKSRT\Settings\Settings')) { $cfg = \AKSRT\Settings\Settings::get_forum_settings(); $amount = (int) ($cfg['credits']['essence'] ?? 0); if ($amount > 0) { $granted = 0; if (class_exists('\AKSRT\Credits\PointsFree')) { $result = \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_essence', $amount, [ 'type' => 'post', 'id' => $topic_id, ]); $granted = is_wp_error($result) ? 0 : (int) $result; } elseif (class_exists('\AKSRT\Credits\Credits')) { $daily_max = (int) ($cfg['credits']['daily_max'] ?? 50); if ($daily_max > 0 && $this->today_credits_count($author_id, 'forum_essence') >= $daily_max) { $amount = 0; } if ($amount > 0) { \AKSRT\Credits\Credits::add($author_id, $amount, 'forum_essence', [ 'type' => 'post', 'id' => $topic_id, ], __('主题被设为精华', 'aksrt')); $granted = $amount; } } if ($granted > 0) { update_post_meta($topic_id, '_aksrt_essence_rewarded', '1'); update_post_meta($topic_id, '_aksrt_essence_rewarded_at', current_time('mysql')); } } } // 站内信通知作者(§7.8)。 if (class_exists('\AKSRT\Messages\Message')) { $title = get_the_title($topic_id); $permalink = (string) get_permalink($topic_id); \AKSRT\Messages\Message::get_instance()->send( $author_id, 'forum_essence', sprintf(__('你的主题「%s」被设为精华', 'aksrt'), $title), [ 'title' => __('主题被设为精华', 'aksrt'), 'target_url' => $permalink, ] ); } do_action('aksrt_forum_topic_featured', $topic_id, $author_id, $operator_id); } /** * 今日某 reason 的积分发放次数(用于精华/被加分等每日上限判断)。 */ public 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 )); } /** * 置顶分级 label(对标 zibll zib_bbs_get_topping_badge)。 */ public function get_sticky_label(int $level): string { switch ($level) { case Forum::STICKY_GLOBAL: return __('全局置顶', 'aksrt'); case Forum::STICKY_SUPER: return __('超级置顶', 'aksrt'); case Forum::STICKY_PLATE: return __('置顶', 'aksrt'); default: return ''; } } /** * 读取并规范化置顶分级 meta。 * * 兼容历史 0/1 二态 meta("1" → STICKY_PLATE),新数据存 0~3 整数。 */ public function resolve_sticky_level(int $topic_id): int { $raw = get_post_meta($topic_id, '_aksrt_sticky', true); if ($raw === '' || $raw === null) { return Forum::STICKY_NONE; } if (is_numeric($raw)) { return max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, (int) $raw)); } return $raw === '1' ? Forum::STICKY_PLATE : Forum::STICKY_NONE; } // ================================================================ // Voting system // ================================================================ /** * Submit vote. * * @return array|\WP_Error */ public function submit_vote(int $topic_id, int $user_id, array $option_ids) { 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')); } $vote = $this->get_vote_payload($topic_id, $user_id); if (empty($vote['enabled'])) { return new \WP_Error('vote_disabled', __('当前主题未开启投票', 'aksrt')); } if (!empty($vote['expired'])) { return new \WP_Error('vote_expired', __('投票已结束', 'aksrt')); } if (!empty($vote['has_voted'])) { return new \WP_Error('already_voted', __('你已经参与过本次投票', 'aksrt')); } $selected = array_values(array_unique(array_filter(array_map('sanitize_text_field', $option_ids)))); if (empty($selected)) { return new \WP_Error('empty_selection', __('请选择至少一个投票选项', 'aksrt')); } if (empty($vote['multiple']) && count($selected) > 1) { return new \WP_Error('too_many_selection', __('当前投票仅支持单选', 'aksrt')); } $options = get_post_meta($topic_id, '_aksrt_vote_options', true); $options = is_array($options) ? $options : []; $valid_option_ids = wp_list_pluck($options, 'id'); foreach ($selected as $selected_id) { if (!in_array($selected_id, $valid_option_ids, true)) { return new \WP_Error('invalid_option', __('投票选项不存在', 'aksrt')); } } $records = get_post_meta($topic_id, '_aksrt_vote_records', true); $records = is_array($records) ? $records : []; $records[(string) $user_id] = $selected; update_post_meta($topic_id, '_aksrt_vote_records', $records); foreach ($options as &$option) { if (in_array((string) ($option['id'] ?? ''), $selected, true)) { $option['count'] = max(0, (int) ($option['count'] ?? 0)) + 1; } } unset($option); update_post_meta($topic_id, '_aksrt_vote_options', $options); return [ 'message' => __('投票成功', 'aksrt'), 'vote' => $this->get_vote_payload($topic_id, $user_id), ]; } }