Files
wordpress-theme-aksrt/src/AKSRT/Forum/TopicServicePart1.php
T
kate522 2ce1b550ff fix(forum): accept plate CPT IDs when creating topics
Compose UI and board search already send aksrt_forum_plate post IDs as
board_id, but create_topic still validated aksrt_forum_board term IDs and
never wrote _aksrt_plate_id — so real plates failed with 版块不存在.
Validate/store under the plate model (and fix move_board the same way).
2026-07-17 18:01:24 +08:00

442 lines
19 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 TopicServicePart1 {
public function moderate_topic(int $topic_id, string $action, array $payload, int $operator_id = 0) {
$post = get_post($topic_id);
if (!$post || $post->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<string,mixed>|\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),
];
}
}