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).
442 lines
19 KiB
PHP
442 lines
19 KiB
PHP
<?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),
|
||
];
|
||
}
|
||
}
|