Files
wordpress-theme-aksrt/inc/classes/forum/topic-service-traits/trait-aksrt-forum-topic-service-part-1.php
T
kate522 780c8cb4f7 feat(forum): frontend 1:1 list/tabs/urls/enqueue
Wire Zibll-style board sort tabs, shared topic rows, page-template
URLs, forum asset enqueue on home/plate, home synthesis load-more,
and topic last_reply meta for last-reply ordering. No dual-read.
2026-07-12 08:38:31 +08:00

414 lines
18 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
defined('ABSPATH') || exit;
trait AKSRT_Forum_Topic_Service_Part_1 {
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_board = get_term($target_board_id, 'aksrt_forum_board');
if (!$target_board || is_wp_error($target_board)) {
return new WP_Error('invalid_board', __('目标版块不存在', 'aksrt'));
}
$current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
wp_set_object_terms($topic_id, [$target_board_id], 'aksrt_forum_board');
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 === AKSRT_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 === AKSRT_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 === AKSRT_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 === AKSRT_Forum::REVIEW_APPROVED) {
wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']);
} elseif ($review_status === AKSRT_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 = AKSRT_Forum::STICKY_NONE;
} elseif (is_numeric($raw)) {
$level = (int) $raw;
} else {
$level = !empty($raw) ? AKSRT_Forum::STICKY_PLATE : AKSRT_Forum::STICKY_NONE;
}
$level = max(AKSRT_Forum::STICKY_NONE, min(AKSRT_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 !== AKSRT_Forum::BBS_TYPE_QUESTION) {
return new WP_Error('not_question', __('该帖不是提问类型', 'aksrt'));
}
$status = (int) ($payload['value'] ?? AKSRT_Forum::QUESTION_UNSOLVED);
$status = in_array($status, [AKSRT_Forum::QUESTION_UNSOLVED, AKSRT_Forum::QUESTION_SOLVED], true)
? $status : AKSRT_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 === AKSRT_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_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):读 `credits.essence`,带每日上限。
if (class_exists('AKSRT_Credits') && class_exists('AKSRT_Settings')) {
$cfg = AKSRT_Settings::get_forum_settings();
$amount = (int) ($cfg['credits']['essence'] ?? 0);
if ($amount > 0) {
$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::add($author_id, $amount, 'forum_essence', [
'type' => 'post',
'id' => $topic_id,
], __('主题被设为精华', 'aksrt'));
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_Message')) {
$title = get_the_title($topic_id);
$permalink = (string) get_permalink($topic_id);
AKSRT_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_DB_Migrator') || $user_id <= 0) {
return 0;
}
$table = AKSRT_DB_Migrator::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 AKSRT_Forum::STICKY_GLOBAL:
return __('全局置顶', 'aksrt');
case AKSRT_Forum::STICKY_SUPER:
return __('超级置顶', 'aksrt');
case AKSRT_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 AKSRT_Forum::STICKY_NONE;
}
if (is_numeric($raw)) {
return max(AKSRT_Forum::STICKY_NONE, min(AKSRT_Forum::STICKY_GLOBAL, (int) $raw));
}
return $raw === '1' ? AKSRT_Forum::STICKY_PLATE : AKSRT_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),
];
}
}