Files
wordpress-theme-aksrt/inc/classes/forum/class-aksrt-forum-moderation-service.php
T

721 lines
27 KiB
PHP

<?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;
}
}