导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,413 @@
|
||||
<?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' => home_url('/forums/'),
|
||||
'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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Forum_Topic_Service_Part_2 {
|
||||
|
||||
|
||||
/**
|
||||
* Prepare vote meta from request data.
|
||||
*/
|
||||
public function prepare_vote_meta(array $data): array {
|
||||
$enabled = !empty($data['vote_enabled']);
|
||||
$title = sanitize_text_field((string) ($data['vote_title'] ?? ''));
|
||||
$multiple = !empty($data['vote_multiple']);
|
||||
$raw_options = $data['vote_options'] ?? $data['vote_options_text'] ?? [];
|
||||
|
||||
$labels = [];
|
||||
if (is_array($raw_options)) {
|
||||
foreach ($raw_options as $option) {
|
||||
$option = sanitize_text_field((string) $option);
|
||||
if ($option !== '') {
|
||||
$labels[] = $option;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$lines = preg_split('/\r\n|\r|\n/', (string) $raw_options) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = sanitize_text_field((string) $line);
|
||||
if ($line !== '') {
|
||||
$labels[] = $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$labels = array_values(array_unique($labels));
|
||||
|
||||
$config = $this->forum->get_config();
|
||||
$vote_max = (int) ($config['other']['vote_max'] ?? 8);
|
||||
if ($vote_max > 0 && count($labels) > $vote_max) {
|
||||
$labels = array_slice($labels, 0, $vote_max);
|
||||
}
|
||||
|
||||
$vote_type = sanitize_key((string) ($data['vote_type'] ?? 'standard'));
|
||||
if ($vote_type === 'pk' && count($labels) !== 2) {
|
||||
$vote_type = 'standard';
|
||||
}
|
||||
if ($vote_type === 'pk') {
|
||||
$multiple = false;
|
||||
}
|
||||
|
||||
if (count($labels) < 2) {
|
||||
$enabled = false;
|
||||
}
|
||||
|
||||
$expiry_days = (int) ($data['vote_expiry_days'] ?? 0);
|
||||
|
||||
$options = [];
|
||||
foreach ($labels as $index => $label) {
|
||||
$options[] = [
|
||||
'id' => 'option_' . ($index + 1),
|
||||
'label' => $label,
|
||||
'count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'title' => $title,
|
||||
'multiple' => $multiple,
|
||||
'vote_type' => $vote_type,
|
||||
'expiry_days' => $expiry_days,
|
||||
'options' => $options,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save vote meta while preserving counts when possible.
|
||||
*/
|
||||
public function save_vote_meta(int $topic_id, array $vote_meta): void {
|
||||
$existing_options = get_post_meta($topic_id, '_aksrt_vote_options', true);
|
||||
$existing_options = is_array($existing_options) ? $existing_options : [];
|
||||
$existing_by_label = [];
|
||||
foreach ($existing_options as $existing_option) {
|
||||
if (!empty($existing_option['label'])) {
|
||||
$existing_by_label[(string) $existing_option['label']] = $existing_option;
|
||||
}
|
||||
}
|
||||
|
||||
$options = [];
|
||||
foreach ($vote_meta['options'] as $index => $option) {
|
||||
$label = (string) ($option['label'] ?? '');
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = $existing_by_label[$label] ?? null;
|
||||
$options[] = [
|
||||
'id' => $existing['id'] ?? ('option_' . ($index + 1)),
|
||||
'label' => $label,
|
||||
'count' => (int) ($existing['count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
update_post_meta($topic_id, '_aksrt_vote_enabled', !empty($vote_meta['enabled']) ? '1' : '0');
|
||||
update_post_meta($topic_id, '_aksrt_vote_title', sanitize_text_field((string) ($vote_meta['title'] ?? '')));
|
||||
update_post_meta($topic_id, '_aksrt_vote_multiple', !empty($vote_meta['multiple']) ? '1' : '0');
|
||||
update_post_meta($topic_id, '_aksrt_vote_options', $options);
|
||||
$vote_type = in_array($vote_meta['vote_type'] ?? '', ['standard', 'pk'], true) ? $vote_meta['vote_type'] : 'standard';
|
||||
update_post_meta($topic_id, '_aksrt_vote_type', $vote_type);
|
||||
$expiry_days = (int) ($vote_meta['expiry_days'] ?? 0);
|
||||
if (!in_array($expiry_days, [0, 1, 7, 30], true)) {
|
||||
$expiry_days = 0;
|
||||
}
|
||||
update_post_meta($topic_id, '_aksrt_vote_expiry_days', $expiry_days);
|
||||
if ($expiry_days > 0 && !get_post_meta($topic_id, '_aksrt_vote_started_at', true)) {
|
||||
update_post_meta($topic_id, '_aksrt_vote_started_at', current_time('mysql'));
|
||||
}
|
||||
|
||||
if (empty($vote_meta['enabled'])) {
|
||||
delete_post_meta($topic_id, '_aksrt_vote_records');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vote payload for a topic.
|
||||
*/
|
||||
public function get_vote_payload(int $topic_id, int $user_id = 0): array {
|
||||
$enabled = get_post_meta($topic_id, '_aksrt_vote_enabled', true) === '1';
|
||||
$options = get_post_meta($topic_id, '_aksrt_vote_options', true);
|
||||
$options = is_array($options) ? $options : [];
|
||||
$records = get_post_meta($topic_id, '_aksrt_vote_records', true);
|
||||
$records = is_array($records) ? $records : [];
|
||||
|
||||
$vote_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_vote_type', true));
|
||||
if (!in_array($vote_type, ['standard', 'pk'], true)) {
|
||||
$vote_type = 'standard';
|
||||
}
|
||||
$expiry_days = (int) get_post_meta($topic_id, '_aksrt_vote_expiry_days', true);
|
||||
$started_at = (string) get_post_meta($topic_id, '_aksrt_vote_started_at', true);
|
||||
$expired = false;
|
||||
if ($expiry_days > 0 && $started_at !== '') {
|
||||
$expire_time = strtotime($started_at) + ($expiry_days * DAY_IN_SECONDS);
|
||||
$expired = time() >= $expire_time;
|
||||
}
|
||||
|
||||
$config = $this->forum->get_config();
|
||||
$display_mode = $config['other']['vote_number_type'] ?? 'percent';
|
||||
|
||||
$selected = $user_id > 0 && isset($records[(string) $user_id]) && is_array($records[(string) $user_id])
|
||||
? array_values(array_map('strval', $records[(string) $user_id]))
|
||||
: [];
|
||||
$total_votes = 0;
|
||||
|
||||
foreach ($options as &$option) {
|
||||
$count = max(0, (int) ($option['count'] ?? 0));
|
||||
$option['count'] = $count;
|
||||
$option['selected'] = in_array((string) ($option['id'] ?? ''), $selected, true);
|
||||
$total_votes += $count;
|
||||
}
|
||||
unset($option);
|
||||
|
||||
return [
|
||||
'enabled' => $enabled && !empty($options),
|
||||
'title' => (string) get_post_meta($topic_id, '_aksrt_vote_title', true),
|
||||
'multiple' => get_post_meta($topic_id, '_aksrt_vote_multiple', true) === '1',
|
||||
'vote_type' => $vote_type,
|
||||
'expiry_days' => $expiry_days,
|
||||
'expired' => $expired,
|
||||
'display_mode' => $display_mode,
|
||||
'options' => $options,
|
||||
'total_votes' => $total_votes,
|
||||
'has_voted' => !empty($selected),
|
||||
'selected' => $selected,
|
||||
];
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Content processing
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Save governance meta fields from an editable payload.
|
||||
*/
|
||||
public function save_topic_governance_meta(int $topic_id, array $data): void {
|
||||
if (isset($data['visibility'])) {
|
||||
$visibility = sanitize_key((string) $data['visibility']);
|
||||
if (isset($this->get_visibility_map()[$visibility])) {
|
||||
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('special_topic', $data)) {
|
||||
update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $data['special_topic']));
|
||||
}
|
||||
|
||||
if (array_key_exists('vote_enabled', $data) || array_key_exists('vote_options', $data) || array_key_exists('vote_options_text', $data)) {
|
||||
$this->save_vote_meta($topic_id, $this->prepare_vote_meta($data));
|
||||
}
|
||||
}
|
||||
|
||||
public function save_cover_meta(int $post_id, array $data): void {
|
||||
$cover_type = sanitize_key((string) ($data['cover_type'] ?? ''));
|
||||
if (!in_array($cover_type, ['image', 'slide', 'video'], true)) {
|
||||
delete_post_meta($post_id, '_aksrt_cover_type');
|
||||
return;
|
||||
}
|
||||
|
||||
update_post_meta($post_id, '_aksrt_cover_type', $cover_type);
|
||||
|
||||
if ($cover_type === 'image') {
|
||||
$url = esc_url_raw((string) ($data['cover_image'] ?? ''));
|
||||
update_post_meta($post_id, '_aksrt_cover_image', $url);
|
||||
} elseif ($cover_type === 'slide') {
|
||||
$raw = $data['featured_slide'] ?? '';
|
||||
$slides = is_array($raw) ? $raw : json_decode((string) $raw, true);
|
||||
if (!is_array($slides)) {
|
||||
$slides = [];
|
||||
}
|
||||
$slides = array_values(array_filter(array_map('esc_url_raw', array_slice($slides, 0, 9))));
|
||||
update_post_meta($post_id, '_aksrt_featured_slide', $slides);
|
||||
} elseif ($cover_type === 'video') {
|
||||
$url = esc_url_raw((string) ($data['featured_video'] ?? ''));
|
||||
update_post_meta($post_id, '_aksrt_featured_video', $url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize topic content based on user's compose capabilities.
|
||||
*
|
||||
* Strips <img>, <video>, <audio>, <iframe>, [aksrt_hide] blocks
|
||||
* when the author lacks the corresponding permission.
|
||||
*/
|
||||
public function sanitize_topic_content(int $user_id, string $content): string {
|
||||
$caps = $this->forum->get_compose_capabilities($user_id);
|
||||
|
||||
if (empty($caps['can_upload_image'])) {
|
||||
$content = preg_replace('/<img[^>]*>/i', '', $content);
|
||||
}
|
||||
|
||||
if (empty($caps['can_upload_video'])) {
|
||||
$content = preg_replace('/<video[^>]*>.*?<\/video>/is', '', $content);
|
||||
$content = preg_replace('/<audio[^>]*>.*?<\/audio>/is', '', $content);
|
||||
}
|
||||
|
||||
if (empty($caps['can_iframe_video'])) {
|
||||
$content = preg_replace('/<iframe[^>]*>.*?<\/iframe>/is', '', $content);
|
||||
}
|
||||
|
||||
if (empty($caps['can_set_hide'])) {
|
||||
$content = preg_replace('/\[aksrt_hide[^\]]*\](.*?)\[\/aksrt_hide\]/s', '$1', $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the [aksrt_hide] shortcode for hidden content blocks.
|
||||
*/
|
||||
public static function register_hide_shortcode(): void {
|
||||
add_shortcode('aksrt_hide', [self::class, 'render_hide_shortcode']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render [aksrt_hide] shortcode.
|
||||
*
|
||||
* Attributes:
|
||||
* type — 'login' (default) | 'comment' | 'role' | 'credits'
|
||||
* roles — comma-separated role slugs (for type=role)
|
||||
* cost — int credits cost (for type=credits)
|
||||
*/
|
||||
public static function render_hide_shortcode($atts, $content = ''): string {
|
||||
$atts = shortcode_atts([
|
||||
'type' => 'login',
|
||||
'roles' => '',
|
||||
'cost' => 0,
|
||||
], $atts, 'aksrt_hide');
|
||||
|
||||
$type = sanitize_key($atts['type']);
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
$visible = false;
|
||||
$message = '';
|
||||
|
||||
switch ($type) {
|
||||
case 'login':
|
||||
$visible = $user_id > 0;
|
||||
if (!$visible) {
|
||||
$message = __('此内容需要登录后才能查看。', 'aksrt');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
if ($user_id <= 0) {
|
||||
$message = __('登录并评论后可查看隐藏内容。', 'aksrt');
|
||||
} else {
|
||||
$post_id = get_the_ID();
|
||||
if ($post_id && function_exists('get_comments')) {
|
||||
$has_commented = (int) get_comments([
|
||||
'post_id' => $post_id,
|
||||
'user_id' => $user_id,
|
||||
'count' => true,
|
||||
'status' => 'approve',
|
||||
]) > 0;
|
||||
$visible = $has_commented;
|
||||
}
|
||||
if (!$visible) {
|
||||
$message = __('评论后可查看隐藏内容。', 'aksrt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'role':
|
||||
if ($user_id <= 0) {
|
||||
$message = __('此内容仅特定角色可见。', 'aksrt');
|
||||
} else {
|
||||
$allowed = array_filter(array_map('trim', explode(',', (string) $atts['roles'])));
|
||||
if (empty($allowed)) {
|
||||
$visible = true;
|
||||
} else {
|
||||
$user = get_userdata($user_id);
|
||||
$visible = $user && !empty(array_intersect($user->roles, $allowed));
|
||||
}
|
||||
if (!$visible) {
|
||||
$message = __('此内容仅限特定用户角色查看。', 'aksrt');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'credits':
|
||||
$cost = max(0, (int) $atts['cost']);
|
||||
if ($user_id <= 0) {
|
||||
$message = sprintf(__('此内容需要 %d 积分解锁。请先登录。', 'aksrt'), $cost);
|
||||
} elseif ($cost <= 0) {
|
||||
$visible = true;
|
||||
} else {
|
||||
$balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::get_balance($user_id) : 0;
|
||||
$visible = $balance >= $cost;
|
||||
if (!$visible) {
|
||||
$message = sprintf(__('此内容需要 %d 积分解锁(当前余额:%d)。', 'aksrt'), $cost, (int) $balance);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$visible = $user_id > 0;
|
||||
if (!$visible) {
|
||||
$message = __('此内容需要登录后才能查看。', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
if ($visible) {
|
||||
return '<div class="aksrt-forum-hide aksrt-forum-hide--revealed">' . do_shortcode($content) . '</div>';
|
||||
}
|
||||
|
||||
return '<div class="aksrt-forum-hide aksrt-forum-hide--locked">'
|
||||
. '<div class="aksrt-forum-hide__message">'
|
||||
. '<i class="fas fa-lock"></i> '
|
||||
. esc_html($message)
|
||||
. '</div></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a tag string.
|
||||
*/
|
||||
public function split_tags(string $tags): array {
|
||||
$parts = preg_split('/[,,]/', $tags) ?: [];
|
||||
$clean = [];
|
||||
foreach ($parts as $part) {
|
||||
$part = sanitize_text_field(trim($part));
|
||||
if ($part !== '') {
|
||||
$clean[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($clean));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Forum_Topic_Service_Part_3 {
|
||||
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Build a topic payload.
|
||||
*/
|
||||
public function build_topic_payload(WP_Post $post, int $viewer_user_id = 0): array {
|
||||
$topic_id = (int) $post->ID;
|
||||
$author_id = (int) $post->post_author;
|
||||
$board_terms = get_the_terms($topic_id, 'aksrt_forum_board');
|
||||
$tag_terms = get_the_terms($topic_id, 'aksrt_forum_tag');
|
||||
|
||||
$boards = [];
|
||||
if ($board_terms && !is_wp_error($board_terms)) {
|
||||
foreach ($board_terms as $board_term) {
|
||||
$board_link = get_term_link($board_term);
|
||||
$boards[] = [
|
||||
'id' => (int) $board_term->term_id,
|
||||
'name' => (string) $board_term->name,
|
||||
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
if ($tag_terms && !is_wp_error($tag_terms)) {
|
||||
foreach ($tag_terms as $tag_term) {
|
||||
$tag_link = get_term_link($tag_term);
|
||||
$tags[] = [
|
||||
'id' => (int) $tag_term->term_id,
|
||||
'name' => (string) $tag_term->name,
|
||||
'url' => !is_wp_error($tag_link) ? (string) $tag_link : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$topic_cat_terms = get_the_terms($topic_id, 'aksrt_forum_topic_cat');
|
||||
$topic_cat = null;
|
||||
if ($topic_cat_terms && !is_wp_error($topic_cat_terms)) {
|
||||
$first = $topic_cat_terms[0];
|
||||
$tc_link = get_term_link($first);
|
||||
$topic_cat = [
|
||||
'id' => (int) $first->term_id,
|
||||
'name' => (string) $first->name,
|
||||
'url' => !is_wp_error($tc_link) ? (string) $tc_link : '',
|
||||
];
|
||||
}
|
||||
|
||||
$visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true));
|
||||
if (!isset($this->get_visibility_map()[$visibility])) {
|
||||
$visibility = AKSRT_Forum::VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
$review_status = sanitize_key((string) get_post_meta($topic_id, '_aksrt_review_status', true));
|
||||
if (!isset($this->get_review_map()[$review_status])) {
|
||||
$review_status = $post->post_status === 'publish' ? AKSRT_Forum::REVIEW_APPROVED : AKSRT_Forum::REVIEW_PENDING;
|
||||
}
|
||||
|
||||
$can_view = $this->can_view_topic($post, $viewer_user_id, $visibility, $review_status);
|
||||
$excerpt = $post->post_excerpt !== ''
|
||||
? $post->post_excerpt
|
||||
: wp_trim_words(wp_strip_all_tags((string) $post->post_content), 40);
|
||||
|
||||
$sticky_level = $this->resolve_sticky_level($topic_id);
|
||||
$featured = get_post_meta($topic_id, '_aksrt_featured', true) === '1';
|
||||
$locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1';
|
||||
$bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true));
|
||||
if (!in_array($bbs_type, [AKSRT_Forum::BBS_TYPE_STANDARD, AKSRT_Forum::BBS_TYPE_QUESTION], true)) {
|
||||
$bbs_type = AKSRT_Forum::BBS_TYPE_STANDARD;
|
||||
}
|
||||
$question_status = $bbs_type === AKSRT_Forum::BBS_TYPE_QUESTION
|
||||
? (int) get_post_meta($topic_id, '_aksrt_question_status', true)
|
||||
: null;
|
||||
$favorite_count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
|
||||
$is_favorited = $viewer_user_id > 0 && $this->forum->is_favorited($viewer_user_id, $topic_id);
|
||||
$score = (int) get_post_meta($topic_id, '_aksrt_score', true);
|
||||
|
||||
$cover = $this->build_cover_payload($topic_id);
|
||||
|
||||
return [
|
||||
'id' => $topic_id,
|
||||
'title' => (string) get_the_title($topic_id),
|
||||
'excerpt' => (string) $excerpt,
|
||||
'content' => (string) $post->post_content,
|
||||
'status' => (string) $post->post_status,
|
||||
'author_id' => $author_id,
|
||||
'author' => $this->build_user_summary($author_id),
|
||||
'date' => get_the_date('Y-m-d H:i', $topic_id),
|
||||
'modified' => get_the_modified_date('Y-m-d H:i', $topic_id),
|
||||
'comment_count' => (int) get_comments([
|
||||
'post_id' => $topic_id,
|
||||
'count' => true,
|
||||
'status' => 'approve',
|
||||
]),
|
||||
'views' => (int) get_post_meta($topic_id, '_aksrt_views', true),
|
||||
'permalink' => (string) get_permalink($topic_id),
|
||||
'sticky' => $sticky_level > AKSRT_Forum::STICKY_NONE,
|
||||
'sticky_level' => $sticky_level,
|
||||
'sticky_label' => $this->get_sticky_label($sticky_level),
|
||||
'featured' => $featured,
|
||||
'locked' => $locked,
|
||||
'special_topic' => (string) get_post_meta($topic_id, '_aksrt_special_topic', true),
|
||||
'bbs_type' => $bbs_type,
|
||||
'question_status' => $question_status,
|
||||
'favorite_count' => $favorite_count,
|
||||
'is_favorited' => $is_favorited,
|
||||
'score' => $score,
|
||||
'cover' => $cover,
|
||||
'visibility' => [
|
||||
'key' => $visibility,
|
||||
'label' => $this->get_visibility_label($visibility),
|
||||
],
|
||||
'review_status' => [
|
||||
'key' => $review_status,
|
||||
'label' => $this->get_review_label($review_status),
|
||||
],
|
||||
'moderation_note' => (string) get_post_meta($topic_id, '_aksrt_moderation_note', true),
|
||||
'plate_id' => (int) get_post_meta($topic_id, '_aksrt_plate_id', true),
|
||||
'boards' => $boards,
|
||||
'tags' => $tags,
|
||||
'topic_cat' => $topic_cat,
|
||||
'vote' => $this->get_vote_payload($topic_id, $viewer_user_id),
|
||||
'can_view' => $can_view,
|
||||
'can_edit' => $this->forum->can_edit_topic($viewer_user_id, $topic_id),
|
||||
'can_moderate' => $this->forum->can_moderate_topic($viewer_user_id, $topic_id),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether current viewer can read a topic.
|
||||
*/
|
||||
public function can_view_topic(WP_Post $post, int $viewer_user_id, string $visibility, string $review_status): bool {
|
||||
if (user_can($viewer_user_id, 'manage_options')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($post->post_status !== 'publish' || $review_status !== AKSRT_Forum::REVIEW_APPROVED) {
|
||||
return $viewer_user_id > 0
|
||||
&& ((int) $post->post_author === $viewer_user_id || $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID));
|
||||
}
|
||||
|
||||
if ((int) $post->post_author === $viewer_user_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_PUBLIC) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_LOGIN) {
|
||||
return $viewer_user_id > 0;
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_COMMENT) {
|
||||
if ($viewer_user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
return $this->has_user_commented_on($viewer_user_id, (int) $post->ID);
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_ROLE) {
|
||||
if ($viewer_user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$allowed_roles = (array) get_post_meta((int) $post->ID, '_aksrt_visibility_roles', true);
|
||||
if (empty($allowed_roles)) {
|
||||
return true;
|
||||
}
|
||||
$user = get_userdata($viewer_user_id);
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
return !empty(array_intersect($user->roles, $allowed_roles));
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_CREDITS) {
|
||||
if ($viewer_user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID);
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_PAY) {
|
||||
if ($viewer_user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID);
|
||||
}
|
||||
|
||||
if ($visibility === AKSRT_Forum::VISIBILITY_MODERATOR) {
|
||||
if ($viewer_user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cover payload for a topic.
|
||||
*
|
||||
* Supports three cover types: image (single), slide (multiple images), video.
|
||||
* Falls back to WP featured image (thumbnail) when no explicit cover_type is set.
|
||||
*/
|
||||
public function build_cover_payload(int $topic_id): ?array {
|
||||
$cover_type = (string) get_post_meta($topic_id, '_aksrt_cover_type', true);
|
||||
|
||||
if ($cover_type === 'image') {
|
||||
$url = (string) get_post_meta($topic_id, '_aksrt_cover_image', true);
|
||||
if ($url === '') {
|
||||
$url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
|
||||
}
|
||||
if ($url === '') return null;
|
||||
|
||||
return [
|
||||
'type' => 'image',
|
||||
'url' => $url,
|
||||
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($cover_type === 'slide') {
|
||||
$slides = get_post_meta($topic_id, '_aksrt_featured_slide', true);
|
||||
if (!is_array($slides) || empty($slides)) return null;
|
||||
|
||||
return [
|
||||
'type' => 'slide',
|
||||
'images' => array_map('esc_url', array_slice($slides, 0, 9)),
|
||||
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($cover_type === 'video') {
|
||||
$video_url = (string) get_post_meta($topic_id, '_aksrt_featured_video', true);
|
||||
if ($video_url === '') return null;
|
||||
|
||||
$poster = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
|
||||
return [
|
||||
'type' => 'video',
|
||||
'url' => $video_url,
|
||||
'poster' => $poster,
|
||||
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
|
||||
];
|
||||
}
|
||||
|
||||
// Fallback: use WP featured image if set
|
||||
$thumb_url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
|
||||
if ($thumb_url !== '') {
|
||||
return [
|
||||
'type' => 'image',
|
||||
'url' => $thumb_url,
|
||||
'ratio' => '16:9',
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build user summary payload.
|
||||
*/
|
||||
public function build_user_summary(int $user_id): array {
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : false;
|
||||
|
||||
return [
|
||||
'id' => $user_id,
|
||||
'display_name' => $user ? $user->display_name : '',
|
||||
'user_email' => $user ? $user->user_email : '',
|
||||
'avatar_url' => $user ? $this->get_user_avatar_url($user_id) : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get avatar url.
|
||||
*/
|
||||
public function get_user_avatar_url(int $user_id): string {
|
||||
if (function_exists('aksrt_get_avatar_url')) {
|
||||
return (string) aksrt_get_avatar_url($user_id);
|
||||
}
|
||||
|
||||
return (string) get_avatar_url($user_id);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Topic unlock
|
||||
// ================================================================
|
||||
|
||||
public function unlock_topic_with_credits(int $user_id, int $topic_id): array {
|
||||
$post = get_post($topic_id);
|
||||
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
|
||||
return $this->forum->error(__('主题不存在', 'aksrt'));
|
||||
}
|
||||
|
||||
$visibility = (string) get_post_meta($topic_id, '_aksrt_visibility', true);
|
||||
if ($visibility !== AKSRT_Forum::VISIBILITY_CREDITS) {
|
||||
return $this->forum->error(__('该主题不需要积分解锁', 'aksrt'));
|
||||
}
|
||||
|
||||
if ($this->has_user_unlocked_topic($user_id, $topic_id)) {
|
||||
return $this->forum->success(__('已解锁', 'aksrt'));
|
||||
}
|
||||
|
||||
$cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true));
|
||||
if ($cost <= 0) {
|
||||
return $this->forum->error(__('积分价格未设置', 'aksrt'));
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Credits')) {
|
||||
return $this->forum->error(__('积分系统不可用', 'aksrt'));
|
||||
}
|
||||
|
||||
$balance = AKSRT_Credits::get_balance($user_id);
|
||||
if ($balance < $cost) {
|
||||
return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost));
|
||||
}
|
||||
|
||||
$deducted = AKSRT_Credits::deduct($user_id, $cost, 'forum_topic_unlock', [
|
||||
'topic_id' => $topic_id,
|
||||
], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_id)));
|
||||
|
||||
if (!$deducted) {
|
||||
return $this->forum->error(__('积分扣除失败', 'aksrt'));
|
||||
}
|
||||
|
||||
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true);
|
||||
if (!is_array($unlocked)) {
|
||||
$unlocked = [];
|
||||
}
|
||||
$unlocked[] = $topic_id;
|
||||
update_user_meta($user_id, 'aksrt_unlocked_topics', array_unique($unlocked));
|
||||
|
||||
$author_id = (int) $post->post_author;
|
||||
if ($author_id > 0 && $author_id !== $user_id) {
|
||||
$author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70));
|
||||
$author_amount = (int) floor($cost * $author_share / 100);
|
||||
if ($author_amount > 0) {
|
||||
AKSRT_Credits::add($author_id, $author_amount, 'forum_content_income', [
|
||||
'topic_id' => $topic_id,
|
||||
'buyer_id' => $user_id,
|
||||
], sprintf(__('帖子「%s」被付费查看', 'aksrt'), get_the_title($topic_id)));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->forum->success(__('解锁成功', 'aksrt'));
|
||||
}
|
||||
|
||||
public function has_user_unlocked_topic(int $user_id, int $post_id): bool {
|
||||
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true);
|
||||
if (!is_array($unlocked)) {
|
||||
return false;
|
||||
}
|
||||
return in_array($post_id, $unlocked, true);
|
||||
}
|
||||
|
||||
public function has_user_commented_on(int $user_id, int $post_id): bool {
|
||||
global $wpdb;
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_post_ID = %d AND user_id = %d AND comment_approved = '1' LIMIT 1",
|
||||
$post_id,
|
||||
$user_id
|
||||
));
|
||||
return $count > 0;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Status helpers
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* Normalize post status filter.
|
||||
*
|
||||
* @return array<int,string>|string
|
||||
*/
|
||||
public function normalize_post_status_query(string $status) {
|
||||
if ($status === 'all') {
|
||||
return ['publish', 'pending', 'draft'];
|
||||
}
|
||||
|
||||
return $status !== '' ? $status : 'publish';
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility labels.
|
||||
*/
|
||||
public function get_visibility_map(): array {
|
||||
return [
|
||||
AKSRT_Forum::VISIBILITY_PUBLIC => __('公开可见', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_LOGIN => __('登录可见', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_COMMENT => __('评论可见', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_ROLE => __('角色可见', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_CREDITS => __('积分查看', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_PAY => __('付费查看', 'aksrt'),
|
||||
AKSRT_Forum::VISIBILITY_MODERATOR => __('版主可见', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Review labels.
|
||||
*/
|
||||
public function get_review_map(): array {
|
||||
return [
|
||||
AKSRT_Forum::REVIEW_APPROVED => __('已通过', 'aksrt'),
|
||||
AKSRT_Forum::REVIEW_PENDING => __('待审核', 'aksrt'),
|
||||
AKSRT_Forum::REVIEW_REJECTED => __('已拒绝', 'aksrt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility label.
|
||||
*/
|
||||
public function get_visibility_label(string $visibility): string {
|
||||
$map = $this->get_visibility_map();
|
||||
return $map[$visibility] ?? $map[AKSRT_Forum::VISIBILITY_PUBLIC];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get review label.
|
||||
*/
|
||||
public function get_review_label(string $review_status): string {
|
||||
$map = $this->get_review_map();
|
||||
return $map[$review_status] ?? $map[AKSRT_Forum::REVIEW_PENDING];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visibility options.
|
||||
*/
|
||||
public function get_visibility_options(): array {
|
||||
$options = [];
|
||||
foreach ($this->get_visibility_map() as $value => $label) {
|
||||
$options[] = ['label' => $label, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
trait AKSRT_Forum_Topic_Service_Part_4 {
|
||||
|
||||
|
||||
/**
|
||||
* Get review options.
|
||||
*/
|
||||
public function get_review_options(): array {
|
||||
$options = [];
|
||||
foreach ($this->get_review_map() as $value => $label) {
|
||||
$options[] = ['label' => $label, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a user can publish directly.
|
||||
*/
|
||||
public function can_user_publish_directly(int $user_id, int $board_id = 0): bool {
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user_can($user_id, 'manage_options')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->forum->is_forum_admin($user_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($board_id > 0 && $this->forum->is_board_moderator($user_id, $board_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── AJAX edit (extracted from facade) ──
|
||||
|
||||
public function ajax_edit_topic(): void {
|
||||
check_ajax_referer('aksrt_forum_compose', '_nonce');
|
||||
$user_id = get_current_user_id();
|
||||
$topic_id = (int) ($_POST['topic_id'] ?? 0);
|
||||
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
if (!$this->forum->can_edit_topic($user_id, $topic_id)) {
|
||||
wp_send_json_error(['message' => __('无编辑权限', 'aksrt')]);
|
||||
}
|
||||
|
||||
$title = sanitize_text_field((string) ($_POST['title'] ?? ''));
|
||||
$content = wp_kses_post((string) ($_POST['content'] ?? ''));
|
||||
if ($title === '' || $content === '') {
|
||||
wp_send_json_error(['message' => __('标题和内容不能为空', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = wp_update_post([
|
||||
'ID' => $topic_id,
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
], true);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
}
|
||||
|
||||
$tags = $this->split_tags((string) ($_POST['tags'] ?? ''));
|
||||
$clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5);
|
||||
wp_set_object_terms($topic_id, $clean_tags, 'aksrt_forum_tag');
|
||||
|
||||
if (isset($_POST['special_topic'])) {
|
||||
update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $_POST['special_topic']));
|
||||
}
|
||||
|
||||
if (isset($_POST['topic_cat']) && taxonomy_exists('aksrt_forum_topic_cat')) {
|
||||
$topic_cat = sanitize_text_field((string) $_POST['topic_cat']);
|
||||
wp_set_object_terms($topic_id, $topic_cat !== '' ? [$topic_cat] : [], 'aksrt_forum_topic_cat');
|
||||
}
|
||||
|
||||
$caps = $this->forum->get_compose_capabilities($user_id);
|
||||
|
||||
if ($caps['can_set_visibility'] && isset($_POST['visibility'])) {
|
||||
$visibility = sanitize_key((string) $_POST['visibility']);
|
||||
if (isset($this->get_visibility_map()[$visibility])) {
|
||||
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
|
||||
if ($visibility === self::VISIBILITY_CREDITS) {
|
||||
update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($_POST['credits_cost'] ?? 0)));
|
||||
}
|
||||
if ($visibility === self::VISIBILITY_PAY) {
|
||||
update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($_POST['pay_price'] ?? '')));
|
||||
}
|
||||
if ($visibility === self::VISIBILITY_ROLE) {
|
||||
$roles = isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
|
||||
? array_map('sanitize_key', $_POST['visibility_roles']) : [];
|
||||
update_post_meta($topic_id, '_aksrt_visibility_roles', $roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($caps['can_set_cover']) {
|
||||
$this->save_cover_meta($topic_id, [
|
||||
'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')),
|
||||
'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')),
|
||||
'featured_slide' => (string) ($_POST['featured_slide'] ?? ''),
|
||||
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($caps['can_set_vote'] && isset($_POST['vote_enabled'])) {
|
||||
$vote_data = [
|
||||
'vote_enabled' => !empty($_POST['vote_enabled']),
|
||||
'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')),
|
||||
'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')),
|
||||
'vote_multiple' => !empty($_POST['vote_multiple']),
|
||||
'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')),
|
||||
'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0),
|
||||
];
|
||||
$vote_meta = $this->prepare_vote_meta($vote_data);
|
||||
$this->save_vote_meta($topic_id, $vote_meta);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => __('帖子已更新', 'aksrt'),
|
||||
'redirect' => get_permalink($topic_id),
|
||||
]);
|
||||
}
|
||||
|
||||
public function can_edit_topic(int $user_id, int $topic_id): bool {
|
||||
if ($user_id <= 0 || $topic_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
// Moderators and above can always edit
|
||||
if ($this->forum->forum_user_can($user_id, 'topic_edit_other', $topic_id)) {
|
||||
return true;
|
||||
}
|
||||
// Authors can edit their own unlocked topics
|
||||
$post = get_post($topic_id);
|
||||
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
|
||||
return false;
|
||||
}
|
||||
if ((int) $post->post_author !== $user_id) {
|
||||
return false;
|
||||
}
|
||||
return get_post_meta($topic_id, '_aksrt_locked', true) !== '1';
|
||||
}
|
||||
|
||||
public function update_topic_meta(int $topic_id, array $meta): bool {
|
||||
if ($topic_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->forum->can_moderate_topic(get_current_user_id(), $topic_id) && !current_user_can('manage_options')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_keys = ['_aksrt_sticky', '_aksrt_featured', '_aksrt_locked', '_aksrt_highlight'];
|
||||
foreach ($meta as $key => $value) {
|
||||
if (!in_array($key, $allowed_keys, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
update_post_meta($topic_id, $key, !empty($value) ? '1' : '0');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function ajax_update_topic(): void {
|
||||
$user_id = get_current_user_id();
|
||||
$topic_id = (int) ($_POST['topic_id'] ?? 0);
|
||||
if ($topic_id <= 0 || !$this->forum->forum_user_can($user_id, 'topic_moderate', $topic_id)) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if (isset($_POST['sticky'])) {
|
||||
$meta['_aksrt_sticky'] = !empty($_POST['sticky']);
|
||||
}
|
||||
if (isset($_POST['featured'])) {
|
||||
$meta['_aksrt_featured'] = !empty($_POST['featured']);
|
||||
}
|
||||
if (isset($_POST['locked'])) {
|
||||
$meta['_aksrt_locked'] = !empty($_POST['locked']);
|
||||
}
|
||||
|
||||
if ($this->update_topic_meta($topic_id, $meta)) {
|
||||
wp_send_json_success(['message' => __('已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('更新失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function ajax_create_topic(): void {
|
||||
check_ajax_referer('aksrt_forum_compose', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'board_id' => (int) ($_POST['board_id'] ?? 0),
|
||||
'title' => sanitize_text_field((string) ($_POST['title'] ?? '')),
|
||||
'content' => wp_kses_post((string) ($_POST['content'] ?? '')),
|
||||
'tags' => $this->split_tags((string) ($_POST['tags'] ?? '')),
|
||||
'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')),
|
||||
'visibility' => sanitize_key((string) ($_POST['visibility'] ?? self::VISIBILITY_PUBLIC)),
|
||||
'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? self::BBS_TYPE_STANDARD)),
|
||||
'topic_cat' => sanitize_text_field((string) ($_POST['topic_cat'] ?? '')),
|
||||
'vote_enabled' => !empty($_POST['vote_enabled']),
|
||||
'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')),
|
||||
'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')),
|
||||
'vote_multiple' => !empty($_POST['vote_multiple']),
|
||||
'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')),
|
||||
'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')),
|
||||
'featured_slide' => (string) ($_POST['featured_slide'] ?? ''),
|
||||
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
|
||||
'credits_cost' => (int) ($_POST['credits_cost'] ?? 0),
|
||||
'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')),
|
||||
'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
|
||||
? array_map('sanitize_key', $_POST['visibility_roles']) : [],
|
||||
];
|
||||
|
||||
$draft_id = (int) ($_POST['draft_id'] ?? 0);
|
||||
if ($draft_id > 0) {
|
||||
$result = $this->forum->publish_draft($user_id, $draft_id, $data);
|
||||
} else {
|
||||
$result = $this->create_topic($user_id, $data);
|
||||
}
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
wp_send_json_error(['message' => $result->get_error_message()]);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => (string) ($result['message'] ?? __('主题已提交', 'aksrt')),
|
||||
'post_id' => (int) ($result['post_id'] ?? 0),
|
||||
'redirect' => (string) ($result['redirect'] ?? home_url('/forums/')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function ajax_unlock_topic(): void {
|
||||
check_ajax_referer('aksrt_forum_action', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$topic_id = (int) ($_POST['topic_id'] ?? 0);
|
||||
if ($topic_id <= 0) {
|
||||
wp_send_json_error(['message' => __('主题不存在', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = $this->unlock_topic_with_credits($user_id, $topic_id);
|
||||
if (!empty($result['success'])) {
|
||||
wp_send_json_success($result);
|
||||
} else {
|
||||
wp_send_json_error(['message' => $result['message'] ?? __('解锁失败', 'aksrt')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user