导入 AKSRT 主题开发源码

This commit is contained in:
Claude Code committed 2026-07-03 22:50:34 +08:00
1 parent 9681dd3d48
commit 1268a55dd0
595 files changed
+127705

No files matched your search

@@ -0,0 +1,419 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Forum_Board_Service_Part_1 {
public function create_plate(array $data): int {
$title = sanitize_text_field((string) ($data['name'] ?? ''));
if ($title === '') {
return 0;
}
$plate_id = wp_insert_post([
'post_type' => 'aksrt_forum_plate',
'post_title' => $title,
'post_content' => wp_kses_post((string) ($data['content'] ?? '')),
'post_excerpt' => sanitize_textarea_field((string) ($data['description'] ?? '')),
'post_status' => 'publish',
'post_parent' => (int) ($data['parent_id'] ?? 0),
'post_author' => (int) ($data['author_id'] ?? get_current_user_id()),
'menu_order' => (int) ($data['sort_order'] ?? 0),
], true);
if (is_wp_error($plate_id)) {
return 0;
}
$icon = sanitize_text_field((string) ($data['icon'] ?? ''));
if ($icon !== '') {
update_post_meta($plate_id, '_aksrt_icon', $icon);
}
update_post_meta($plate_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0));
update_post_meta($plate_id, '_aksrt_posts_count', 0);
update_post_meta($plate_id, '_aksrt_reply_count', 0);
update_post_meta($plate_id, '_aksrt_follow_count', 0);
update_post_meta($plate_id, '_aksrt_plate_views', 0);
if (!empty($data['plate_cat'])) {
wp_set_object_terms($plate_id, [(int) $data['plate_cat']], 'aksrt_plate_cat');
}
// Also create a corresponding taxonomy term for backward compat
$term_result = wp_insert_term($title, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($data['description'] ?? '')),
]);
if (!is_wp_error($term_result)) {
$term_id = (int) $term_result['term_id'];
update_term_meta($term_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0));
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_post_meta($plate_id, '_aksrt_migrated_from_term', $term_id);
}
return $plate_id;
}
/**
* Update plate.
*/
public function update_plate(int $plate_id, array $data): bool {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return false;
}
$update = ['ID' => $plate_id];
if (isset($data['name'])) {
$name = sanitize_text_field((string) $data['name']);
if ($name !== '') {
$update['post_title'] = $name;
}
}
if (isset($data['description'])) {
$update['post_excerpt'] = sanitize_textarea_field((string) $data['description']);
}
if (isset($data['content'])) {
$update['post_content'] = wp_kses_post((string) $data['content']);
}
if (isset($data['parent_id'])) {
$update['post_parent'] = (int) $data['parent_id'];
}
if (isset($data['sort_order'])) {
$update['menu_order'] = (int) $data['sort_order'];
update_post_meta($plate_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
if (isset($data['author_id'])) {
$update['post_author'] = (int) $data['author_id'];
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return false;
}
if (isset($data['icon'])) {
update_post_meta($plate_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['allow_view'])) {
$allowed_views = ['public', 'login', 'role', 'follow'];
$view = in_array($data['allow_view'], $allowed_views, true) ? $data['allow_view'] : 'public';
update_post_meta($plate_id, '_aksrt_allow_view', $view);
}
// Sync to legacy taxonomy term if exists
$term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true);
if ($term_id > 0) {
$term_update = [];
if (isset($update['post_title'])) {
$term_update['name'] = $update['post_title'];
}
if (isset($update['post_excerpt'])) {
$term_update['description'] = $update['post_excerpt'];
}
if (!empty($term_update)) {
wp_update_term($term_id, 'aksrt_forum_board', $term_update);
}
if (isset($data['icon'])) {
update_term_meta($term_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['sort_order'])) {
update_term_meta($term_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
}
return true;
}
/**
* Delete plate (with cascade option).
*/
public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return false;
}
$topic_ids = get_posts([
'post_type' => 'aksrt_forum_topic',
'meta_key' => '_aksrt_plate_id',
'meta_value' => $plate_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
]);
if (!empty($topic_ids)) {
if ($cascade === 'move' && $move_to > 0 && $move_to !== $plate_id) {
$target = get_post($move_to);
if ($target && $target->post_type === 'aksrt_forum_plate') {
foreach ($topic_ids as $tid) {
update_post_meta((int) $tid, '_aksrt_plate_id', $move_to);
}
$this->forum->sync_plate_counts($move_to);
}
} else {
foreach ($topic_ids as $tid) {
wp_trash_post((int) $tid);
}
}
}
// Handle child plates (move to top-level)
$children = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_parent' => $plate_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
]);
foreach ($children as $child_id) {
wp_update_post(['ID' => (int) $child_id, 'post_parent' => 0]);
}
// Remove legacy taxonomy term if linked
$term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true);
if ($term_id > 0) {
wp_delete_term($term_id, 'aksrt_forum_board');
}
wp_delete_post($plate_id, true);
return true;
}
/**
* Reorder plates.
*/
public function reorder_plates(array $order_map): bool {
foreach ($order_map as $plate_id => $sort_order) {
$plate_id = (int) $plate_id;
if ($plate_id <= 0) continue;
wp_update_post(['ID' => $plate_id, 'menu_order' => (int) $sort_order]);
update_post_meta($plate_id, '_aksrt_sort_order', (int) $sort_order);
}
return true;
}
/**
* Resolve plate_id for a topic (reads _aksrt_plate_id meta, falls back to taxonomy).
*/
public function get_topic_plate_id(int $topic_id): int {
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
return $plate_id;
}
// Fallback: find plate via legacy taxonomy term
$terms = get_the_terms($topic_id, 'aksrt_forum_board');
if (!$terms || is_wp_error($terms)) {
return 0;
}
$term = $terms[0];
// Look up plate post by migrated_from_term meta
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'meta_key' => '_aksrt_migrated_from_term',
'meta_value' => (string) $term->term_id,
'numberposts' => 1,
'fields' => 'ids',
'post_status' => 'publish',
]);
if (!empty($plates)) {
$resolved = (int) $plates[0];
update_post_meta($topic_id, '_aksrt_plate_id', $resolved);
return $resolved;
}
return 0;
}
/**
* Check if a user is a plate moderator (CPT-based).
*/
public function is_plate_moderator(int $user_id, int $plate_id): bool {
if ($user_id <= 0 || $plate_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$post = get_post($plate_id);
if ($post && (int) $post->post_author === $user_id) {
return true;
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($moderators) && in_array($user_id, $moderators, true)) {
return true;
}
return false;
}
/**
* Get plate view (topics + moderators + pagination) for frontend.
*/
public function get_plate_view(int $plate_id, array $args = []): array {
$plate = $this->get_plate($plate_id);
if (!$plate) {
return [
'plate' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'children' => [],
];
}
$user_id = (int) ($args['user_id'] ?? get_current_user_id());
$paged = max(1, (int) ($args['paged'] ?? 1));
$topics = $this->forum->get_topic_list([
'plate_id' => $plate_id,
'page' => $paged,
'limit' => 20,
]);
// Global + super + plate sticky
$sticky_topics = $this->get_sticky_topics_for_plate($plate_id);
$children = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_parent' => $plate_id,
'post_status' => 'publish',
'posts_per_page' => 50,
'orderby' => 'menu_order',
'order' => 'ASC',
]);
$child_plates = [];
foreach ($children as $child) {
$child_plates[] = $this->build_plate_payload($child);
}
return [
'plate' => $plate,
'topics' => $topics['items'],
'sticky_topics' => $sticky_topics,
'moderators' => $this->get_plate_moderator_profiles($plate_id),
'pagination' => [
'page' => $topics['page'],
'max_pages' => $topics['max_pages'],
'total' => $topics['total'],
],
'permissions' => [
'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id),
'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'),
'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id),
'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id),
'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id),
'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id),
],
'children' => $child_plates,
];
}
/**
* Get sticky topics for a plate (global + super + plate-level).
*/
private function get_sticky_topics_for_plate(int $plate_id): array {
$global_sticky = get_posts([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'meta_key' => '_aksrt_sticky',
'meta_value' => AKSRT_Forum::STICKY_GLOBAL,
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
]);
$plate_sticky = get_posts([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => [
'relation' => 'AND',
['key' => '_aksrt_plate_id', 'value' => $plate_id, 'type' => 'NUMERIC'],
['key' => '_aksrt_sticky', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'],
['key' => '_aksrt_sticky', 'value' => AKSRT_Forum::STICKY_GLOBAL, 'compare' => '<', 'type' => 'NUMERIC'],
],
]);
$items = [];
$seen = [];
foreach (array_merge($global_sticky, $plate_sticky) as $post) {
if (isset($seen[$post->ID])) continue;
$seen[$post->ID] = true;
$items[] = $this->forum->build_topic_payload($post);
}
return array_slice($items, 0, 10);
}
/**
* Get moderator user profiles for a plate.
*/
private function get_plate_moderator_profiles(int $plate_id): array {
$post = get_post($plate_id);
$profiles = [];
// Author = super moderator
if ($post && $post->post_author > 0) {
$user = get_userdata((int) $post->post_author);
if ($user) {
$profiles[] = [
'id' => (int) $user->ID,
'name' => $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user->ID, 40) : get_avatar_url($user->ID, ['size' => 40]),
'role' => 'plate_author',
];
}
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return $profiles;
}
$roles = get_post_meta($plate_id, '_aksrt_moderator_roles', true);
if (!is_array($roles)) {
$roles = [];
}
foreach ($moderators as $uid) {
$uid = (int) $uid;
if ($post && $uid === (int) $post->post_author) continue;
$user = get_userdata($uid);
if (!$user) continue;
$profiles[] = [
'id' => $uid,
'name' => $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($uid, 40) : get_avatar_url($uid, ['size' => 40]),
'role' => (string) ($roles[$uid] ?? 'moderator'),
];
}
return $profiles;
}
}
@@ -0,0 +1,425 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Forum_Board_Service_Part_2 {
/**
* Get board page view (统一入口 CPT/taxonomy).
*/
public function get_board_view(int $board_id, array $args = []): array {
$user_id = (int) ($args['user_id'] ?? get_current_user_id());
$page = max(1, (int) ($args['paged'] ?? 1));
if ($this->forum->is_plate_cpt_active()) {
return $this->get_plate_view_compat($board_id, $user_id, $page);
}
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return [
'board' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'application' => null,
'board_options' => [],
];
}
$topics = $this->forum->get_topic_list([
'board_id' => $board_id,
'page' => $page,
'limit' => 20,
'status' => 'publish',
'viewer_user_id' => $user_id,
]);
$sticky_topics = $this->forum->get_sticky_topics_for_board($board_id);
$board_link = get_term_link($board);
return [
'board' => [
'id' => $board_id,
'name' => (string) $board->name,
'description' => (string) $board->description,
'content' => '',
'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true),
'topic_count' => (int) $board->count,
'reply_count' => 0,
'today_reply_count' => 0,
'views' => 0,
'follow_count' => 0,
'last_reply' => '',
'last_post' => '',
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
'thumbnail_url' => '',
'author_id' => 0,
'allow_view' => 'public',
],
'topics' => $topics['items'],
'sticky_topics' => array_slice($sticky_topics, 0, 5),
'moderators' => $this->forum->get_board_moderator_profiles($board_id),
'pagination' => [
'page' => $topics['page'],
'max_pages' => $topics['max_pages'],
'total' => $topics['total'],
],
'permissions' => [
'can_compose' => $user_id > 0,
'can_apply_moderator' => $user_id > 0 && !$this->forum->is_board_moderator($user_id, $board_id),
'can_moderate' => $this->forum->is_board_moderator($user_id, $board_id),
],
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $board_id) : null,
'board_options' => $this->get_boards(),
];
}
/**
* Plate CPT board view (backwards-compatible with get_board_view return shape).
*/
private function get_plate_view_compat(int $plate_id, int $user_id, int $page): array {
$plate = $this->get_plate($plate_id);
if (!$plate) {
return [
'board' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'application' => null,
'board_options' => [],
];
}
// Check access permission
$allow_view = $plate['allow_view'] ?? 'public';
if (!$this->can_view_plate($user_id, $plate_id, $allow_view)) {
return [
'board' => $plate,
'topics' => [],
'sticky_topics' => [],
'moderators' => $this->get_plate_moderator_profiles($plate_id),
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [
'can_compose' => false,
'can_apply_moderator' => false,
'can_moderate' => false,
'access_denied' => true,
'access_reason' => $allow_view,
],
'application' => null,
'board_options' => $this->get_boards(),
];
}
$topics = $this->forum->get_topic_list([
'plate_id' => $plate_id,
'page' => $page,
'limit' => 20,
'status' => 'publish',
'viewer_user_id' => $user_id,
]);
$sticky_topics = $this->get_sticky_topics_for_plate($plate_id);
return [
'board' => $plate,
'topics' => $topics['items'],
'sticky_topics' => array_slice($sticky_topics, 0, 5),
'moderators' => $this->get_plate_moderator_profiles($plate_id),
'pagination' => [
'page' => $topics['page'],
'max_pages' => $topics['max_pages'],
'total' => $topics['total'],
],
'permissions' => [
'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id),
'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'),
'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id),
'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id),
'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id),
'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id),
],
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $plate_id) : null,
'board_options' => $this->get_boards(),
];
}
/**
* Check if a user can view a plate (§7.2 板块阅读权限).
*/
public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool {
if ($allow_view === '') {
$allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public');
}
if ($allow_view === 'public') {
return true;
}
if ($user_id <= 0) {
return false;
}
if ($this->is_plate_moderator($user_id, $plate_id)) {
return true;
}
if ($allow_view === 'login') {
return true;
}
if ($allow_view === 'follow') {
return $this->is_user_following_plate($user_id, $plate_id);
}
if ($allow_view === 'role') {
$allowed_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true);
if (!is_array($allowed_roles) || empty($allowed_roles)) {
return true;
}
$user = get_userdata($user_id);
if (!$user) {
return false;
}
return !empty(array_intersect($user->roles, $allowed_roles));
}
return true;
}
/**
* Check if a user can compose in a plate (§7.2 板块发帖限制).
*/
public function can_compose_in_plate(int $user_id, int $plate_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_plate_moderator($user_id, $plate_id)) {
return true;
}
$add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all');
if ($add_limit === 'all') {
return true;
}
$user = get_userdata($user_id);
if (!$user) {
return false;
}
if ($add_limit === 'role') {
$allowed_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true);
if (!is_array($allowed_roles) || empty($allowed_roles)) {
return true;
}
return !empty(array_intersect($user->roles, $allowed_roles));
}
return true;
}
/**
* Check if a user is following a plate (§7.2 关注).
*/
public function is_user_following_plate(int $user_id, int $plate_id): bool {
if ($user_id <= 0) {
return false;
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
return false;
}
return in_array($plate_id, $follow_list, true);
}
/**
* Toggle user follow on a plate (§7.2 关注/取消关注).
*
* @return array{followed:bool,follow_count:int}
*/
public function toggle_follow_plate(int $user_id, int $plate_id): array {
if ($user_id <= 0) {
return ['followed' => false, 'follow_count' => 0];
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
$follow_list = [];
}
$followed = in_array($plate_id, $follow_list, true);
if ($followed) {
$follow_list = array_values(array_diff($follow_list, [$plate_id]));
} else {
$follow_list[] = $plate_id;
}
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
// Update plate follow_count
$count = $this->count_plate_followers($plate_id);
update_post_meta($plate_id, '_aksrt_follow_count', $count);
return [
'followed' => !$followed,
'follow_count' => $count,
];
}
/**
* Count followers for a plate.
*/
private function count_plate_followers(int $plate_id): int {
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->usermeta}
WHERE meta_key = 'aksrt_follow_plate'
AND meta_value LIKE %s",
'%i:' . $plate_id . ';%'
));
}
/**
* Get plates a user follows.
*/
public function get_user_followed_plates(int $user_id): array {
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list) || empty($follow_list)) {
return [];
}
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'post__in' => array_map('intval', $follow_list),
'post_status' => 'publish',
'posts_per_page' => 100,
'orderby' => 'post__in',
]);
$result = [];
foreach ($plates as $plate) {
$result[] = $this->build_plate_payload($plate);
}
return $result;
}
/**
* Get a user's managed plates (plates where user is moderator/author).
*
* @return array{owned:array,moderating:array}
*/
public function get_user_managed_plates(int $user_id): array {
$owned = [];
$moderating = [];
if (!$this->forum->is_plate_cpt_active()) {
return ['owned' => $owned, 'moderating' => $moderating];
}
// Plates where user is author (超级版主)
$own_plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'author' => $user_id,
'post_status' => 'publish',
'posts_per_page' => 50,
'orderby' => 'title',
'order' => 'ASC',
]);
foreach ($own_plates as $plate) {
$owned[] = $this->build_plate_payload($plate);
}
// Plates where user is a moderator (in _aksrt_moderators list)
global $wpdb;
$mod_plate_ids = $wpdb->get_col($wpdb->prepare(
"SELECT pm.post_id FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'aksrt_forum_plate' AND p.post_status = 'publish'
WHERE pm.meta_key = '_aksrt_moderators'",
));
foreach ($mod_plate_ids as $plate_id) {
$plate_id = (int) $plate_id;
// Skip plates already in owned
if (in_array($plate_id, wp_list_pluck($own_plates, 'ID'), true)) {
continue;
}
$mods = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
$plate = get_post($plate_id);
if ($plate) {
$moderating[] = $this->build_plate_payload($plate);
}
}
}
return ['owned' => $owned, 'moderating' => $moderating];
}
// ── Plate utilities (extracted from facade) ──
public function increment_plate_views(int $plate_id): void {
if ($plate_id <= 0 || !$this->forum->is_plate_cpt_active()) {
return;
}
$views = (int) get_post_meta($plate_id, '_aksrt_views', true);
update_post_meta($plate_id, '_aksrt_views', $views + 1);
}
/**
* Check if a plate qualifies as "hot" (§7.2 热门版块).
*/
public function is_hot_plate(int $plate_id): bool {
$config = $this->forum->get_config();
$threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100);
$posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true);
$follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true);
return ($posts_count + $follow_count * 2) >= $threshold;
}
/**
* Get plate categories (分区) with moderators.
*/
public function get_plate_categories(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_plate_cat',
'hide_empty' => false,
'orderby' => 'name',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$categories = [];
foreach ($terms as $term) {
$term_id = (int) $term->term_id;
$moderator_ids = get_term_meta($term_id, '_aksrt_cat_moderator', true);
if (!is_array($moderator_ids)) {
$moderator_ids = [];
}
$categories[] = [
'id' => $term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'slug' => (string) $term->slug,
'count' => (int) $term->count,
'parent_id' => (int) $term->parent,
'moderators' => array_values(array_unique(array_filter(array_map('intval', $moderator_ids)))),
'add_limit' => (string) (get_term_meta($term_id, '_aksrt_add_limit', true) ?: 'all'),
'url' => (string) get_term_link($term),
];
}
return $categories;
}
}
@@ -0,0 +1,137 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Forum_Board_Service_Part_3 {
/**
* Check if a user is a plate_cat moderator (分区版主).
*/
public function is_cat_moderator(int $user_id, int $cat_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$moderator_ids = get_term_meta($cat_id, '_aksrt_cat_moderator', true);
if (!is_array($moderator_ids)) {
return false;
}
return in_array($user_id, array_map('intval', $moderator_ids), true);
}
/**
* Get plates sorted by a specified dimension (§7.2 多维排序).
*/
public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array {
$allowed = [
'name' => 'title',
'date' => 'date',
'modified' => 'modified',
'menu_order' => 'menu_order',
'last_post' => '_aksrt_last_post',
'last_reply' => '_aksrt_last_reply',
'posts_count' => '_aksrt_posts_count',
'reply_count' => '_aksrt_reply_count',
'today_reply_count' => '_aksrt_today_reply_count',
'views' => '_aksrt_views',
'follow_count' => '_aksrt_follow_count',
'rand' => 'rand',
];
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'order' => strtoupper($order) === 'ASC' ? 'ASC' : 'DESC',
];
if (isset($allowed[$orderby])) {
$meta_key = $allowed[$orderby];
if (str_starts_with($meta_key, '_aksrt_')) {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = $meta_key;
} else {
$query_args['orderby'] = $meta_key;
}
} else {
$query_args['orderby'] = 'menu_order';
}
$posts = get_posts($query_args);
$plates = [];
foreach ($posts as $post) {
$plates[] = $this->build_plate_payload($post);
}
return $plates;
}
public function maybe_migrate_boards(): void {
if (get_option('aksrt_forum_boards_migrated')) {
return;
}
$old_boards = get_option('aksrt_forum_boards', []);
if (!is_array($old_boards) || empty($old_boards)) {
update_option('aksrt_forum_boards_migrated', time());
return;
}
foreach ($old_boards as $board) {
$name = sanitize_text_field((string) ($board['name'] ?? ''));
if ($name === '') {
continue;
}
$existing = get_terms([
'taxonomy' => 'aksrt_forum_board',
'name' => $name,
'hide_empty' => false,
'fields' => 'ids',
]);
if (!is_wp_error($existing) && !empty($existing)) {
$term_id = (int) $existing[0];
} else {
$result = wp_insert_term($name, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($board['description'] ?? '')),
]);
if (is_wp_error($result)) {
continue;
}
$term_id = (int) $result['term_id'];
}
$icon = sanitize_text_field((string) ($board['icon'] ?? ''));
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_term_meta($term_id, '_aksrt_sort_order', (int) ($board['sort_order'] ?? $term_id));
if (!empty($board['moderators']) && is_array($board['moderators'])) {
update_term_meta($term_id, '_aksrt_moderators', array_map('intval', $board['moderators']));
}
}
update_option('aksrt_forum_boards_migrated', time());
}
public function ajax_follow_plate(): void {
check_ajax_referer('aksrt_forum_action', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$plate_id = (int) ($_POST['plate_id'] ?? 0);
if ($plate_id <= 0) {
wp_send_json_error(['message' => __('版块不存在', 'aksrt')]);
}
$result = $this->toggle_follow_plate($user_id, $plate_id);
wp_send_json_success($result);
}
}
@@ -0,0 +1,399 @@
<?php
/**
* Forum board / plate service.
*
* Extracted from AKSRT_Forum to isolate board & plate CRUD, hierarchy,
* view assembly, access checks, and follow handling into a cohesive unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Board_Service {
use AKSRT_Forum_Board_Service_Part_1;
use AKSRT_Forum_Board_Service_Part_2;
use AKSRT_Forum_Board_Service_Part_3;
/** @var AKSRT_Forum */
private $forum;
public function __construct(AKSRT_Forum $forum) {
$this->forum = $forum;
}
/**
* Get board list (plate CPT).
*
* 读取 aksrt_forum_plate CPT,支持层级查询。
* Fallback:若 plate CPT 不存在或迁移未完成,降级读取 taxonomy。
*/
public function get_boards(array $args = []): array {
// 优先使用 plate CPT
if ($this->forum->is_plate_cpt_active()) {
return $this->get_boards_from_cpt($args);
}
// Fallback: 读取旧 taxonomy(向后兼容)
return $this->get_boards_from_taxonomy();
}
/**
* 从 plate CPT 读取板块列表(新实现)。
*/
private function get_boards_from_cpt(array $args = []): array {
$defaults = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'menu_order',
'order' => 'ASC',
'hierarchical' => true,
];
$query_args = wp_parse_args($args, $defaults);
$plates = get_posts($query_args);
if (empty($plates)) {
return [];
}
$boards = [];
foreach ($plates as $plate) {
$plate_id = (int) $plate->ID;
$moderators = $this->forum->get_plate_moderators($plate_id);
$applications = $this->forum->get_plate_moderator_applications($plate_id);
$boards[] = [
'id' => $plate_id,
'name' => (string) $plate->post_title,
'description' => (string) $plate->post_excerpt,
'content' => (string) $plate->post_content,
'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true),
'sort_order' => (int) $plate->menu_order,
'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true),
'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true),
'views' => (int) get_post_meta($plate_id, '_aksrt_views', true),
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true),
'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true),
'moderator_count' => count($moderators),
'pending_application_count' => count(array_filter($applications, static function ($item): bool {
return (string) ($item['status'] ?? '') === AKSRT_Forum::APPLICATION_PENDING;
})),
'url' => (string) get_permalink($plate),
'author_id' => (int) $plate->post_author,
'parent_id' => (int) $plate->post_parent,
'created_at' => (string) $plate->post_date,
'thumbnail_url' => (string) get_the_post_thumbnail_url($plate, 'thumbnail'),
];
}
return $this->build_hierarchical_boards($boards);
}
/**
* 从 taxonomy 读取板块(向后兼容)。
*/
private function get_boards_from_taxonomy(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => false,
'orderby' => 'meta_value_num',
'meta_key' => '_aksrt_sort_order',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$boards = [];
foreach ($terms as $term) {
$board_id = (int) $term->term_id;
$moderators = $this->forum->get_board_moderators($board_id);
$applications = $this->forum->get_board_moderator_applications($board_id);
$board_link = get_term_link($term);
$boards[] = [
'id' => $board_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'content' => '',
'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true),
'sort_order' => (int) get_term_meta($board_id, '_aksrt_sort_order', true),
'post_count' => (int) $term->count,
'reply_count' => 0,
'today_reply_count' => 0,
'views' => 0,
'follow_count' => 0,
'last_reply' => '',
'last_post' => '',
'moderator_count' => count($moderators),
'pending_application_count' => count(array_filter($applications, static function ($item): bool {
return (string) ($item['status'] ?? '') === AKSRT_Forum::APPLICATION_PENDING;
})),
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
'author_id' => 0,
'parent_id' => (int) $term->parent,
'created_at' => '',
'thumbnail_url' => '',
];
}
return $boards;
}
/**
* 构建层级板块树(子板块嵌套)。
*/
private function build_hierarchical_boards(array $boards): array {
$by_id = [];
foreach ($boards as $board) {
$by_id[$board['id']] = $board;
$by_id[$board['id']]['children'] = [];
}
$roots = [];
foreach ($boards as $board) {
if ($board['parent_id'] > 0 && isset($by_id[$board['parent_id']])) {
$by_id[$board['parent_id']]['children'][] = &$by_id[$board['id']];
} else {
$roots[] = &$by_id[$board['id']];
}
}
return $roots;
}
/**
* Create board (统一入口,自动选择 CPT/taxonomy).
*/
public function create_board(array $data): int {
if ($this->forum->is_plate_cpt_active()) {
return $this->create_plate($data);
}
// Fallback: taxonomy
$name = sanitize_text_field((string) ($data['name'] ?? ''));
if ($name === '') {
return 0;
}
$result = wp_insert_term($name, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($data['description'] ?? '')),
]);
if (is_wp_error($result)) {
return 0;
}
$term_id = (int) $result['term_id'];
$icon = sanitize_text_field((string) ($data['icon'] ?? ''));
$sort_order = (int) ($data['sort_order'] ?? $term_id);
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_term_meta($term_id, '_aksrt_sort_order', $sort_order);
return $term_id;
}
/**
* Update board (统一入口,自动选择 CPT/taxonomy).
*/
public function update_board(int $board_id, array $data): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->update_plate($board_id, $data);
}
// Fallback: taxonomy
$term = get_term($board_id, 'aksrt_forum_board');
if (!$term || is_wp_error($term)) {
return false;
}
$update = ['taxonomy' => 'aksrt_forum_board'];
if (isset($data['name'])) {
$name = sanitize_text_field((string) $data['name']);
if ($name !== '') {
$update['name'] = $name;
}
}
if (isset($data['description'])) {
$update['description'] = sanitize_textarea_field((string) $data['description']);
}
$result = wp_update_term($board_id, 'aksrt_forum_board', $update);
if (is_wp_error($result)) {
return false;
}
if (isset($data['icon'])) {
update_term_meta($board_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['sort_order'])) {
update_term_meta($board_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
return true;
}
/**
* Delete board (统一入口,自动选择 CPT/taxonomy).
*/
public function delete_board(int $board_id): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->delete_plate($board_id);
}
// Fallback: taxonomy
$term = get_term($board_id, 'aksrt_forum_board');
if (!$term || is_wp_error($term)) {
return false;
}
$result = wp_delete_term($board_id, 'aksrt_forum_board');
return !is_wp_error($result) && !empty($result);
}
/**
* Reorder boards (统一入口).
*/
public function reorder_boards(array $order_map): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->reorder_plates($order_map);
}
foreach ($order_map as $term_id => $sort_order) {
$term_id = (int) $term_id;
if ($term_id <= 0) {
continue;
}
update_term_meta($term_id, '_aksrt_sort_order', (int) $sort_order);
}
return true;
}
// ================================================================
// §7.0 Plate CPT methods —板块 CPT 化 CRUD
// ================================================================
/**
* Get all plates (板块 CPT posts).
*
* @return array<int, array<string, mixed>>
*/
public function get_plates(array $args = []): array {
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => (int) ($args['limit'] ?? 100),
'orderby' => 'menu_order',
'order' => 'ASC',
];
if (!empty($args['parent'])) {
$query_args['post_parent'] = (int) $args['parent'];
}
if (!empty($args['plate_cat'])) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_plate_cat',
'field' => 'term_id',
'terms' => (int) $args['plate_cat'],
]];
}
$posts = get_posts($query_args);
$plates = [];
foreach ($posts as $post) {
$plates[] = $this->build_plate_payload($post);
}
return $plates;
}
/**
* Get single plate by ID.
*/
public function get_plate(int $plate_id): ?array {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return null;
}
return $this->build_plate_payload($post);
}
/**
* Build plate data payload from WP_Post.
*/
public function build_plate_payload(\WP_Post $post): array {
$plate_id = (int) $post->ID;
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
$moderators = [];
}
$cover_url = (string) get_the_post_thumbnail_url($plate_id, 'medium');
if ($cover_url === '') {
$config = $this->forum->get_config();
$default_thumbs = $config['other']['bbs_term_thumb'] ?? [];
if (!empty($default_thumbs) && is_array($default_thumbs)) {
$cover_url = $default_thumbs[array_rand($default_thumbs)];
}
}
$plate_cats = wp_get_object_terms($plate_id, 'aksrt_plate_cat', ['fields' => 'all']);
$plate_cat = (!is_wp_error($plate_cats) && !empty($plate_cats)) ? [
'id' => (int) $plate_cats[0]->term_id,
'name' => (string) $plate_cats[0]->name,
'slug' => (string) $plate_cats[0]->slug,
] : null;
return [
'id' => $plate_id,
'name' => (string) $post->post_title,
'description' => (string) $post->post_excerpt,
'content' => (string) $post->post_content,
'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true),
'cover_url' => $cover_url,
'sort_order' => (int) $post->menu_order,
'parent_id' => (int) $post->post_parent,
'author_id' => (int) $post->post_author,
'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true),
'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true),
'views' => (int) get_post_meta($plate_id, '_aksrt_views', true),
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true),
'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true),
'moderator_count' => count($moderators),
'moderators' => $moderators,
'url' => (string) get_permalink($plate_id),
'created_at' => (string) $post->post_date,
'allow_view' => (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'),
'add_limit' => (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'),
'is_hot' => $this->forum->is_hot_plate($plate_id),
'plate_cat' => $plate_cat,
// Alias fields for backward compat with get_board_view consumer
'topic_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'thumbnail_url' => $cover_url,
];
}
/**
* Create plate (CPT post).
*/
}
@@ -0,0 +1,583 @@
<?php
/**
* Forum comment/reply service.
*
* Extracted from AKSRT_Forum to isolate the comment engine:
* querying, rendering, pinning, hot-comment detection, and adopt-answer flow.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Comment_Service {
const COMMENT_TOP_NONE = 0;
const COMMENT_TOP_NORMAL = 1;
const COMMENT_TOP_HOT = 2;
const COMMENT_TOP_BEST = 3;
/** @var AKSRT_Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ── Hook: forum comment posted ───────────────────────────────────
public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void {
$comment = get_comment($comment_id);
if (!$comment) return;
$post = get_post((int) $comment->comment_post_ID);
if (!$post || $post->post_type !== 'aksrt_forum_topic') return;
if ((int) $comment->comment_approved !== 1) return;
$commenter_id = (int) $comment->user_id;
if ($commenter_id <= 0) return;
$this->forum->notify_topic_reply((int) $post->ID, $commenter_id);
}
// ── AJAX: get forum comments ─────────────────────────────────────
/**
* AJAX: Get forum comments with sort/filter/pagination.
*/
public function ajax_forum_comments(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$topic_id = (int) ($_POST['topic_id'] ?? 0);
$sort = sanitize_key((string) ($_POST['sort'] ?? 'newest'));
$sort = in_array($sort, ['newest', 'oldest', 'hottest'], true) ? $sort : 'newest';
$author_only = !empty($_POST['author_only']);
$page = max(1, (int) ($_POST['page'] ?? 1));
$per_page = max(1, min(100, (int) ($_POST['per_page'] ?? 30)));
if ($topic_id <= 0) {
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
}
$result = $this->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page);
ob_start();
$this->render_forum_comments($result['comments'], $topic_id, $result['adopted_id']);
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'total' => $result['total'],
'max_pages' => $result['max_pages'],
'page' => $result['page'],
'has_more' => $result['page'] < $result['max_pages'],
]);
}
// ── AJAX: comment topping ────────────────────────────────────────
/**
* AJAX: Set comment topping level (moderator action).
*/
public function ajax_comment_topping(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$comment_id = (int) ($_POST['comment_id'] ?? 0);
$level = max(0, min(3, (int) ($_POST['level'] ?? 0)));
$user_id = get_current_user_id();
$comment = get_comment($comment_id);
if (!$comment) {
wp_send_json_error(['message' => __('评论不存在', 'aksrt')]);
}
$topic_id = (int) $comment->comment_post_ID;
if (!$this->forum->forum_user_can($user_id, 'reply_top', $topic_id)) {
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
}
if ($this->set_comment_topping($comment_id, $level)) {
wp_send_json_success([
'message' => $level > 0 ? __('已置顶', 'aksrt') : __('已取消置顶', 'aksrt'),
'level' => $level,
]);
}
wp_send_json_error(['message' => __('操作失败', 'aksrt')]);
}
// ── Render forum comments ────────────────────────────────────────
/**
* Render forum comments HTML for AJAX and initial load.
*
* @param WP_Comment[] $comments Comments (parents + children mixed).
* @param int $topic_id Topic post ID.
* @param int $adopted_id Adopted comment ID (0 if none).
*/
public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void {
$post = get_post($topic_id);
$post_author_id = $post ? (int) $post->post_author : 0;
$current_user_id = get_current_user_id();
$is_question = 'question' === (string) get_post_meta($topic_id, '_aksrt_bbs_type', true);
$can_adopt = $is_question && $current_user_id > 0
&& ($current_user_id === $post_author_id || $this->forum->forum_user_can($current_user_id, 'reply_adopt_other', $topic_id));
$can_moderate = $this->forum->forum_user_can($current_user_id, 'topic_moderate', $topic_id);
if (empty($comments)) {
echo '<div class="aksrt-forum-replies__empty"><i class="far fa-comment-dots"></i> ' . esc_html__('暂无回复,来抢沙发吧!', 'aksrt') . '</div>';
return;
}
foreach ($comments as $comment) {
if (!($comment instanceof \WP_Comment)) continue;
if ((int) $comment->comment_parent !== 0) continue;
$comment_id = (int) $comment->comment_ID;
$commenter_uid = (int) $comment->user_id;
$is_adopted = $comment_id === $adopted_id;
$is_hot = $this->is_hot_comment($comment_id);
$is_post_author = $commenter_uid > 0 && $commenter_uid === $post_author_id;
$topping = (int) get_comment_meta($comment_id, '_aksrt_comment_topping', true);
$badge = $this->get_commenter_badge($commenter_uid, $topic_id);
$like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0;
$is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($comment_id) : false;
$classes = ['aksrt-forum-reply'];
if ($is_adopted) $classes[] = 'is-adopted';
if ($is_hot) $classes[] = 'is-hot';
if ($topping > 0) $classes[] = 'is-pinned';
?>
<div class="<?php echo esc_attr(implode(' ', $classes)); ?>" id="comment-<?php echo esc_attr((string) $comment_id); ?>" data-comment-id="<?php echo esc_attr((string) $comment_id); ?>">
<div class="aksrt-forum-reply__avatar"><?php echo get_avatar($comment, 40); ?></div>
<div class="aksrt-forum-reply__body">
<div class="aksrt-forum-reply__meta">
<strong><?php comment_author($comment_id); ?></strong>
<?php if ($is_post_author) : ?>
<span class="aksrt-forum-reply__role-badge aksrt-forum-reply__role-badge--author"><?php esc_html_e('楼主', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($badge) : ?>
<span class="aksrt-forum-reply__role-badge <?php echo esc_attr($badge['class']); ?>"><?php echo esc_html($badge['label']); ?></span>
<?php endif; ?>
<?php if ($topping > 0) : ?>
<span class="aksrt-forum-reply__badge aksrt-forum-reply__badge--pinned"><i class="fas fa-thumbtack"></i> <?php esc_html_e('置顶', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($is_hot) : ?>
<span class="aksrt-forum-reply__badge aksrt-forum-reply__badge--hot"><i class="fas fa-fire"></i> <?php esc_html_e('神评', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($is_adopted) : ?>
<span class="aksrt-forum-reply__adopted-badge"><i class="fas fa-check-circle"></i> <?php esc_html_e('最佳答案', 'aksrt'); ?></span>
<?php endif; ?>
<time datetime="<?php echo esc_attr(get_comment_date('c', $comment)); ?>"><?php echo esc_html(get_comment_date('Y-m-d H:i', $comment)); ?></time>
</div>
<div class="aksrt-forum-reply__content"><?php echo function_exists('aksrt_render_comment_content') ? aksrt_render_comment_content((string) get_comment_text($comment_id)) : wp_kses_post(get_comment_text($comment_id)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-reply__actions">
<button type="button" class="aksrt-forum-reply__like-btn js-aksrt-comment-like<?php echo $is_liked ? ' is-active' : ''; ?>"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-active="<?php echo $is_liked ? '1' : '0'; ?>">
<i class="<?php echo $is_liked ? 'fas' : 'far'; ?> fa-thumbs-up"></i>
<span class="aksrt-forum-reply__like-count"><?php echo $like_count > 0 ? esc_html((string) $like_count) : ''; ?></span>
</button>
<?php if (is_user_logged_in()) : ?>
<button type="button" class="aksrt-forum-reply__report-btn js-aksrt-report-trigger"
data-target-type="comment"
data-target-id="<?php echo esc_attr((string) $comment_id); ?>"
data-target-title="<?php echo esc_attr(wp_strip_all_tags(get_comment_excerpt($comment))); ?>">
<i class="far fa-flag"></i> <?php esc_html_e('举报', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if (comments_open($topic_id)) : ?>
<button type="button" class="aksrt-forum-reply__reply-btn js-aksrt-comment-reply"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-author="<?php echo esc_attr(get_comment_author($comment_id)); ?>">
<i class="fas fa-reply"></i> <?php esc_html_e('回复', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if ($can_adopt && !$is_adopted) : ?>
<button type="button" class="aksrt-forum-reply__adopt-btn js-aksrt-adopt-answer"
data-topic-id="<?php echo esc_attr((string) $topic_id); ?>"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>">
<i class="fas fa-check"></i> <?php esc_html_e('采纳', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if ($can_moderate) : ?>
<button type="button" class="aksrt-forum-reply__top-btn js-aksrt-comment-top"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-level="<?php echo esc_attr((string) $topping); ?>">
<i class="fas fa-thumbtack"></i> <?php echo $topping > 0 ? esc_html__('取消置顶', 'aksrt') : esc_html__('置顶', 'aksrt'); ?>
</button>
<?php endif; ?>
</div>
<?php
// Render nested children (楼中楼)
$child_comments = array_filter($comments, function ($c) use ($comment_id) {
return ($c instanceof \WP_Comment) && (int) $c->comment_parent === $comment_id;
});
if (!empty($child_comments)) : ?>
<div class="aksrt-forum-reply__children">
<?php foreach ($child_comments as $child) :
$child_id = (int) $child->comment_ID;
$child_uid = (int) $child->user_id;
$child_badge = $this->get_commenter_badge($child_uid, $topic_id);
$child_is_author = $child_uid > 0 && $child_uid === $post_author_id;
$child_like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($child_id) : 0;
$child_is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($child_id) : false;
$parent_comment = get_comment((int) $child->comment_parent);
$parent_name = $parent_comment ? get_comment_author((int) $parent_comment->comment_ID) : '';
?>
<div class="aksrt-forum-reply aksrt-forum-reply--nested" id="comment-<?php echo esc_attr((string) $child_id); ?>" data-comment-id="<?php echo esc_attr((string) $child_id); ?>">
<div class="aksrt-forum-reply__avatar aksrt-forum-reply__avatar--sm"><?php echo get_avatar($child, 28); ?></div>
<div class="aksrt-forum-reply__body">
<div class="aksrt-forum-reply__meta">
<strong><?php comment_author($child_id); ?></strong>
<?php if ($child_is_author) : ?>
<span class="aksrt-forum-reply__role-badge aksrt-forum-reply__role-badge--author"><?php esc_html_e('楼主', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($child_badge) : ?>
<span class="aksrt-forum-reply__role-badge <?php echo esc_attr($child_badge['class']); ?>"><?php echo esc_html($child_badge['label']); ?></span>
<?php endif; ?>
<?php if ($parent_name && (int) $child->comment_parent !== $comment_id) : ?>
<span class="aksrt-forum-reply__reply-to"><?php printf(esc_html__('回复 %s', 'aksrt'), esc_html($parent_name)); ?></span>
<?php endif; ?>
<time datetime="<?php echo esc_attr(get_comment_date('c', $child)); ?>"><?php echo esc_html(get_comment_date('Y-m-d H:i', $child)); ?></time>
</div>
<div class="aksrt-forum-reply__content"><?php echo function_exists('aksrt_render_comment_content') ? aksrt_render_comment_content((string) get_comment_text($child_id)) : wp_kses_post(get_comment_text($child_id)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-reply__actions">
<button type="button" class="aksrt-forum-reply__like-btn js-aksrt-comment-like<?php echo $child_is_liked ? ' is-active' : ''; ?>"
data-comment-id="<?php echo esc_attr((string) $child_id); ?>"
data-active="<?php echo $child_is_liked ? '1' : '0'; ?>">
<i class="<?php echo $child_is_liked ? 'fas' : 'far'; ?> fa-thumbs-up"></i>
<span class="aksrt-forum-reply__like-count"><?php echo $child_like_count > 0 ? esc_html((string) $child_like_count) : ''; ?></span>
</button>
<?php if (comments_open($topic_id)) : ?>
<button type="button" class="aksrt-forum-reply__reply-btn js-aksrt-comment-reply"
data-comment-id="<?php echo esc_attr((string) $child_id); ?>"
data-author="<?php echo esc_attr(get_comment_author($child_id)); ?>">
<i class="fas fa-reply"></i> <?php esc_html_e('回复', 'aksrt'); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
}
// ── Adopt answer (best answer) ──────────────────────────────────
public function adopt_answer(int $topic_id, int $comment_id, int $operator_id) {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true));
if ($bbs_type !== 'question') {
return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt'));
}
$author_id = (int) $post->post_author;
if ($operator_id !== $author_id && !$this->forum->can_moderate_topic($operator_id, $topic_id)) {
return new \WP_Error('forbidden', __('仅提问者或版主可采纳答案', 'aksrt'));
}
$comment = get_comment($comment_id);
if (!$comment || (int) $comment->comment_post_ID !== $topic_id) {
return new \WP_Error('invalid_comment', __('回复不存在', 'aksrt'));
}
$existing = get_post_meta($topic_id, '_aksrt_adopted_comment', true);
if ($existing && (int) $existing === $comment_id) {
return new \WP_Error('already_adopted', __('该回复已被采纳', 'aksrt'));
}
if ($existing) {
delete_comment_meta((int) $existing, '_aksrt_adopted');
}
update_comment_meta($comment_id, '_aksrt_adopted', '1');
update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id);
update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED
$adoptee_id = (int) $comment->user_id;
if ($adoptee_id > 0 && class_exists('AKSRT_Credits')) {
$config = $this->forum->get_config();
$amount = (int) ($config['credits']['adopted'] ?? 20);
if ($amount > 0) {
$already = get_comment_meta($comment_id, '_aksrt_adopt_credited', true);
if (!$already) {
\AKSRT_Credits::add($adoptee_id, $amount, 'forum_adopted', [
'type' => 'comment',
'id' => $comment_id,
]);
update_comment_meta($comment_id, '_aksrt_adopt_credited', '1');
}
}
$this->forum->notify($adoptee_id, 'system', sprintf(
__('你的回复被采纳为最佳答案(帖子「%s」)', 'aksrt'),
get_the_title($topic_id)
), [
'title' => __('回复被采纳', 'aksrt'),
'target_url' => get_permalink($topic_id) . '#comment-' . $comment_id,
]);
}
return [
'message' => __('已采纳该回复为最佳答案', 'aksrt'),
'comment_id' => $comment_id,
'solved' => true,
];
}
public function get_adopted_comment_id(int $topic_id): int {
return (int) get_post_meta($topic_id, '_aksrt_adopted_comment', true);
}
// ── Get forum comments (query) ──────────────────────────────────
/**
* Get forum comments with sort/filter support.
*
* @param int $topic_id Topic post ID.
* @param string $sort 'newest'|'oldest'|'hottest'
* @param bool $author_only Only show post-author replies.
* @param int $page Page number.
* @param int $per_page Items per page.
* @return array{comments:WP_Comment[],total:int,max_pages:int,page:int,adopted_id:int}
*/
public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return ['comments' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1, 'adopted_id' => 0];
}
$args = [
'post_id' => $topic_id,
'status' => 'approve',
'parent' => 0,
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
];
if ($author_only) {
$args['user_id'] = (int) $post->post_author;
}
if ($sort === 'hottest') {
$args['meta_key'] = 'aksrt_comment_likes';
$args['orderby'] = 'meta_value_num';
$args['order'] = 'DESC';
} elseif ($sort === 'oldest') {
$args['orderby'] = 'comment_date_gmt';
$args['order'] = 'ASC';
} else {
$args['orderby'] = 'comment_date_gmt';
$args['order'] = 'DESC';
}
// Total count for pagination
$count_args = $args;
unset($count_args['number'], $count_args['offset']);
$count_args['count'] = true;
$total = (int) get_comments($count_args);
$top_level = get_comments($args);
// Fetch child comments for these parents
if (!empty($top_level)) {
$parent_ids = wp_list_pluck($top_level, 'comment_ID');
$children = get_comments([
'post_id' => $topic_id,
'status' => 'approve',
'parent__in' => $parent_ids,
'orderby' => 'comment_date_gmt',
'order' => 'ASC',
'number' => 200,
]);
} else {
$children = [];
}
// Float pinned top-level comments
$pinned = [];
$normal = [];
foreach ($top_level as $c) {
$topping = (int) get_comment_meta((int) $c->comment_ID, '_aksrt_comment_topping', true);
if ($topping > 0) {
$c->aksrt_topping = $topping;
$pinned[] = $c;
} else {
$normal[] = $c;
}
}
usort($pinned, function ($a, $b) {
return ($b->aksrt_topping ?? 0) <=> ($a->aksrt_topping ?? 0);
});
$comments = array_merge($pinned, $normal, $children);
return [
'comments' => $comments,
'total' => $total,
'max_pages' => max(1, (int) ceil($total / $per_page)),
'page' => $page,
'adopted_id' => $this->get_adopted_comment_id($topic_id),
];
}
// ── Set comment topping ─────────────────────────────────────────
/**
* Set comment topping level.
*
* @param int $comment_id Comment ID.
* @param int $level 0=unpin, 1=normal pin, 2=hot pin, 3=best answer pin.
*/
public function set_comment_topping(int $comment_id, int $level): bool {
$comment = get_comment($comment_id);
if (!$comment) {
return false;
}
if ($level <= 0) {
delete_comment_meta($comment_id, '_aksrt_comment_topping');
delete_comment_meta($comment_id, '_aksrt_comment_pinned');
} else {
update_comment_meta($comment_id, '_aksrt_comment_topping', $level);
update_comment_meta($comment_id, '_aksrt_comment_pinned', '1');
}
return true;
}
// ── Maybe mark hot comment ──────────────────────────────────────
/**
* Check and mark a comment as hot ("神评") if it crosses the like threshold.
*
* @return bool True if newly marked as hot.
*/
public function maybe_mark_hot_comment(int $comment_id): bool {
$comment = get_comment($comment_id);
if (!$comment || '1' !== (string) $comment->comment_approved) {
return false;
}
$post = get_post((int) $comment->comment_post_ID);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return false;
}
$already_hot = '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true);
if ($already_hot) {
return false;
}
$config = $this->forum->get_config();
$threshold = max(1, (int) ($config['reply']['hot_threshold'] ?? 10));
$likes = max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true));
if ($likes < $threshold) {
return false;
}
update_comment_meta($comment_id, '_aksrt_is_hot', '1');
// Credit the comment author
$author_id = (int) $comment->user_id;
if ($author_id > 0 && class_exists('AKSRT_Credits')) {
$amount = (int) ($config['credits']['comment_like'] ?? 1);
if ($amount > 0) {
\AKSRT_Credits::add($author_id, $amount, 'forum_hot_comment', [
'type' => 'comment',
'id' => $comment_id,
]);
}
}
// Notify the author
if ($author_id > 0) {
$this->forum->notify($author_id, 'system', sprintf(
__('你的回复成为了神评(帖子「%s」)', 'aksrt'),
get_the_title((int) $comment->comment_post_ID)
), [
'title' => __('恭喜获得神评', 'aksrt'),
'target_url' => get_permalink((int) $comment->comment_post_ID) . '#comment-' . $comment_id,
]);
}
return true;
}
// ── Is hot comment ──────────────────────────────────────────────
/**
* Check if a comment is a hot comment ("神评").
*/
public function is_hot_comment(int $comment_id): bool {
return '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true);
}
// ── Get commenter badge ─────────────────────────────────────────
/**
* Get the user's moderator tier label for badge rendering in comments.
*
* @return array{tier:int,label:string,class:string}|null
*/
public function get_commenter_badge(int $user_id, int $topic_id): ?array {
if ($user_id <= 0) {
return null;
}
if ($this->forum->is_forum_admin($user_id)) {
return [
'tier' => AKSRT_Forum::TIER_FORUM_ADMIN,
'label' => __('管理员', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--admin',
];
}
$plate_id = $this->forum->get_topic_plate_id($topic_id);
if ($plate_id > 0) {
$tier = $this->forum->get_user_plate_tier($user_id, $plate_id);
if ($tier >= AKSRT_Forum::TIER_CAT_MOD) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['cat_moderator'] ?? __('分区版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--catmod',
];
}
if ($tier >= AKSRT_Forum::TIER_PLATE_AUTHOR) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['plate_author'] ?? __('超级版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--super',
];
}
if ($tier >= AKSRT_Forum::TIER_MODERATOR) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['plate_moderator'] ?? __('版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--mod',
];
}
}
return null;
}
}
@@ -0,0 +1,409 @@
<?php
/**
* Forum draft management service.
*
* Extracted from AKSRT_Forum to isolate draft CRUD, loading,
* and AJAX handlers into a cohesive unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Draft_Service {
/** @var AKSRT_Forum */
private $forum;
public function __construct(AKSRT_Forum $forum) {
$this->forum = $forum;
}
public function save_draft(int $user_id, array $data) {
if ($user_id <= 0) {
return new WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$draft_id = (int) ($data['draft_id'] ?? 0);
$post_arr = [
'post_type' => 'aksrt_forum_topic',
'post_status' => 'draft',
'post_author' => $user_id,
'post_title' => sanitize_text_field((string) ($data['title'] ?? '')),
'post_content' => wp_kses_post((string) ($data['content'] ?? '')),
];
if ($draft_id > 0) {
$existing = get_post($draft_id);
if (!$existing || $existing->post_type !== 'aksrt_forum_topic'
|| (int) $existing->post_author !== $user_id
|| $existing->post_status !== 'draft') {
return new WP_Error('invalid_draft', __('草稿不存在或无权编辑', 'aksrt'));
}
$post_arr['ID'] = $draft_id;
$result = wp_update_post($post_arr, true);
} else {
$result = wp_insert_post($post_arr, true);
}
if (is_wp_error($result)) {
return $result;
}
$post_id = (int) $result;
$board_id = (int) ($data['board_id'] ?? 0);
if ($board_id > 0) {
if ($this->forum->is_plate_cpt_active()) {
update_post_meta($post_id, '_aksrt_plate_id', $board_id);
} else {
wp_set_object_terms($post_id, [$board_id], 'aksrt_forum_board');
}
}
$tags = is_array($data['tags'] ?? null) ? $data['tags'] : [];
if (!empty($tags)) {
$clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5);
wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag');
}
$meta_fields = [
'visibility' => '_aksrt_visibility',
'bbs_type' => '_aksrt_bbs_type',
'special_topic' => '_aksrt_special_topic',
'cover_type' => '_aksrt_cover_type',
];
foreach ($meta_fields as $key => $meta_key) {
if (isset($data[$key])) {
update_post_meta($post_id, $meta_key, sanitize_key((string) $data[$key]));
}
}
if (isset($data['credits_cost'])) {
update_post_meta($post_id, '_aksrt_credits_cost', max(0, (int) $data['credits_cost']));
}
if (isset($data['pay_price'])) {
update_post_meta($post_id, '_aksrt_pay_price', sanitize_text_field((string) $data['pay_price']));
}
if (isset($data['visibility_roles']) && is_array($data['visibility_roles'])) {
update_post_meta($post_id, '_aksrt_visibility_roles', array_map('sanitize_key', $data['visibility_roles']));
}
$this->forum->save_cover_meta($post_id, $data);
if (!empty($data['vote_enabled'])) {
$vote_meta = $this->forum->prepare_vote_meta($data);
$this->forum->save_vote_meta($post_id, $vote_meta);
}
if (isset($data['topic_cat'])) {
$topic_cat = sanitize_text_field((string) $data['topic_cat']);
if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) {
wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat');
}
}
return [
'post_id' => $post_id,
'message' => __('草稿已保存', 'aksrt'),
];
}
public function get_user_draft_count(int $user_id): int {
if ($user_id <= 0) {
return 0;
}
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_forum_topic' AND post_status = 'draft' AND post_author = %d",
$user_id
));
}
/**
* @return array<int,array{id:int,title:string,date:string,board_name:string}>
*/
public function get_user_drafts(int $user_id, int $limit = 20): array {
if ($user_id <= 0) {
return [];
}
$query = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'draft',
'author' => $user_id,
'posts_per_page' => $limit,
'orderby' => 'modified',
'order' => 'DESC',
'no_found_rows' => true,
]);
$drafts = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post();
if (!$post) {
continue;
}
$board_name = '';
if ($this->forum->is_plate_cpt_active()) {
$plate_id = (int) get_post_meta($post->ID, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$board_name = get_the_title($plate_id);
}
} else {
$terms = wp_get_post_terms($post->ID, 'aksrt_forum_board', ['fields' => 'names']);
if (!is_wp_error($terms) && !empty($terms)) {
$board_name = $terms[0];
}
}
$drafts[] = [
'id' => $post->ID,
'title' => $post->post_title ?: __('(无标题)', 'aksrt'),
'date' => $post->post_modified,
'board_name' => $board_name,
];
}
wp_reset_postdata();
return $drafts;
}
public function delete_draft(int $user_id, int $draft_id): bool {
if ($user_id <= 0 || $draft_id <= 0) {
return false;
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return false;
}
return (bool) wp_delete_post($draft_id, true);
}
/**
* Load a draft's full data for the compose form.
*/
public function load_draft_data(int $user_id, int $draft_id): ?array {
if ($user_id <= 0 || $draft_id <= 0) {
return null;
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return null;
}
$board_id = 0;
if ($this->forum->is_plate_cpt_active()) {
$board_id = (int) get_post_meta($draft_id, '_aksrt_plate_id', true);
} else {
$terms = wp_get_post_terms($draft_id, 'aksrt_forum_board', ['fields' => 'ids']);
if (!is_wp_error($terms) && !empty($terms)) {
$board_id = (int) $terms[0];
}
}
$tags = wp_get_post_terms($draft_id, 'aksrt_forum_tag', ['fields' => 'names']);
if (is_wp_error($tags)) {
$tags = [];
}
$topic_cat = '';
if (taxonomy_exists('aksrt_forum_topic_cat')) {
$cats = wp_get_post_terms($draft_id, 'aksrt_forum_topic_cat', ['fields' => 'names']);
if (!is_wp_error($cats) && !empty($cats)) {
$topic_cat = $cats[0];
}
}
$slide_raw = get_post_meta($draft_id, '_aksrt_featured_slide', true);
return [
'draft_id' => $draft_id,
'title' => $post->post_title,
'content' => $post->post_content,
'board_id' => $board_id,
'tags' => implode(', ', $tags),
'visibility' => get_post_meta($draft_id, '_aksrt_visibility', true) ?: AKSRT_Forum::VISIBILITY_PUBLIC,
'bbs_type' => get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: AKSRT_Forum::BBS_TYPE_STANDARD,
'special_topic' => get_post_meta($draft_id, '_aksrt_special_topic', true) ?: '',
'cover_type' => get_post_meta($draft_id, '_aksrt_cover_type', true) ?: '',
'cover_image' => get_post_meta($draft_id, '_aksrt_cover_image', true) ?: '',
'featured_slide' => is_array($slide_raw) ? $slide_raw : [],
'featured_video' => get_post_meta($draft_id, '_aksrt_featured_video', true) ?: '',
'credits_cost' => (int) get_post_meta($draft_id, '_aksrt_credits_cost', true),
'pay_price' => get_post_meta($draft_id, '_aksrt_pay_price', true) ?: '',
'visibility_roles' => get_post_meta($draft_id, '_aksrt_visibility_roles', true) ?: [],
'topic_cat' => $topic_cat,
'vote_enabled' => get_post_meta($draft_id, '_aksrt_vote_enabled', true) === '1',
'vote_title' => get_post_meta($draft_id, '_aksrt_vote_title', true) ?: '',
'vote_multiple' => get_post_meta($draft_id, '_aksrt_vote_multiple', true) === '1',
'vote_type' => get_post_meta($draft_id, '_aksrt_vote_type', true) ?: 'standard',
'vote_expiry_days' => (int) get_post_meta($draft_id, '_aksrt_vote_expiry_days', true),
'vote_options' => $this->get_vote_options_text($draft_id),
];
}
private function get_vote_options_text(int $post_id): string {
$options = get_post_meta($post_id, '_aksrt_vote_options', true);
if (!is_array($options)) {
return '';
}
return implode("\n", array_column($options, 'label'));
}
// ──────────────────────────────────────────────────────────
// AJAX handlers
// ──────────────────────────────────────────────────────────
public function ajax_save_draft(): 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 = [
'draft_id' => (int) ($_POST['draft_id'] ?? 0),
'board_id' => (int) ($_POST['board_id'] ?? 0),
'title' => sanitize_text_field((string) ($_POST['title'] ?? '')),
'content' => wp_kses_post((string) ($_POST['content'] ?? '')),
'tags' => $this->forum->split_tags((string) ($_POST['tags'] ?? '')),
'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')),
'visibility' => sanitize_key((string) ($_POST['visibility'] ?? AKSRT_Forum::VISIBILITY_PUBLIC)),
'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? AKSRT_Forum::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']),
'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')),
'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0),
'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']) : [],
];
$result = $this->save_draft($user_id, $data);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success($result);
}
public function ajax_load_draft(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
$draft_id = (int) ($_GET['draft_id'] ?? $_POST['draft_id'] ?? 0);
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$data = $this->load_draft_data($user_id, $draft_id);
if (!$data) {
wp_send_json_error(['message' => __('草稿不存在', 'aksrt')]);
}
wp_send_json_success($data);
}
public function ajax_delete_draft(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
$draft_id = (int) ($_POST['draft_id'] ?? 0);
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
if (!$this->delete_draft($user_id, $draft_id)) {
wp_send_json_error(['message' => __('删除失败', 'aksrt')]);
}
wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]);
}
// ──────────────────────────────────────────────────────────
// Publish draft (transition from draft → publish/pending)
// ──────────────────────────────────────────────────────────
public function publish_draft(int $user_id, int $draft_id, array $data) {
if ($user_id <= 0) {
return new WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return new WP_Error('invalid_draft', __('草稿不存在', 'aksrt'));
}
$data['draft_id'] = $draft_id;
$save_result = $this->save_draft($user_id, $data);
if (is_wp_error($save_result)) {
return $save_result;
}
$board_id = (int) ($data['board_id'] ?? 0);
$title = sanitize_text_field((string) ($data['title'] ?? ''));
$content = wp_kses_post((string) ($data['content'] ?? ''));
if ($board_id <= 0 || $title === '' || $content === '') {
return new WP_Error('invalid_payload', __('请填写版块、标题和内容', 'aksrt'));
}
$can_publish = $this->forum->can_user_publish_directly($user_id, $board_id);
$post_status = $can_publish ? 'publish' : 'pending';
$review_status = $can_publish ? AKSRT_Forum::REVIEW_APPROVED : AKSRT_Forum::REVIEW_PENDING;
wp_update_post([
'ID' => $draft_id,
'post_status' => $post_status,
]);
update_post_meta($draft_id, '_aksrt_review_status', $review_status);
update_post_meta($draft_id, '_aksrt_sticky', AKSRT_Forum::STICKY_NONE);
update_post_meta($draft_id, '_aksrt_featured', '0');
update_post_meta($draft_id, '_aksrt_locked', '0');
update_post_meta($draft_id, '_aksrt_views', 0);
$bbs_type = get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: AKSRT_Forum::BBS_TYPE_STANDARD;
if ($bbs_type === AKSRT_Forum::BBS_TYPE_QUESTION) {
update_post_meta($draft_id, '_aksrt_question_status', AKSRT_Forum::QUESTION_UNSOLVED);
}
if ($post_status === 'publish') {
$this->forum->sync_board_count($board_id);
if (class_exists('AKSRT_Credits_Earn')) {
do_action('aksrt_forum_topic_created', $user_id, $draft_id);
}
}
if ($post_status === 'pending') {
$this->forum->notify_topic_pending($draft_id, $user_id);
}
return [
'post_id' => $draft_id,
'message' => $post_status === 'publish'
? __('主题已发布', 'aksrt')
: __('主题已提交审核,审核通过后对外展示', 'aksrt'),
'redirect' => $post_status === 'publish'
? get_permalink($draft_id)
: add_query_arg('compose_status', 'pending', home_url('/forums/')),
];
}
}
@@ -0,0 +1,720 @@
<?php
/**
* Forum moderation service.
*
* Extracted from AKSRT_Forum to isolate moderator management, applications,
* board/plate counting, and moderator role CRUD into a cohesive unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Moderation_Service {
private AKSRT_Forum $forum;
public function __construct(AKSRT_Forum $forum) {
$this->forum = $forum;
}
public function apply_moderator(int $user_id, int $board_id, string $reason) {
if ($user_id <= 0) {
return new WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
if ($this->forum->is_plate_cpt_active()) {
$post = get_post($board_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return new WP_Error('invalid_board', __('版块不存在', 'aksrt'));
}
} else {
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return new WP_Error('invalid_board', __('版块不存在', 'aksrt'));
}
}
if ($this->is_board_moderator($user_id, $board_id)) {
return new WP_Error('already_moderator', __('你已经是该版块版主', 'aksrt'));
}
$applications = $this->get_board_moderator_applications($board_id);
foreach ($applications as $application) {
if ((int) ($application['user_id'] ?? 0) === $user_id && (string) ($application['status'] ?? '') === AKSRT_Forum::APPLICATION_PENDING) {
return new WP_Error('application_exists', __('你已经提交过申请,等待处理即可', 'aksrt'));
}
}
$reason = trim($reason);
if ($reason === '') {
return new WP_Error('empty_reason', __('请填写申请理由', 'aksrt'));
}
$application = [
'id' => wp_generate_uuid4(),
'board_id' => $board_id,
'user_id' => $user_id,
'reason' => sanitize_textarea_field($reason),
'status' => AKSRT_Forum::APPLICATION_PENDING,
'created_at' => current_time('mysql'),
'reviewed_at' => '',
'review_note' => '',
'reviewer_id' => 0,
];
$applications[] = $application;
if ($this->forum->is_plate_cpt_active()) {
update_post_meta($board_id, '_aksrt_moderator_applications', $applications);
} else {
update_term_meta($board_id, '_aksrt_moderator_applications', $applications);
}
return [
'message' => __('版主申请已提交', 'aksrt'),
'application' => $application,
];
}
/**
* Review a moderator application.
*
* @return array<string,mixed>|WP_Error
*/
public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) {
if ($reviewer_id <= 0 || !$this->forum->forum_user_can($reviewer_id, 'moderator_review')) {
return new WP_Error('forbidden', __('权限不足', 'aksrt'));
}
if (!in_array($status, [AKSRT_Forum::APPLICATION_APPROVED, AKSRT_Forum::APPLICATION_REJECTED], true)) {
return new WP_Error('invalid_status', __('申请处理状态无效', 'aksrt'));
}
$use_cpt = $this->forum->is_plate_cpt_active();
$boards = $this->forum->get_boards();
$flat_boards = $this->flatten_boards($boards);
foreach ($flat_boards as $board) {
$board_id = (int) ($board['id'] ?? 0);
$applications = $this->get_board_moderator_applications($board_id);
$updated = false;
foreach ($applications as &$application) {
if ((string) ($application['id'] ?? '') !== $application_id) {
continue;
}
$application['status'] = $status;
$application['review_note'] = sanitize_textarea_field($note);
$application['reviewed_at'] = current_time('mysql');
$application['reviewer_id'] = $reviewer_id;
$updated = true;
if ($status === AKSRT_Forum::APPLICATION_APPROVED) {
$this->add_moderator((int) ($application['user_id'] ?? 0), $board_id);
}
break;
}
unset($application);
if ($updated) {
if ($use_cpt) {
update_post_meta($board_id, '_aksrt_moderator_applications', $applications);
} else {
update_term_meta($board_id, '_aksrt_moderator_applications', $applications);
}
$reviewed_application = $this->find_application_by_id($application_id);
if ($reviewed_application) {
$reviewed_user_id = (int) ($reviewed_application['user_id'] ?? 0);
if ($reviewed_user_id > 0 && $reviewed_user_id !== $reviewer_id) {
$board_name = (string) ($board['name'] ?? __('当前版块', 'aksrt'));
$status_label = $status === AKSRT_Forum::APPLICATION_APPROVED
? __('已通过', 'aksrt')
: __('已拒绝', 'aksrt');
$payload = [
'title' => __('版主申请结果', 'aksrt'),
'target_url' => isset($board['url']) ? (string) $board['url'] : home_url('/forums/'),
];
$clean_note = sanitize_textarea_field($note);
if ($clean_note !== '') {
$payload['reason'] = $clean_note;
}
$this->forum->notify(
$reviewed_user_id,
'system',
sprintf(__('你在「%1$s」提交的版主申请%2$s', 'aksrt'), $board_name, $status_label),
$payload
);
}
}
return [
'message' => __('申请状态已更新', 'aksrt'),
'application' => $reviewed_application,
];
}
}
return new WP_Error('application_not_found', __('申请记录不存在', 'aksrt'));
}
/**
* Flatten hierarchical boards array (collapse children into flat list).
*/
private function flatten_boards(array $boards): array {
$flat = [];
foreach ($boards as $board) {
$children = $board['children'] ?? [];
unset($board['children']);
$flat[] = $board;
if (!empty($children)) {
$flat = array_merge($flat, $this->flatten_boards($children));
}
}
return $flat;
}
/**
* Get flattened moderator application list.
*/
public function get_moderator_application_list(array $args = []): array {
$status = sanitize_key((string) ($args['status'] ?? ''));
$board_id = (int) ($args['board_id'] ?? 0);
$keyword = sanitize_text_field((string) ($args['keyword'] ?? ''));
$items = [];
foreach ($this->forum->get_boards() as $board) {
$current_board_id = (int) ($board['id'] ?? 0);
if ($board_id > 0 && $current_board_id !== $board_id) {
continue;
}
foreach ($this->get_board_moderator_applications($current_board_id) as $application) {
if ($status !== '' && (string) ($application['status'] ?? '') !== $status) {
continue;
}
$user = get_userdata((int) ($application['user_id'] ?? 0));
$row = [
'id' => (string) ($application['id'] ?? ''),
'board_id' => $current_board_id,
'board_name' => (string) ($board['name'] ?? ''),
'user_id' => (int) ($application['user_id'] ?? 0),
'display_name' => $user ? $user->display_name : '',
'user_email' => $user ? $user->user_email : '',
'avatar_url' => $user ? $this->forum->get_user_avatar_url((int) $user->ID) : '',
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? ''),
'status_label' => $this->get_application_status_label((string) ($application['status'] ?? '')),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer' => $this->forum->build_user_summary((int) ($application['reviewer_id'] ?? 0)),
];
if ($keyword !== '') {
$haystack = strtolower(implode(' ', [
(string) $row['board_name'],
(string) $row['display_name'],
(string) $row['user_email'],
(string) $row['reason'],
]));
if (strpos($haystack, strtolower($keyword)) === false) {
continue;
}
}
$items[] = $row;
}
}
usort($items, static function ($left, $right): int {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
return $items;
}
/**
* Sync board/plate topic count + reply count.
*/
public function sync_board_count(int $board_id): void {
if ($board_id <= 0) {
return;
}
if ($this->forum->is_plate_cpt_active()) {
$this->sync_plate_counts($board_id);
return;
}
$count = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => 1,
'fields' => 'ids',
'tax_query' => [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]],
'post_status' => 'publish',
]);
update_term_meta($board_id, '_aksrt_topic_count', (int) $count->found_posts);
}
/**
* Sync plate multi-dimensional counters (§7.2 去规范化统计).
*/
public function sync_plate_counts(int $plate_id): void {
global $wpdb;
$posts_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'",
(string) $plate_id
));
$reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1'",
(string) $plate_id
));
$today_start = current_time('Y-m-d') . ' 00:00:00';
$today_reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1' AND c.comment_date >= %s",
(string) $plate_id,
$today_start
));
$last_post = $wpdb->get_var($wpdb->prepare(
"SELECT p.post_date FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'
ORDER BY p.post_date DESC LIMIT 1",
(string) $plate_id
));
$last_reply = $wpdb->get_var($wpdb->prepare(
"SELECT c.comment_date FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1'
ORDER BY c.comment_date DESC LIMIT 1",
(string) $plate_id
));
update_post_meta($plate_id, '_aksrt_posts_count', $posts_count);
update_post_meta($plate_id, '_aksrt_reply_count', $reply_count);
update_post_meta($plate_id, '_aksrt_today_reply_count', $today_reply_count);
update_post_meta($plate_id, '_aksrt_last_post', (string) ($last_post ?: ''));
update_post_meta($plate_id, '_aksrt_last_reply', (string) ($last_reply ?: ''));
}
/**
* Get board moderator IDs (统一入口,自动检测 CPT/taxonomy).
*/
public function get_board_moderators(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
return $this->get_plate_moderators($board_id);
}
// Fallback: taxonomy
$moderators = get_term_meta($board_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', $moderators))));
}
/**
* Get plate moderators from CPT meta.
*/
public function get_plate_moderators(int $plate_id): array {
if (!post_type_exists('aksrt_forum_plate')) {
return [];
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', $moderators))));
}
/**
* Check if a user is a forum-level admin (bbs_admin_users setting).
*
* Forum admins have cross-board governance privileges and bypass bans.
*/
public function is_forum_admin(int $user_id): bool {
if ($user_id <= 0) {
return false;
}
if (user_can($user_id, 'manage_options')) {
return true;
}
$forum = AKSRT_Settings::get_forum_settings();
$admin_users = $forum['admin_users'] ?? [];
return in_array($user_id, $admin_users, true);
}
/**
* Check if a user is a board moderator.
*/
public function is_board_moderator(int $user_id, int $board_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_forum_admin($user_id)) {
return true;
}
return in_array($user_id, $this->get_board_moderators($board_id), true);
}
/**
* Get board moderator profile rows.
*/
public function get_board_moderator_profiles(int $board_id): array {
$profiles = [];
foreach ($this->get_board_moderators($board_id) as $user_id) {
$user = get_userdata($user_id);
if (!$user) {
continue;
}
$profiles[] = [
'id' => $user_id,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
'avatar_url' => $this->forum->get_user_avatar_url($user_id),
];
}
return $profiles;
}
/**
* Get board moderator profiles with role information.
*/
public function get_board_moderator_profiles_with_roles(int $board_id): array {
$roles_map = $this->get_moderator_roles($board_id);
$profiles = [];
foreach ($this->get_board_moderators($board_id) as $user_id) {
$user = get_userdata($user_id);
if (!$user) continue;
$profiles[] = [
'id' => $user_id,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
'avatar_url' => $this->forum->get_user_avatar_url($user_id),
'role' => $roles_map[$user_id] ?? 'moderator',
];
}
return $profiles;
}
/**
* Get moderator role map for a board: [user_id => role].
*
* In CPT mode, also maps plate author → 'plate_author' (超级版主).
*/
public function get_moderator_roles(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
$roles = get_post_meta($board_id, '_aksrt_moderator_roles', true);
$roles = is_array($roles) ? $roles : [];
$post = get_post($board_id);
if ($post && $post->post_author > 0) {
$roles[(int) $post->post_author] = 'plate_author';
}
return $roles;
}
$roles = get_term_meta($board_id, '_aksrt_moderator_roles', true);
return is_array($roles) ? $roles : [];
}
/**
* Set a moderator's role on a board.
*/
public function set_moderator_role(int $user_id, int $board_id, string $role): bool {
$allowed = ['super_moderator', 'moderator', 'junior_moderator', 'plate_author', 'cat_moderator'];
if (!in_array($role, $allowed, true)) {
return false;
}
if (!in_array($user_id, $this->get_board_moderators($board_id), true)) {
return false;
}
$roles = $this->get_moderator_roles($board_id);
$roles[$user_id] = $role;
if ($this->forum->is_plate_cpt_active()) {
return (bool) update_post_meta($board_id, '_aksrt_moderator_roles', $roles);
}
return (bool) update_term_meta($board_id, '_aksrt_moderator_roles', $roles);
}
/**
* Add a moderator to a board with optional role.
*/
public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool {
$current = $this->get_board_moderators($board_id);
if (in_array($user_id, $current, true)) {
$this->set_moderator_role($user_id, $board_id, $role);
return true;
}
$current[] = $user_id;
$new_list = array_values(array_unique(array_map('intval', $current)));
if ($this->forum->is_plate_cpt_active()) {
$result = update_post_meta($board_id, '_aksrt_moderators', $new_list);
} else {
$result = update_term_meta($board_id, '_aksrt_moderators', $new_list);
}
if ($result !== false) {
$this->set_moderator_role($user_id, $board_id, $role);
}
return $result !== false;
}
/**
* Remove a moderator from a board.
*/
public function remove_moderator(int $user_id, int $board_id): bool {
$current = $this->get_board_moderators($board_id);
$current = array_values(array_diff($current, [$user_id]));
$roles = $this->get_moderator_roles($board_id);
unset($roles[$user_id]);
if ($this->forum->is_plate_cpt_active()) {
$result = update_post_meta($board_id, '_aksrt_moderators', $current);
update_post_meta($board_id, '_aksrt_moderator_roles', $roles);
} else {
$result = update_term_meta($board_id, '_aksrt_moderators', $current);
update_term_meta($board_id, '_aksrt_moderator_roles', $roles);
}
return $result !== false;
}
/**
* Get all boards a user moderates (with roles).
*/
public function get_user_moderator_boards(int $user_id): array {
$boards = [];
foreach ($this->forum->get_boards() as $board) {
$board_id = (int) ($board['id'] ?? 0);
if ($board_id <= 0) continue;
$moderators = $this->get_board_moderators($board_id);
if (in_array($user_id, $moderators, true)) {
$roles = $this->get_moderator_roles($board_id);
$boards[] = [
'board_id' => $board_id,
'board_name' => (string) ($board['name'] ?? ''),
'role' => $roles[$user_id] ?? 'moderator',
];
}
}
return $boards;
}
/**
* Check if a user has a specific minimum tier on a board/plate.
*
* Tier hierarchy (aligned with zibll 4-level):
* forum_admin(4) > cat_moderator(3) > plate_author(2) > moderator(1)
*
* @param string $min_role 'moderator'|'plate_author'|'cat_moderator'|'forum_admin'
*/
public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool {
$tier_map = [
'junior_moderator' => AKSRT_Forum::TIER_MODERATOR, // backward-compat alias
'moderator' => AKSRT_Forum::TIER_MODERATOR,
'plate_author' => AKSRT_Forum::TIER_PLATE_AUTHOR,
'super_moderator' => AKSRT_Forum::TIER_PLATE_AUTHOR, // backward-compat alias
'cat_moderator' => AKSRT_Forum::TIER_CAT_MOD,
'forum_admin' => AKSRT_Forum::TIER_FORUM_ADMIN,
];
$required = $tier_map[$min_role] ?? AKSRT_Forum::TIER_MODERATOR;
return $this->forum->get_user_plate_tier($user_id, $board_id) >= $required;
}
/**
* Get board moderator applications (统一入口,自动检测 CPT/taxonomy).
*/
public function get_board_moderator_applications(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
return $this->get_plate_moderator_applications($board_id);
}
// Fallback: taxonomy
$applications = get_term_meta($board_id, '_aksrt_moderator_applications', true);
if (!is_array($applications)) {
return [];
}
return array_values(array_filter(array_map(function ($application) use ($board_id): array {
if (!is_array($application)) {
$application = [];
}
return [
'id' => (string) ($application['id'] ?? wp_generate_uuid4()),
'board_id' => $board_id,
'user_id' => (int) ($application['user_id'] ?? 0),
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? AKSRT_Forum::APPLICATION_PENDING),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer_id' => (int) ($application['reviewer_id'] ?? 0),
];
}, $applications)));
}
/**
* Get plate moderator applications from CPT meta.
*/
public function get_plate_moderator_applications(int $plate_id): array {
if (!post_type_exists('aksrt_forum_plate')) {
return [];
}
$applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true);
if (!is_array($applications)) {
return [];
}
return array_values(array_filter(array_map(function ($application) use ($plate_id): array {
if (!is_array($application)) {
$application = [];
}
return [
'id' => (string) ($application['id'] ?? wp_generate_uuid4()),
'board_id' => $plate_id,
'user_id' => (int) ($application['user_id'] ?? 0),
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? AKSRT_Forum::APPLICATION_PENDING),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer_id' => (int) ($application['reviewer_id'] ?? 0),
];
}, $applications)));
}
/**
* Get current user's board application.
*/
public function get_board_application_for_user(int $user_id, int $board_id): ?array {
$applications = $this->get_board_moderator_applications($board_id);
$matched = array_values(array_filter($applications, static function ($application) use ($user_id): bool {
return (int) ($application['user_id'] ?? 0) === $user_id;
}));
if (empty($matched)) {
return null;
}
usort($matched, static function ($left, $right): int {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
$application = $matched[0];
$application['status_label'] = $this->get_application_status_label((string) ($application['status'] ?? ''));
return $application;
}
public function find_application_by_id(string $application_id): ?array {
foreach ($this->get_moderator_application_list() as $application) {
if ((string) ($application['id'] ?? '') === $application_id) {
return $application;
}
}
return null;
}
public function get_application_status_label(string $status): string {
$map = [
AKSRT_Forum::APPLICATION_PENDING => __('待处理', 'aksrt'),
AKSRT_Forum::APPLICATION_APPROVED => __('已通过', 'aksrt'),
AKSRT_Forum::APPLICATION_REJECTED => __('已拒绝', 'aksrt'),
];
return $map[$status] ?? $map[AKSRT_Forum::APPLICATION_PENDING];
}
public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void {
if (!$this->forum->is_plate_cpt_active()) {
return;
}
$plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$this->sync_plate_counts($plate_id);
}
}
public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void {
if (!$this->forum->is_plate_cpt_active()) {
return;
}
$post_id = (int) ($commentdata['comment_post_ID'] ?? 0);
if ($post_id <= 0) {
return;
}
$post = get_post($post_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return;
}
$plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$this->sync_plate_counts($plate_id);
}
}
public function is_moderator(int $user_id, int $topic_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_forum_admin($user_id)) {
return true;
}
// CPT mode: resolve plate from topic meta
if ($this->forum->is_plate_cpt_active()) {
$plate_id = $this->forum->get_topic_plate_id($topic_id);
if ($plate_id > 0) {
return $this->forum->get_user_plate_tier($user_id, $plate_id) >= self::TIER_MODERATOR;
}
}
// Fallback: taxonomy-based boards
$boards = get_the_terms($topic_id, 'aksrt_forum_board');
if ($boards && !is_wp_error($boards)) {
foreach ($boards as $board) {
if ($this->is_board_moderator($user_id, (int) $board->term_id)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,416 @@
<?php
/**
* Forum permission/capability service.
*
* Extracted from AKSRT_Forum to keep the capability matrix,
* tier resolution, and cache invalidation in a focused unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Permission_Service {
// ── Tier constants (mirrored from AKSRT_Forum) ───────────────
const TIER_NONE = 0;
const TIER_MODERATOR = 1;
const TIER_PLATE_AUTHOR = 2;
const TIER_CAT_MOD = 3;
const TIER_FORUM_ADMIN = 4;
/** @var array<string,bool> cap_key => bool */
private static array $cap_cache = [];
/** @var array<string,int> "uid:plate_id" => tier */
private static array $tier_cache = [];
/** @var AKSRT_Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ── Capability matrix ─────────────────────────────────────────
/**
* Capability definitions matrix.
*
* Keys:
* 'min_tier' — minimum TIER_* constant required
* 'context' — 'topic'|'plate'|'global'|'any'
* 'self' — true = only applies to own content
* 'logged' — shorthand: any logged-in user
* 'settings_key' — optional forum settings permissions key
*/
private function cap_defs(): array {
return [
// ── Topic viewing ─────────────────────────────────────────────
'topic_view_pending' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_view_hidden' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_view_deleted' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
// ── Topic editing ─────────────────────────────────────────────
'topic_edit' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_delete' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Topic governance ──────────────────────────────────────────
'topic_sticky_plate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_sticky_super' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'],
'topic_sticky_global' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
'topic_featured' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_lock' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_lock_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_highlight' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_score_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_move' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_moderate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Topic interaction (logged users) ──────────────────────────
'topic_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_favorite' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_score' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_report' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Reply governance ──────────────────────────────────────────
'reply_adopt' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'reply_adopt_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_top' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Reply interaction ─────────────────────────────────────────
'reply_like' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'reply_score' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Plate management ──────────────────────────────────────────
'plate_follow' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'plate_create' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
'plate_manage' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'],
'plate_delete' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
// ── Moderator system ──────────────────────────────────────────
'moderator_manage' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'],
'moderator_apply' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'moderator_review' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'],
// ── User governance ───────────────────────────────────────────
'user_ban' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'global'],
'user_report' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Compose capabilities ──────────────────────────────────────
'compose_post' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => ''],
'compose_post_no_review' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'compose_upload_image' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'],
'compose_upload_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_video'],
'compose_upload_file' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_file'],
'compose_iframe_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_iframe_video'],
'compose_set_type' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_visibility' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_cover' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'],
'compose_set_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_paid' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'],
'compose_set_hide' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'],
];
}
// ── Tier resolution ───────────────────────────────────────────
/**
* Return the user's highest tier for a given plate.
*
* Result is cached per request on "$user_id:$plate_id".
*/
public function get_user_plate_tier(int $user_id, int $plate_id): int {
if ($user_id <= 0) {
return self::TIER_NONE;
}
$cache_key = "$user_id:$plate_id";
if (isset(self::$tier_cache[$cache_key])) {
return self::$tier_cache[$cache_key];
}
$tier = $this->compute_user_plate_tier($user_id, $plate_id);
self::$tier_cache[$cache_key] = $tier;
return $tier;
}
private function compute_user_plate_tier(int $user_id, int $plate_id): int {
// Tier 4: WP admin or bbs_admin_users list
if ($this->forum->is_forum_admin($user_id)) {
return self::TIER_FORUM_ADMIN;
}
// Tier 3: category moderator (plate_cat term meta 'moderator')
$cats = get_the_terms($plate_id, 'aksrt_plate_cat');
if ($cats && !is_wp_error($cats)) {
foreach ($cats as $cat) {
if ($this->forum->is_cat_moderator($user_id, (int) $cat->term_id)) {
return self::TIER_CAT_MOD;
}
}
}
// Tier 2: plate_author (plate CPT post_author)
if ($this->forum->is_plate_cpt_active()) {
$post = get_post($plate_id);
if ($post && (int) $post->post_author === $user_id) {
return self::TIER_PLATE_AUTHOR;
}
// Tier 1: in plate _aksrt_moderators list
$mods = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
return self::TIER_MODERATOR;
}
return self::TIER_NONE;
}
// Fallback: taxonomy board
$roles = get_term_meta($plate_id, '_aksrt_moderator_roles', true);
$roles = is_array($roles) ? $roles : [];
$role = $roles[$user_id] ?? '';
if ($role === 'super_moderator' || $role === 'plate_author') {
return self::TIER_PLATE_AUTHOR;
}
$mods = get_term_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
return self::TIER_MODERATOR;
}
return self::TIER_NONE;
}
// ── Capability check ──────────────────────────────────────────
/**
* Unified capability resolver.
*
* @param int $user_id User to check (0 = guest).
* @param string $cap Capability id from cap_defs().
* @param int $context_id topic_id for 'topic' context, plate_id for 'plate' context.
* @return bool
*/
public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool {
$cache_key = "$user_id:$cap:$context_id";
if (array_key_exists($cache_key, self::$cap_cache)) {
return self::$cap_cache[$cache_key];
}
$result = $this->compute_forum_cap($user_id, $cap, $context_id);
self::$cap_cache[$cache_key] = $result;
return $result;
}
private function compute_forum_cap(int $user_id, string $cap, int $context_id): bool {
$defs = $this->cap_defs();
if (!isset($defs[$cap])) {
return false;
}
$def = $defs[$cap];
$logged_only = !empty($def['logged']) && ($def['min_tier'] ?? self::TIER_NONE) === self::TIER_NONE;
$self_only = !empty($def['self']);
$context = $def['context'] ?? 'any';
$min_tier = (int) ($def['min_tier'] ?? self::TIER_NONE);
$settings_key = $def['settings_key'] ?? '';
// Guest short-circuit
if ($user_id <= 0) {
return false;
}
// Forum-wide admin bypasses everything
if ($this->forum->is_forum_admin($user_id)) {
// Honour settings-gated compose caps even for admins when the feature is globally off
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (isset($perms[$settings_key]) && $perms[$settings_key] === false) {
return false;
}
}
return true;
}
// 'logged' / any-user caps: just need to be logged in
if ($logged_only || ($min_tier === self::TIER_NONE && $context === 'any')) {
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) {
return false;
}
}
return true;
}
// 'self' caps: caller passes topic_id as context; check authorship
if ($self_only) {
if ($context_id <= 0) {
return false;
}
$post = get_post($context_id);
return $post && (int) $post->post_author === $user_id;
}
// Global caps (no specific plate/topic context)
if ($context === 'global') {
if ($min_tier <= self::TIER_CAT_MOD) {
return $this->is_cat_moderator_anywhere($user_id);
}
return false;
}
// Plate-context caps
if ($context === 'plate') {
$plate_id = $this->resolve_plate_id($context_id, $cap);
if ($plate_id <= 0) {
return false;
}
$tier = $this->get_user_plate_tier($user_id, $plate_id);
// Check optional settings gate
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) {
return $tier >= self::TIER_PLATE_AUTHOR;
}
}
return $tier >= $min_tier;
}
return false;
}
// ── Helper methods ────────────────────────────────────────────
/**
* Resolve a plate_id from either a plate_id or topic_id, depending on the cap context.
*/
private function resolve_plate_id(int $context_id, string $cap): int {
if ($context_id <= 0) {
return 0;
}
$post = get_post($context_id);
if (!$post) {
return 0;
}
if ($post->post_type === 'aksrt_forum_plate') {
return $context_id;
}
if ($post->post_type === 'aksrt_forum_topic') {
return $this->forum->get_topic_plate_id($context_id);
}
return 0;
}
/**
* Check if the user is a cat_moderator on ANY plate category (for global caps).
*/
private function is_cat_moderator_anywhere(int $user_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$cats = get_terms(['taxonomy' => 'aksrt_plate_cat', 'hide_empty' => false, 'fields' => 'ids']);
if (is_wp_error($cats) || empty($cats)) {
return false;
}
foreach ((array) $cats as $cat_id) {
if ($this->forum->is_cat_moderator($user_id, (int) $cat_id)) {
return true;
}
}
return false;
}
// ── Compose capabilities ──────────────────────────────────────
/**
* Get per-feature capabilities for the compose/edit form.
*
* @return array<string,bool>
*/
public function get_compose_capabilities(int $user_id): array {
$keys = [
'can_post', 'can_set_type', 'can_set_visibility',
'can_set_cover', 'can_set_cover_image', 'can_set_cover_slide', 'can_set_cover_video',
'can_set_vote', 'can_set_paid', 'can_set_hide',
'can_upload_image', 'can_upload_video', 'can_upload_file',
'can_iframe_video',
];
if ($user_id <= 0) {
return array_fill_keys($keys, false);
}
return [
'can_post' => $this->forum_user_can($user_id, 'compose_post'),
'can_set_type' => $this->forum_user_can($user_id, 'compose_set_type'),
'can_set_visibility' => $this->forum_user_can($user_id, 'compose_set_visibility'),
'can_set_cover' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_image' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_slide' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_video' => $this->forum_user_can($user_id, 'compose_upload_video'),
'can_set_vote' => $this->forum_user_can($user_id, 'compose_set_vote'),
'can_set_paid' => $this->forum_user_can($user_id, 'compose_set_paid'),
'can_set_hide' => $this->forum_user_can($user_id, 'compose_set_hide'),
'can_upload_image' => $this->forum_user_can($user_id, 'compose_upload_image'),
'can_upload_video' => $this->forum_user_can($user_id, 'compose_upload_video'),
'can_upload_file' => $this->forum_user_can($user_id, 'compose_upload_file'),
'can_iframe_video' => $this->forum_user_can($user_id, 'compose_iframe_video'),
];
}
// ── Cache invalidation ────────────────────────────────────────
public function flush_cap_cache(int $user_id = 0): void {
if ($user_id > 0) {
foreach (array_keys(self::$cap_cache) as $k) {
if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) {
unset(self::$cap_cache[$k]);
}
}
foreach (array_keys(self::$tier_cache) as $k) {
if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) {
unset(self::$tier_cache[$k]);
}
}
} else {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
public function cap_cache_on_login(string $user_login, \WP_User $user): void {
$this->flush_cap_cache((int) $user->ID);
}
/** Flush cache when plate moderator meta changes. */
public function cap_cache_on_plate_meta(int $meta_id, int $object_id, string $meta_key): void {
if (in_array($meta_key, ['_aksrt_moderators', '_aksrt_moderator_roles'], true)) {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
/** Flush cache when plate_cat moderator term meta changes. */
public function cap_cache_on_cat_meta(int $meta_id, int $object_id, string $meta_key): void {
if ($meta_key === 'moderator') {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
/** Flush cache when a plate CPT post is saved (plate_author may change). */
public function cap_cache_on_plate_save(int $post_id): void {
foreach (array_keys(self::$tier_cache) as $k) {
if (substr($k, -strlen(":$post_id")) === ":$post_id") {
unset(self::$tier_cache[$k]);
}
}
self::$cap_cache = [];
}
}
@@ -0,0 +1,430 @@
<?php
/**
* Forum scoring, favorites, and notification service.
*
* Extracted from AKSRT_Forum to isolate:
* - topic favorites (toggle, check)
* - topic scoring (rate, detail, recommend score)
* - forum notifications (pending, moderation result, reply, scored)
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Score_Service {
/** @var AKSRT_Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ================================================================
// Favorite (§7.3)
// ================================================================
public function is_favorited(int $user_id, int $topic_id): bool {
if ($user_id <= 0) {
return false;
}
$favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorites = is_array($favorites) ? $favorites : [];
return in_array($topic_id, $favorites, true);
}
public function toggle_favorite(int $user_id, int $topic_id) {
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'));
}
$favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorites = is_array($favorites) ? $favorites : [];
$was_favorited = in_array($topic_id, $favorites, true);
if ($was_favorited) {
$favorites = array_values(array_diff($favorites, [$topic_id]));
} else {
$favorites[] = $topic_id;
}
update_user_meta($user_id, 'aksrt_favorite_forum_posts', $favorites);
$count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
$count = $was_favorited ? max(0, $count - 1) : $count + 1;
update_post_meta($topic_id, '_aksrt_favorite_count', $count);
$this->refresh_recommend_score($topic_id);
return [
'favorited' => !$was_favorited,
'count' => $count,
'message' => $was_favorited
? __('已取消收藏', 'aksrt')
: __('收藏成功', 'aksrt'),
];
}
// ================================================================
// Score (§7.3)
// ================================================================
public function score_topic(int $user_id, int $topic_id, int $amount) {
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'));
}
if ($amount === 0) {
return new WP_Error('invalid_amount', __('评分值不能为零', 'aksrt'));
}
$author_id = (int) $post->post_author;
if ($author_id === $user_id) {
return new WP_Error('self_score', __('不能给自己的帖子评分', 'aksrt'));
}
$config = $this->forum->get_config();
$max_extra = (int) ($config['post_page']['score_extra_max'] ?? 5);
$max_deduct = (int) ($config['post_page']['score_deduct_max'] ?? 3);
if ($amount > 0 && $amount > $max_extra) {
$amount = $max_extra;
}
if ($amount < 0 && abs($amount) > $max_deduct) {
$amount = -$max_deduct;
}
$detail = get_post_meta($topic_id, '_aksrt_score_detail', true);
$detail = is_array($detail) ? $detail : [];
$user_key = (string) $user_id;
if (isset($detail[$user_key])) {
return new WP_Error('already_scored', __('你已经对该帖评过分', 'aksrt'));
}
$detail[$user_key] = [
'amount' => $amount,
'time' => current_time('mysql'),
];
update_post_meta($topic_id, '_aksrt_score_detail', $detail);
$total = (int) get_post_meta($topic_id, '_aksrt_score', true);
$total += $amount;
update_post_meta($topic_id, '_aksrt_score', $total);
$reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true);
$reputation += $amount;
update_user_meta($author_id, 'aksrt_reputation', $reputation);
if ($amount > 0 && class_exists('AKSRT_Credits')) {
$credits_amount = (int) ($config['credits']['score_extra'] ?? 3);
if ($credits_amount > 0) {
$daily = $this->today_credits_count($author_id, 'forum_score_extra');
$daily_max = (int) ($config['credits']['daily_max'] ?? 50);
if ($daily < $daily_max) {
AKSRT_Credits::add($author_id, $credits_amount, 'forum_score_extra', sprintf(
__('帖子「%s」被加分', 'aksrt'),
get_the_title($topic_id)
));
}
}
}
$this->refresh_recommend_score($topic_id);
$this->notify_topic_scored($topic_id, $user_id, $amount);
return [
'score' => $total,
'message' => $amount > 0
? __('加分成功', 'aksrt')
: __('减分成功', 'aksrt'),
];
}
public function get_score_detail(int $topic_id): array {
$detail = get_post_meta($topic_id, '_aksrt_score_detail', true);
$detail = is_array($detail) ? $detail : [];
$entries = [];
foreach ($detail as $uid => $entry) {
$user = get_userdata((int) $uid);
$entries[] = [
'user_id' => (int) $uid,
'display_name' => $user ? $user->display_name : '',
'amount' => (int) ($entry['amount'] ?? 0),
'time' => (string) ($entry['time'] ?? ''),
];
}
return $entries;
}
// ================================================================
// Recommend score (§7.3)
// ================================================================
public function calculate_recommend_score(int $topic_id): float {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return 0.0;
}
$views = (int) get_post_meta($topic_id, '_aksrt_views', true);
$favorites = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
$comments = (int) get_comments([
'post_id' => $topic_id,
'count' => true,
'status' => 'approve',
]);
$score = (int) get_post_meta($topic_id, '_aksrt_score', true);
$w_views = 1.0;
$w_favorites = 5.0;
$w_comments = 3.0;
$w_score = 2.0;
$raw = ($views * $w_views)
+ ($favorites * $w_favorites)
+ ($comments * $w_comments)
+ (max(0, $score) * $w_score);
$post_time = strtotime($post->post_date_gmt);
$age_hours = max(1, (time() - $post_time) / 3600);
$decay = pow($age_hours, 1.5);
$recommend = ($raw + 1) / $decay * 10000;
return round($recommend, 2);
}
public function refresh_recommend_score(int $topic_id): float {
$score = $this->calculate_recommend_score($topic_id);
update_post_meta($topic_id, '_aksrt_recommend_score', $score);
$this->maybe_reward_hot_topic($topic_id, $score);
return $score;
}
private function maybe_reward_hot_topic(int $topic_id, float $score): void {
$hot_threshold = 500.0;
if ($score < $hot_threshold) {
return;
}
if (get_post_meta($topic_id, '_aksrt_hot_rewarded', true)) {
return;
}
$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;
}
update_post_meta($topic_id, '_aksrt_hot_rewarded', '1');
$config = $this->forum->get_config();
$amount = (int) ($config['credits']['posts_hot'] ?? 5);
if ($amount > 0 && class_exists('AKSRT_Credits')) {
AKSRT_Credits::add($author_id, $amount, 'forum_hot_topic', [
'type' => 'post', 'id' => $topic_id,
]);
}
$this->notify($author_id, 'hot', sprintf(
__('你的帖子「%s」成为热帖!', 'aksrt'), get_the_title($topic_id)
), [
'title' => __('热帖通知', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
public function batch_refresh_recommend_scores(int $limit = 100): int {
$query = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => $limit,
'orderby' => 'modified',
'order' => 'DESC',
'fields' => 'ids',
'no_found_rows' => true,
]);
$count = 0;
foreach ($query->posts as $post_id) {
$this->refresh_recommend_score((int) $post_id);
$count++;
}
return $count;
}
public function maybe_refresh_recommend_on_view(): void {
if (!is_singular('aksrt_forum_topic')) {
return;
}
$topic_id = get_the_ID();
if (!$topic_id) {
return;
}
$last = (int) get_post_meta($topic_id, '_aksrt_recommend_refreshed', true);
if ((time() - $last) > 300) {
$this->refresh_recommend_score($topic_id);
update_post_meta($topic_id, '_aksrt_recommend_refreshed', time());
}
}
private function get_bbs_type_label(string $bbs_type): string {
$config = $this->forum->get_config();
$names = $config['names'] ?? [];
$map = [
AKSRT_Forum::BBS_TYPE_STANDARD => $names['posts'] ?? __('帖子', 'aksrt'),
AKSRT_Forum::BBS_TYPE_QUESTION => $names['question_badge'] ?? __('提问', 'aksrt'),
];
return $map[$bbs_type] ?? $map[AKSRT_Forum::BBS_TYPE_STANDARD];
}
private function get_question_status_label(int $status): string {
$config = $this->forum->get_config();
$names = $config['names'] ?? [];
return $status === AKSRT_Forum::QUESTION_SOLVED
? ($names['question_ok_badge'] ?? __('已解决', 'aksrt'))
: ($names['question_badge'] ?? __('提问', 'aksrt'));
}
// ================================================================
// Notifications (§7.8)
// ================================================================
public function notify(int $user_id, string $type, string $content, array $data = []): void {
if ($user_id <= 0 || !class_exists('AKSRT_Message')) {
return;
}
AKSRT_Message::get_instance()->send($user_id, $type, $content, $data);
}
public function notify_topic_pending(int $topic_id, int $author_id): void {
$title = get_the_title($topic_id);
$config = $this->forum->get_config();
$admin_users = $config['admin_users'] ?? [];
foreach ($admin_users as $admin_id) {
$this->notify((int) $admin_id, 'posts', sprintf(
__('帖子「%s」待审核', 'aksrt'), $title
), [
'title' => __('待审核帖子', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
}
public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $operator_id) return;
$title = get_the_title($topic_id);
$action_labels = [
'review' => __('审核状态已变更', 'aksrt'),
'sticky' => __('置顶状态已变更', 'aksrt'),
'featured' => __('精华状态已变更', 'aksrt'),
'locked' => __('锁定状态已变更', 'aksrt'),
'close' => __('帖子已关闭', 'aksrt'),
'hide' => __('帖子已隐藏', 'aksrt'),
'delete' => __('帖子已删除', 'aksrt'),
'undelete' => __('帖子已恢复', 'aksrt'),
];
if ($action === 'review') {
$review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true);
if ($review_status === AKSRT_Forum::REVIEW_REJECTED) {
$action_labels['review'] = __('审核未通过', 'aksrt');
} elseif ($review_status === AKSRT_Forum::REVIEW_PENDING) {
$action_labels['review'] = __('已转为待审核', 'aksrt');
} elseif ($review_status === AKSRT_Forum::REVIEW_APPROVED) {
$action_labels['review'] = __('审核已通过', 'aksrt');
}
}
$label = $action_labels[$action] ?? __('治理动作已执行', 'aksrt');
$target_url = $action === 'delete' ? home_url('/forums/') : get_permalink($topic_id);
$payload = [
'title' => $label,
'target_url' => $target_url,
];
$note = (string) get_post_meta($topic_id, '_aksrt_moderation_note', true);
if ($note !== '') {
$payload['reason'] = $note;
}
$this->notify($author_id, 'system', sprintf(
__('你的帖子「%1$s」%2$s', 'aksrt'), $title, $label
), $payload);
}
public function notify_topic_reply(int $topic_id, int $commenter_id): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $commenter_id) return;
$commenter = get_userdata($commenter_id);
$name = $commenter ? $commenter->display_name : __('有人', 'aksrt');
$title = get_the_title($topic_id);
$this->notify($author_id, 'comment', sprintf(
__('%1$s 回复了你的帖子「%2$s」', 'aksrt'), $name, $title
), [
'title' => __('新回复', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $scorer_id) return;
$title = get_the_title($topic_id);
$this->notify($author_id, 'system', sprintf(
__('你的帖子「%1$s」被%2$s %3$d 分', 'aksrt'),
$title,
$amount > 0 ? __('加', 'aksrt') : __('减', 'aksrt'),
abs($amount)
), [
'title' => $amount > 0 ? __('帖子被加分', 'aksrt') : __('帖子被减分', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
// ================================================================
// Private helpers
// ================================================================
private 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
));
}
}
@@ -0,0 +1,212 @@
<?php
/**
* Forum topic service.
*
* Extracted from AKSRT_Forum to isolate topic CRUD, moderation, voting,
* content processing, payload building, unlock flow, and status helpers
* into a cohesive unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_Topic_Service {
use AKSRT_Forum_Topic_Service_Part_1;
use AKSRT_Forum_Topic_Service_Part_2;
use AKSRT_Forum_Topic_Service_Part_3;
use AKSRT_Forum_Topic_Service_Part_4;
private AKSRT_Forum $forum;
public function __construct(AKSRT_Forum $forum) {
$this->forum = $forum;
}
// ================================================================
// Topic CRUD
// ================================================================
/**
* Update topic content and editable governance metadata.
*
* @return bool|WP_Error
*/
public function update_topic(int $topic_id, array $data) {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$update = ['ID' => $topic_id];
if (isset($data['title'])) {
$title = sanitize_text_field((string) $data['title']);
if ($title === '') {
return new WP_Error('empty_title', __('标题不能为空', 'aksrt'));
}
$update['post_title'] = $title;
}
if (isset($data['content'])) {
$content = wp_kses_post((string) $data['content']);
if ($content === '') {
return new WP_Error('empty_content', __('内容不能为空', 'aksrt'));
}
$update['post_content'] = $content;
}
if (isset($data['excerpt'])) {
$update['post_excerpt'] = sanitize_textarea_field((string) $data['excerpt']);
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return $result;
}
if (isset($data['tags']) && is_array($data['tags'])) {
$tags = array_values(array_filter(array_map('sanitize_text_field', $data['tags'])));
wp_set_object_terms($topic_id, $tags, 'aksrt_forum_tag');
}
$this->save_topic_governance_meta($topic_id, $data);
return true;
}
/**
* Create topic.
*
* @return array<string,mixed>|WP_Error
*/
public function create_topic(int $user_id, array $data) {
if ($user_id <= 0) {
return new WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$board_id = (int) ($data['board_id'] ?? 0);
$title = sanitize_text_field((string) ($data['title'] ?? ''));
$content = wp_kses_post((string) ($data['content'] ?? ''));
$visibility = sanitize_key((string) ($data['visibility'] ?? AKSRT_Forum::VISIBILITY_PUBLIC));
$special_topic = sanitize_text_field((string) ($data['special_topic'] ?? ''));
$tags = is_array($data['tags'] ?? null) ? $data['tags'] : [];
$bbs_type = sanitize_key((string) ($data['bbs_type'] ?? AKSRT_Forum::BBS_TYPE_STANDARD));
if (!in_array($bbs_type, [AKSRT_Forum::BBS_TYPE_STANDARD, AKSRT_Forum::BBS_TYPE_QUESTION], true)) {
$bbs_type = AKSRT_Forum::BBS_TYPE_STANDARD;
}
if ($board_id <= 0 || $title === '' || $content === '') {
return new WP_Error('invalid_payload', __('请填写所有必填项', 'aksrt'));
}
// §7.3: Strip unauthorized media from content based on compose capabilities
$content = $this->sanitize_topic_content($user_id, $content);
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return new WP_Error('invalid_board', __('版块不存在', 'aksrt'));
}
if (!isset($this->get_visibility_map()[$visibility])) {
$visibility = AKSRT_Forum::VISIBILITY_PUBLIC;
}
$can_publish = $this->can_user_publish_directly($user_id, $board_id);
$post_status = $can_publish ? 'publish' : 'pending';
$review_status = $can_publish ? AKSRT_Forum::REVIEW_APPROVED : AKSRT_Forum::REVIEW_PENDING;
$post_id = wp_insert_post([
'post_title' => $title,
'post_content' => $content,
'post_status' => $post_status,
'post_author' => $user_id,
'post_type' => 'aksrt_forum_topic',
], true);
if (is_wp_error($post_id)) {
return $post_id;
}
wp_set_object_terms($post_id, [$board_id], 'aksrt_forum_board');
$clean_tags = array_values(array_filter(array_map('sanitize_text_field', $tags)));
if (count($clean_tags) > 5) {
$clean_tags = array_slice($clean_tags, 0, 5);
}
if (!empty($clean_tags)) {
wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag');
}
$topic_cat = sanitize_text_field((string) ($data['topic_cat'] ?? ''));
if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) {
wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat');
}
update_post_meta($post_id, '_aksrt_visibility', $visibility);
update_post_meta($post_id, '_aksrt_review_status', $review_status);
if ($visibility === AKSRT_Forum::VISIBILITY_CREDITS) {
$credits_cost = max(0, (int) ($data['credits_cost'] ?? 0));
update_post_meta($post_id, '_aksrt_credits_cost', $credits_cost);
}
if ($visibility === AKSRT_Forum::VISIBILITY_PAY) {
$pay_price = sanitize_text_field((string) ($data['pay_price'] ?? ''));
update_post_meta($post_id, '_aksrt_pay_price', $pay_price);
}
if ($visibility === AKSRT_Forum::VISIBILITY_ROLE) {
$roles = isset($data['visibility_roles']) && is_array($data['visibility_roles'])
? array_map('sanitize_key', $data['visibility_roles']) : [];
update_post_meta($post_id, '_aksrt_visibility_roles', $roles);
}
update_post_meta($post_id, '_aksrt_special_topic', $special_topic);
// 置顶分级 0~3(§7.3):新建主题默认不置顶。
update_post_meta($post_id, '_aksrt_sticky', AKSRT_Forum::STICKY_NONE);
update_post_meta($post_id, '_aksrt_featured', '0');
update_post_meta($post_id, '_aksrt_locked', '0');
update_post_meta($post_id, '_aksrt_views', 0);
update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type);
if ($bbs_type === AKSRT_Forum::BBS_TYPE_QUESTION) {
update_post_meta($post_id, '_aksrt_question_status', AKSRT_Forum::QUESTION_UNSOLVED);
}
$this->save_cover_meta($post_id, $data);
$vote_meta = $this->prepare_vote_meta($data);
$this->save_vote_meta($post_id, $vote_meta);
if ($post_status === 'publish') {
$this->forum->sync_board_count($board_id);
}
if (class_exists('AKSRT_Credits_Earn') && $post_status === 'publish') {
do_action('aksrt_forum_topic_created', $user_id, $post_id);
}
if ($post_status === 'pending') {
$this->forum->notify_topic_pending($post_id, $user_id);
}
return [
'post_id' => $post_id,
'message' => $post_status === 'publish'
? __('主题已发布', 'aksrt')
: __('主题已提交审核,审核通过后对外展示', 'aksrt'),
'redirect' => $post_status === 'publish'
? get_permalink($post_id)
: add_query_arg('compose_status', 'pending', home_url('/forums/')),
];
}
// ================================================================
// Topic moderation
// ================================================================
/**
* Moderate topic.
*
* @return array<string,mixed>|WP_Error
*/
}
@@ -0,0 +1,419 @@
<?php
/**
* Forum view builder service.
*
* Extracted from AKSRT_Forum to isolate topic listing, home/stream views,
* user-dimension queries, topic/compose views, stats, and sticky topics
* into a cohesive read-path unit.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Forum_View_Builder {
use AKSRT_Forum_View_Builder_Part_1;
use AKSRT_Forum_View_Builder_Part_2;
private AKSRT_Forum $forum;
public function __construct(AKSRT_Forum $forum) {
$this->forum = $forum;
}
public function get_topic_list(array $args = []): array {
$board_id = (int) ($args['board_id'] ?? 0);
$limit = max(1, min(100, (int) ($args['limit'] ?? ($args['per_page'] ?? 20))));
$page = max(1, (int) ($args['page'] ?? $args['paged'] ?? 1));
$offset = array_key_exists('offset', $args)
? max(0, (int) $args['offset'])
: ($page - 1) * $limit;
$status = sanitize_key((string) ($args['status'] ?? 'publish'));
$keyword = sanitize_text_field((string) ($args['keyword'] ?? ''));
$visibility = sanitize_key((string) ($args['visibility'] ?? ''));
$review_status = sanitize_key((string) ($args['review_status'] ?? ''));
$author_id = (int) ($args['author_id'] ?? 0);
$orderby = sanitize_key((string) ($args['orderby'] ?? 'date'));
$viewer_user_id = (int) ($args['viewer_user_id'] ?? get_current_user_id());
$include_hidden = !empty($args['include_hidden']);
$query_args = [
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => $limit,
'offset' => $offset,
'post_status' => $this->forum->normalize_post_status_query($status),
'order' => 'DESC',
'ignore_sticky_posts' => true,
];
if ($orderby === 'views') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_views';
} elseif ($orderby === 'recommend') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_recommend_score';
} elseif ($orderby === 'comment_count') {
$query_args['orderby'] = 'comment_count';
} elseif ($orderby === 'score') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_score';
} elseif ($orderby === 'favorites') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_favorite_count';
} else {
$query_args['orderby'] = 'date';
}
if ($keyword !== '') {
$query_args['s'] = $keyword;
}
if ($author_id > 0) {
$query_args['author'] = $author_id;
}
$plate_id = (int) ($args['plate_id'] ?? 0);
$plate_ids = $args['plate_ids'] ?? [];
if (!empty($plate_ids) && is_array($plate_ids)) {
$query_args['meta_query'][] = [
'key' => '_aksrt_plate_id',
'value' => array_map('intval', $plate_ids),
'type' => 'NUMERIC',
'compare' => 'IN',
];
} elseif ($plate_id > 0) {
$query_args['meta_query'][] = [
'key' => '_aksrt_plate_id',
'value' => $plate_id,
'type' => 'NUMERIC',
'compare' => '=',
];
} elseif ($board_id > 0) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]];
}
$meta_query = [];
if ($visibility !== '') {
$meta_query[] = [
'key' => '_aksrt_visibility',
'value' => $visibility,
'compare' => '=',
];
}
if ($review_status !== '') {
$meta_query[] = [
'key' => '_aksrt_review_status',
'value' => $review_status,
'compare' => '=',
];
}
if (!empty($meta_query)) {
$query_args['meta_query'] = $meta_query;
}
$query = new WP_Query($query_args);
$items = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if (!$post) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$include_hidden && !$topic['can_view']) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
'page' => $page,
];
}
/**
* Get forum homepage view.
*/
public function get_home_view(int $user_id = 0): array {
$boards = $this->forum->get_boards();
$stats = $this->get_stats();
$recent = $this->get_topic_list([
'limit' => 10,
'status' => 'publish',
'viewer_user_id' => $user_id,
]);
$hot = $this->get_topic_list([
'limit' => 8,
'status' => 'publish',
'viewer_user_id' => $user_id,
'orderby' => 'views',
]);
$applications = [];
if ($user_id > 0) {
foreach ($boards as $board) {
$applications[(int) $board['id']] = $this->forum->get_board_application_for_user($user_id, (int) $board['id']);
}
}
$config = $this->forum->get_config();
$home_tabs = $config['homepage']['tabs'] ?? [
['type' => 'follow', 'label' => '关注'],
['type' => 'synthesis', 'label' => '综合'],
['type' => 'plate', 'label' => '版块'],
];
$active_index = (int) ($config['homepage']['tab_active_index'] ?? 1);
$quick_posts = !empty($config['homepage']['quick_posts']);
return [
'boards' => $boards,
'recent_topics' => $recent['items'],
'hot_topics' => $hot['items'],
'stats' => $stats,
'permissions' => [
'can_compose' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'compose_post'),
'can_manage' => $this->forum->is_forum_admin($user_id),
'can_create_plate' => $this->forum->forum_user_can($user_id, 'plate_create'),
'user_tier' => $user_id > 0 ? $this->forum->get_user_plate_tier($user_id, 0) : AKSRT_Forum::TIER_NONE,
],
'applications' => $applications,
'home_tabs' => $home_tabs,
'active_index' => $active_index,
'quick_posts' => $quick_posts,
];
}
// ── §7.10 Tab data streams ──────────────────────────────────────────
/**
* Get topics from plates the user follows.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_follow_stream(int $user_id, int $page = 1, int $limit = 15): array {
if ($user_id <= 0) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
$follow_ids = get_user_meta($user_id, 'aksrt_follow_plate', true);
$follow_ids = is_array($follow_ids) ? array_filter(array_map('intval', $follow_ids)) : [];
if (empty($follow_ids)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
return $this->get_topic_list([
'plate_ids' => $follow_ids,
'page' => $page,
'limit' => $limit,
'status' => 'publish',
'viewer_user_id' => $user_id,
]);
}
/**
* Get synthesis (all topics) stream sorted by date.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_synthesis_stream(int $page = 1, int $limit = 15, int $viewer = 0): array {
return $this->get_topic_list([
'page' => $page,
'limit' => $limit,
'status' => 'publish',
'viewer_user_id' => $viewer,
]);
}
/**
* Get pending-review topics for moderators.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_pending_stream(int $user_id, int $page = 1, int $limit = 15): array {
if ($user_id <= 0 || !$this->forum->is_forum_admin($user_id)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
return $this->get_topic_list([
'page' => $page,
'limit' => $limit,
'status' => 'pending',
'viewer_user_id' => $user_id,
'include_hidden' => true,
]);
}
/**
* AJAX: Load a forum home tab stream.
*/
public function ajax_home_tab(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$tab_type = sanitize_key((string) ($_POST['tab_type'] ?? 'synthesis'));
$page = max(1, (int) ($_POST['page'] ?? 1));
$user_id = get_current_user_id();
switch ($tab_type) {
case 'follow':
$result = $this->get_follow_stream($user_id, $page);
break;
case 'pending':
$result = $this->get_pending_stream($user_id, $page);
break;
case 'plate':
wp_send_json_success([
'html' => '',
'type' => 'plate',
'boards' => $this->forum->get_boards(),
'total' => 0,
'max_pages' => 1,
'page' => 1,
]);
return;
default:
$result = $this->get_synthesis_stream($page, 15, $user_id);
}
ob_start();
if (!empty($result['items'])) {
foreach ($result['items'] as $topic) {
$this->render_topic_row($topic);
}
} else {
echo '<div class="aksrt-forum__empty"><i class="far fa-comment-dots"></i><p>' . esc_html__('暂无内容', 'aksrt') . '</p></div>';
}
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'type' => $tab_type,
'total' => $result['total'],
'max_pages' => $result['max_pages'],
'page' => $page,
'has_more' => $page < $result['max_pages'],
]);
}
/**
* Render a single topic row (reusable for AJAX and initial render).
*/
public function render_topic_row(array $topic): void {
$board = !empty($topic['boards'][0]) ? $topic['boards'][0] : null;
$names = $this->forum->get_config()['names'] ?? [];
?>
<article class="aksrt-forum-topic-row">
<div class="aksrt-forum-topic-row__avatar">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="40" height="40" loading="lazy">
<?php endif; ?>
</div>
<div class="aksrt-forum-topic-row__body">
<div class="aksrt-forum-topic-row__title-wrap">
<?php if (!empty($topic['sticky'])) : ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--sticky"><?php esc_html_e('置顶', 'aksrt'); ?></span>
<?php endif; ?>
<?php if (!empty($topic['featured'])) : ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--featured"><?php esc_html_e('精华', 'aksrt'); ?></span>
<?php endif; ?>
<?php if (($topic['bbs_type'] ?? '') === 'question') : ?>
<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--question"><?php echo esc_html($names['question_badge'] ?? __('提问', 'aksrt')); ?></span>
<?php endif; ?>
<h3 class="aksrt-forum-topic-row__title">
<a href="<?php echo esc_url((string) ($topic['permalink'] ?? '#')); ?>"><?php echo esc_html((string) ($topic['title'] ?? '')); ?></a>
</h3>
</div>
<div class="aksrt-forum-topic-row__meta">
<?php if ($board) : ?>
<a class="aksrt-forum-topic-row__board" href="<?php echo esc_url((string) ($board['url'] ?? '#')); ?>"><?php echo esc_html((string) ($board['name'] ?? '')); ?></a>
<?php endif; ?>
<span class="aksrt-forum-topic-row__author"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__time"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></span>
<span><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></span>
</div>
</div>
</article>
<?php
}
public function get_user_forum_topics(int $user_id, array $args = []): array {
$status = $args['status'] ?? 'publish';
$page = max(1, (int) ($args['page'] ?? 1));
$limit = max(1, min(50, (int) ($args['limit'] ?? 10)));
$query_args = [
'post_type' => 'aksrt_forum_topic',
'author' => $user_id,
'post_status' => $status,
'posts_per_page' => $limit,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
$query = new \WP_Query($query_args);
$items = [];
foreach ($query->posts as $post) {
$items[] = $this->forum->build_topic_payload($post, $user_id);
}
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
/**
* Get a user's favorited forum topics.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_user_favorite_topics(int $user_id, int $page = 1, int $limit = 10): array {
$fav_ids = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$fav_ids = is_array($fav_ids) ? array_map('intval', array_filter($fav_ids)) : [];
if (empty($fav_ids)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post__in' => $fav_ids,
'post_status' => 'publish',
'posts_per_page' => $limit,
'paged' => $page,
'orderby' => 'post__in',
]);
$items = [];
foreach ($query->posts as $post) {
$items[] = $this->forum->build_topic_payload($post, $user_id);
}
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
/**
* Get a user's managed plates (plates where user is moderator/author).
*
* @return array{owned:array,moderating:array}
*/
}
@@ -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')]);
}
}
}
@@ -0,0 +1,410 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Forum_View_Builder_Part_1 {
public function get_user_managed_plates(int $user_id): array {
return $this->forum->get_user_managed_plates($user_id);
}
/**
* Forum stats for a user (counts, cached per request).
*
* @return array{topic_count:int,reply_count:int,favorite_count:int,followed_plate_count:int,managed_plate_count:int}
*/
public function get_user_forum_stats(int $user_id): array {
$topic_count = (int) count_user_posts($user_id, 'aksrt_forum_topic', true);
global $wpdb;
$reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->posts} p ON p.ID = c.comment_post_ID
WHERE c.user_id = %d AND p.post_type = 'aksrt_forum_topic' AND c.comment_approved = '1'",
$user_id
));
$fav_ids = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorite_count = is_array($fav_ids) ? count(array_filter($fav_ids)) : 0;
$follow_ids = get_user_meta($user_id, 'aksrt_follow_plate', true);
$followed_plate_count = is_array($follow_ids) ? count(array_filter($follow_ids)) : 0;
$managed = $this->forum->get_user_managed_plates($user_id);
$managed_count = count($managed['owned']) + count($managed['moderating']);
return [
'topic_count' => $topic_count,
'reply_count' => $reply_count,
'favorite_count' => $favorite_count,
'followed_plate_count' => $followed_plate_count,
'managed_plate_count' => $managed_count,
];
}
public function get_topic_view(int $topic_id, int $user_id = 0): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return [
'topic' => null,
'board' => null,
'permissions' => [],
'board_options' => [],
'visibility_options' => $this->forum->get_visibility_options(),
'review_options' => $this->forum->get_review_options(),
];
}
$topic = $this->forum->build_topic_payload($post, $user_id);
$boards = !empty($topic['boards']) ? $topic['boards'] : [];
$board = !empty($boards[0]) ? $boards[0] : null;
return [
'topic' => $topic,
'board' => $board,
'permissions' => [
'can_edit' => $this->forum->can_edit_topic($user_id, $topic_id),
'can_moderate' => $this->forum->can_moderate_topic($user_id, $topic_id),
// 加锁语义统一到原生 comment_status(§7.3):comment_status=closed 或 _aksrt_locked=1 均不可回复。
'can_reply' => comments_open($topic_id) && empty($topic['locked']),
],
'board_options' => $this->forum->get_boards(),
'visibility_options' => $this->forum->get_visibility_options(),
'review_options' => $this->forum->get_review_options(),
];
}
/**
* Get compose page view.
*/
public function get_compose_view(int $user_id, array $args = []): array {
$board_id = (int) ($args['board_id'] ?? 0);
return [
'boards' => $this->forum->get_boards(),
'selected_board_id' => $board_id,
'visibility_options' => $this->forum->get_visibility_options(),
'publish_mode' => $this->forum->can_user_publish_directly($user_id, $board_id) ? 'publish' : 'pending',
'draft_count' => $this->forum->get_user_draft_count($user_id),
'drafts' => $this->forum->get_user_drafts($user_id, 10),
'capabilities' => $this->forum->get_compose_capabilities($user_id),
];
}
/**
* Get forum stats.
*/
public function get_stats(): array {
$boards = $this->forum->get_boards();
$topic_count = post_type_exists('aksrt_forum_topic')
? (int) (wp_count_posts('aksrt_forum_topic')->publish ?? 0)
: 0;
$pending_topic_count = post_type_exists('aksrt_forum_topic')
? (int) (wp_count_posts('aksrt_forum_topic')->pending ?? 0)
: 0;
global $wpdb;
$post_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
WHERE p.post_type = 'aksrt_forum_topic' AND c.comment_approved = '1'"
);
$moderator_count = 0;
$pending_application_count = 0;
foreach ($boards as $board) {
$moderator_count += (int) ($board['moderator_count'] ?? 0);
$pending_application_count += (int) ($board['pending_application_count'] ?? 0);
}
return [
'board_count' => count($boards),
'topic_count' => $topic_count,
'post_count' => $post_count,
'pending_topic_count' => $pending_topic_count,
'moderator_count' => $moderator_count,
'pending_application_count' => $pending_application_count,
];
}
public function get_sticky_topics_for_board(int $board_id): array {
$viewer_user_id = get_current_user_id();
// 先取本版块内的置顶帖(plate/super),再取全局置顶帖(跨版块)。
$query = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 20,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'orderby' => 'meta_value_num date',
'order' => 'DESC DESC',
'meta_query' => [
'relation' => 'OR',
[
'key' => '_aksrt_sticky',
'value' => [AKSRT_Forum::STICKY_PLATE, AKSRT_Forum::STICKY_SUPER],
'compare' => 'IN',
'type' => 'NUMERIC',
],
[
'key' => '_aksrt_sticky',
'value' => AKSRT_Forum::STICKY_GLOBAL,
'compare' => '=',
'type' => 'NUMERIC',
],
],
'tax_query' => [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]],
]);
$items = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if (!$post) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$topic['can_view']) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
// 全局置顶帖可能不归属本版块,单独查询后合并去重。
$global = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 5,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'meta_query' => [[
'key' => '_aksrt_sticky',
'value' => AKSRT_Forum::STICKY_GLOBAL,
'compare' => '=',
'type' => 'NUMERIC',
]],
]);
$existing_ids = array_map(static fn($t) => (int) $t['id'], $items);
while ($global->have_posts()) {
$global->the_post();
$post = get_post(get_the_ID());
if (!$post || in_array((int) $post->ID, $existing_ids, true)) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$topic['can_view']) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
// 按 sticky_level DESC, date DESC 排序(全局 > 超级 > 版块)。
usort($items, function (array $a, array $b): int {
$la = (int) ($a['sticky_level'] ?? 0);
$lb = (int) ($b['sticky_level'] ?? 0);
if ($la !== $lb) {
return $lb <=> $la;
}
return strcmp((string) $b['date'], (string) $a['date']);
});
return array_slice($items, 0, 5);
}
/**
* Get topic detail payload for admin.
*/
public function get_topic_detail(int $topic_id, int $viewer_user_id = 0): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return [
'topic' => null,
'options' => [
'boards' => $this->forum->get_boards(),
'visibility' => $this->forum->get_visibility_options(),
'review' => $this->forum->get_review_options(),
],
];
}
return [
'topic' => $this->forum->build_topic_payload($post, $viewer_user_id),
'options' => [
'boards' => $this->forum->get_boards(),
'visibility' => $this->forum->get_visibility_options(),
'review' => $this->forum->get_review_options(),
],
];
}
// ── Tags + Search (extracted from facade) ──
public function get_tags(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_forum_tag',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
]);
if (is_wp_error($terms)) {
return [];
}
return array_map(static function ($term): array {
$url = get_term_link($term);
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'count' => (int) $term->count,
'url' => !is_wp_error($url) ? (string) $url : '',
];
}, $terms);
}
/**
* Get tag archive page payload.
*/
public function get_tag_archive_view(array $args = []): array {
$term_id = (int) ($args['term_id'] ?? 0);
$paged = max(1, (int) ($args['paged'] ?? 1));
$per_page = 20;
$term = get_term($term_id, 'aksrt_forum_tag');
if (is_wp_error($term) || !$term) {
return ['topics' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
}
$query = new WP_Query([
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => $per_page,
'paged' => $paged,
'post_status' => 'publish',
'tax_query' => [[
'taxonomy' => 'aksrt_forum_tag',
'field' => 'term_id',
'terms' => $term_id,
]],
]);
$topics = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if ($post) {
$topics[] = $this->forum->build_topic_payload($post, get_current_user_id());
}
}
wp_reset_postdata();
return [
'topics' => $topics,
'term' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'count' => (int) $term->count,
],
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
public function increment_view_count(int $topic_id): void {
if ($topic_id <= 0) {
return;
}
$views = (int) get_post_meta($topic_id, '_aksrt_views', true);
update_post_meta($topic_id, '_aksrt_views', $views + 1);
}
public function ajax_search_boards(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
if ($this->forum->is_plate_cpt_active()) {
$args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'no_found_rows' => true,
'orderby' => 'title',
'order' => 'ASC',
];
if ($search !== '') {
$args['s'] = $search;
}
$query = new WP_Query($args);
$results = [];
while ($query->have_posts()) {
$query->the_post();
$p = get_post();
if ($p) {
$results[] = ['id' => $p->ID, 'name' => $p->post_title];
}
}
wp_reset_postdata();
} else {
$args = [
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'name',
'order' => 'ASC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = ['id' => (int) $term->term_id, 'name' => (string) $term->name];
}
}
}
wp_send_json_success($results);
}
public function ajax_search_tags(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
$args = [
'taxonomy' => 'aksrt_forum_tag',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'count',
'order' => 'DESC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'count' => (int) $term->count,
];
}
}
wp_send_json_success($results);
}
}
@@ -0,0 +1,110 @@
<?php
defined('ABSPATH') || exit;
trait AKSRT_Forum_View_Builder_Part_2 {
public function ajax_search_topics(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
if (!taxonomy_exists('aksrt_forum_topic_cat')) {
wp_send_json_success([]);
}
$args = [
'taxonomy' => 'aksrt_forum_topic_cat',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'name',
'order' => 'ASC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = ['id' => (int) $term->term_id, 'name' => (string) $term->name];
}
}
wp_send_json_success($results);
}
public function template_routing(string $template): string {
// Plate CPT single → board template
if (is_singular('aksrt_forum_plate')) {
$plate_template = AKSRT_DIR . '/templates/forum/forum-plate.php';
if (file_exists($plate_template)) {
return $plate_template;
}
$board_template = AKSRT_DIR . '/templates/forum/forum-board.php';
if (file_exists($board_template)) {
return $board_template;
}
}
// Plate category (分区) archive
if (is_tax('aksrt_plate_cat')) {
$cat_template = AKSRT_DIR . '/templates/forum/forum-plate-cat.php';
if (file_exists($cat_template)) {
return $cat_template;
}
}
if (is_tax('aksrt_forum_board')) {
$board_template = AKSRT_DIR . '/templates/forum/forum-board.php';
if (file_exists($board_template)) {
return $board_template;
}
}
if (is_tax('aksrt_forum_tag')) {
$tag_template = AKSRT_DIR . '/templates/forum/forum-tag.php';
if (file_exists($tag_template)) {
return $tag_template;
}
}
if (is_singular('aksrt_forum_topic')) {
if (isset($_GET['edit']) && $_GET['edit'] === '1') {
$edit_template = AKSRT_DIR . '/templates/forum/forum-edit.php';
if (file_exists($edit_template)) {
return $edit_template;
}
}
$topic_template = AKSRT_DIR . '/templates/forum/forum-topic.php';
if (file_exists($topic_template)) {
return $topic_template;
}
}
return $template;
}
public function ajax_topic_list(): void {
$user_id = get_current_user_id();
if (!$this->forum->is_forum_admin($user_id)) {
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
}
$board_id = (int) ($_GET['board_id'] ?? 0);
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 20)));
$view = $this->get_topic_list([
'board_id' => $board_id,
'limit' => $limit,
'status' => 'all',
'viewer_user_id' => get_current_user_id(),
'include_hidden' => true,
]);
wp_send_json_success([
'topics' => $view['items'],
'stats' => $this->get_stats(),
]);
}
}