*/ private const ASSIGNABLE_ROLES = [ 'super_moderator' => '超级版主', 'moderator' => '版主', 'junior_moderator' => '初级版主', ]; /** @var list */ private const ALLOW_VIEW = ['public', 'login', 'follow', 'role']; /** @var list */ private const ADD_LIMIT = ['all', 'login', 'moderator', 'role']; public static function get_instance(): self { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function init(): void { if (!is_admin()) { return; } add_action('add_meta_boxes', [$this, 'register_meta_box']); add_action('save_post_aksrt_forum_plate', [$this, 'save'], 20, 2); } public function register_meta_box(): void { add_meta_box( 'aksrt_plate_moderators', __('版主与权限', 'aksrt'), [$this, 'render'], 'aksrt_forum_plate', 'side', 'high' ); } public function render(WP_Post $post): void { wp_nonce_field('aksrt_plate_meta_save', 'aksrt_plate_meta_nonce'); $plate_id = (int) $post->ID; $author_id = (int) $post->post_author; $moderator_ids = []; $roles_map = []; $applications = []; if (class_exists('AKSRT_Forum')) { $forum = AKSRT_Forum::get_instance(); $moderator_ids = $forum->get_plate_moderators($plate_id); $roles_map = $forum->get_moderator_roles($plate_id); $applications = $forum->get_plate_moderator_applications($plate_id); } else { $raw = get_post_meta($plate_id, '_aksrt_moderators', true); $moderator_ids = is_array($raw) ? array_map('intval', $raw) : []; $roles_raw = get_post_meta($plate_id, '_aksrt_moderator_roles', true); $roles_map = is_array($roles_raw) ? $roles_raw : []; $applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true); $applications = is_array($applications) ? $applications : []; } $allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'); if (!in_array($allow_view, self::ALLOW_VIEW, true)) { // Rewrite legacy values (e.g. moderator) on display; save will persist new contract. $allow_view = 'login'; } $add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'); if (!in_array($add_limit, self::ADD_LIMIT, true)) { $add_limit = 'all'; } $view_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true); $view_roles = is_array($view_roles) ? array_map('sanitize_key', $view_roles) : []; $add_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true); $add_roles = is_array($add_roles) ? array_map('sanitize_key', $add_roles) : []; $wp_roles = $this->wp_role_options(); $icon = (string) get_post_meta($plate_id, '_aksrt_icon', true); $sort_order = (int) get_post_meta($plate_id, '_aksrt_sort_order', true); $follow_pay = $this->get_follow_pay_meta($plate_id); $this->render_access_fields($allow_view, $add_limit, $view_roles, $add_roles, $wp_roles); $this->render_follow_pay_fields($follow_pay); $this->render_display_fields($icon, $sort_order); $this->render_moderator_fields($moderator_ids, $roles_map, $author_id); $this->render_applications($applications); } /** * @param list $view_roles * @param list $add_roles * @param array $wp_roles */ private function render_access_fields(string $allow_view, string $add_limit, array $view_roles, array $add_roles, array $wp_roles): void { ?>




$moderator_ids * @param array $roles_map */ private function render_moderator_fields(array $moderator_ids, array $roles_map, int $author_id): void { if ($author_id > 0) { $author = get_userdata($author_id); echo '

' . esc_html__('超级版主(版块作者)', 'aksrt') . '
'; echo '' . esc_html($author ? $author->user_login . ' (#' . $author_id . ')' : (string) $author_id) . ''; echo '
' . esc_html__('由版块作者决定,不可在此移除。', 'aksrt') . '

'; } echo '

' . esc_html__('版主列表', 'aksrt') . '

'; $rows = 0; foreach ($moderator_ids as $uid) { $uid = (int) $uid; if ($uid <= 0 || $uid === $author_id) { continue; } $user = get_userdata($uid); $role = sanitize_key((string) ($roles_map[$uid] ?? 'moderator')); if (!isset(self::ASSIGNABLE_ROLES[$role])) { $role = 'moderator'; } $rows++; echo '
'; echo '
' . esc_html($user ? $user->user_login . ' (#' . $uid . ')' : (string) $uid) . '
'; echo ''; echo ''; echo '
'; } if ($rows === 0) { echo '

' . esc_html__('暂无版主。', 'aksrt') . '

'; } echo '

' . esc_html__('添加版主', 'aksrt') . '

'; echo '

'; echo '

'; echo '

' . esc_html__('保存后生效。勿将版块作者重复加为版主。', 'aksrt') . '

'; echo '
'; } /** * @param array> $applications */ private function render_applications(array $applications): void { echo '

' . esc_html__('版主申请', 'aksrt') . '

'; $pending = array_values(array_filter($applications, static function ($app) { if (!is_array($app)) { return false; } $status = (string) ($app['status'] ?? ''); if ($status === 'pending') { return true; } return class_exists('AKSRT_Forum') && $status === AKSRT_Forum::APPLICATION_PENDING; })); if (!$pending) { echo '

' . esc_html__('暂无待处理申请。', 'aksrt') . '

'; return; } echo '
    '; foreach ($pending as $app) { $uid = (int) ($app['user_id'] ?? 0); $user = $uid > 0 ? get_userdata($uid) : null; $aid = (string) ($app['id'] ?? ''); $reason = (string) ($app['reason'] ?? ''); echo '
  • '; echo '
    ' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : (string) $uid) . '
    '; if ($reason !== '') { echo '
    ' . esc_html($reason) . '
    '; } echo ''; echo ''; echo ''; echo '
  • '; } echo '
'; } public function save(int $post_id, WP_Post $post): void { if (!isset($_POST['aksrt_plate_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_plate_meta_nonce'])), 'aksrt_plate_meta_save')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if ($post->post_type !== 'aksrt_forum_plate' || !current_user_can('edit_post', $post_id)) { return; } $this->save_access_meta($post_id); $this->save_follow_pay_meta($post_id); $this->save_display_meta($post_id); $this->save_moderators($post_id, (int) $post->post_author); $this->save_applications(); } private function save_display_meta(int $post_id): void { $icon = isset($_POST['aksrt_plate_icon']) ? sanitize_text_field((string) wp_unslash($_POST['aksrt_plate_icon'])) : ''; update_post_meta($post_id, '_aksrt_icon', $icon); $sort = isset($_POST['aksrt_plate_sort_order']) ? (int) wp_unslash($_POST['aksrt_plate_sort_order']) : 0; update_post_meta($post_id, '_aksrt_sort_order', $sort); } private function save_access_meta(int $post_id): void { $allow_view = isset($_POST['aksrt_plate_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_allow_view'])) : 'public'; if (!in_array($allow_view, self::ALLOW_VIEW, true)) { $allow_view = 'public'; } update_post_meta($post_id, '_aksrt_allow_view', $allow_view); if ($allow_view === 'role') { $roles = $this->sanitize_role_list($_POST['aksrt_plate_allow_view_roles'] ?? []); update_post_meta($post_id, '_aksrt_allow_view_roles', $roles); } else { delete_post_meta($post_id, '_aksrt_allow_view_roles'); } $add_limit = isset($_POST['aksrt_plate_add_limit']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_add_limit'])) : 'all'; if (!in_array($add_limit, self::ADD_LIMIT, true)) { $add_limit = 'all'; } update_post_meta($post_id, '_aksrt_add_limit', $add_limit); if ($add_limit === 'role') { $roles = $this->sanitize_role_list($_POST['aksrt_plate_add_limit_roles'] ?? []); update_post_meta($post_id, '_aksrt_add_limit_roles', $roles); } else { delete_post_meta($post_id, '_aksrt_add_limit_roles'); } } private function save_follow_pay_meta(int $post_id): void { $enabled = !empty($_POST['aksrt_plate_follow_pay_enabled']); $modo = isset($_POST['aksrt_plate_follow_pay_modo']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_follow_pay_modo'])) : 'money'; if (!in_array($modo, ['money', 'points'], true)) { $modo = 'money'; } $price = isset($_POST['aksrt_plate_follow_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_price'])) : 0; $points = isset($_POST['aksrt_plate_follow_pay_points']) ? max(0, (int) wp_unslash($_POST['aksrt_plate_follow_pay_points'])) : 0; // Invalid price while enabled → force off (normalize, no migration). if ($enabled) { if ($modo === 'points' && $points <= 0) { $enabled = false; } if ($modo === 'money' && $price <= 0) { $enabled = false; } } if (!$enabled) { delete_post_meta($post_id, '_aksrt_follow_pay'); delete_post_meta($post_id, '_aksrt_follow_pay_modo'); delete_post_meta($post_id, '_aksrt_follow_pay_price'); delete_post_meta($post_id, '_aksrt_follow_pay_points'); delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s'); return; } update_post_meta($post_id, '_aksrt_follow_pay', 1); update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo); update_post_meta($post_id, '_aksrt_follow_pay_price', $price); update_post_meta($post_id, '_aksrt_follow_pay_points', $points); update_post_meta($post_id, '_aksrt_follow_pay_coupon_s', !empty($_POST['aksrt_plate_follow_pay_coupon_s']) ? 1 : 0); if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') { $vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price'])); update_post_meta($post_id, '_aksrt_follow_pay_vip1_price', $vip1); // Keep legacy single VIP key in sync with VIP1 for old readers. update_post_meta($post_id, '_aksrt_follow_pay_vip_price', $vip1); } else { delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price'); delete_post_meta($post_id, '_aksrt_follow_pay_vip_price'); } if (isset($_POST['aksrt_plate_follow_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']) !== '') { update_post_meta($post_id, '_aksrt_follow_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']))); } else { delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price'); } } /** * @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string,coupon_s:bool} */ private function get_follow_pay_meta(int $plate_id): array { $enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); if (!in_array($modo, ['money', 'points'], true)) { $modo = 'money'; } $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true); $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); if (!$vip1_set) { // Legacy single VIP key → vip1. $vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true); $vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false); } $vip2_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true); $vip2_set = ($vip2_raw !== '' && $vip2_raw !== null && $vip2_raw !== false); return [ 'enabled' => $enabled, 'modo' => $modo, 'price' => (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true), 'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true), 'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '', 'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '', 'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1, ]; } private function save_moderators(int $post_id, int $author_id): void { $role_posted = isset($_POST['aksrt_plate_mod_role']) && is_array($_POST['aksrt_plate_mod_role']) ? wp_unslash($_POST['aksrt_plate_mod_role']) : []; $remove = isset($_POST['aksrt_plate_mod_remove']) && is_array($_POST['aksrt_plate_mod_remove']) ? array_map('intval', wp_unslash($_POST['aksrt_plate_mod_remove'])) : []; $remove_set = array_fill_keys($remove, true); $desired = []; foreach ($role_posted as $uid => $role) { $uid = (int) $uid; $role = sanitize_key((string) $role); if ($uid <= 0 || $uid === $author_id || isset($remove_set[$uid])) { continue; } if (!isset(self::ASSIGNABLE_ROLES[$role])) { $role = 'moderator'; } $desired[$uid] = $role; } $add_id = isset($_POST['aksrt_plate_mod_add_id']) ? (int) wp_unslash($_POST['aksrt_plate_mod_add_id']) : 0; $add_role = isset($_POST['aksrt_plate_mod_add_role']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_mod_add_role'])) : 'moderator'; if ($add_id > 0 && $add_id !== $author_id) { if (!isset(self::ASSIGNABLE_ROLES[$add_role])) { $add_role = 'moderator'; } $desired[$add_id] = $add_role; } if (class_exists('AKSRT_Forum')) { $forum = AKSRT_Forum::get_instance(); $current = $forum->get_plate_moderators($post_id); foreach ($current as $uid) { $uid = (int) $uid; if ($uid === $author_id) { continue; } if (!isset($desired[$uid])) { $forum->remove_moderator($uid, $post_id); } } foreach ($desired as $uid => $role) { if (in_array($uid, $current, true)) { $forum->set_moderator_role($uid, $post_id, $role); } else { $forum->add_moderator($uid, $post_id, $role); } } return; } // Fallback without forum service: rewrite meta directly (no legacy keep). $ids = array_values(array_unique(array_map('intval', array_keys($desired)))); update_post_meta($post_id, '_aksrt_moderators', $ids); update_post_meta($post_id, '_aksrt_moderator_roles', $desired); } private function save_applications(): void { $actions = isset($_POST['aksrt_plate_app']) && is_array($_POST['aksrt_plate_app']) ? wp_unslash($_POST['aksrt_plate_app']) : []; if (!$actions || !class_exists('AKSRT_Forum')) { return; } $forum = AKSRT_Forum::get_instance(); foreach ($actions as $application_id => $action) { $application_id = sanitize_text_field((string) $application_id); $action = sanitize_key((string) $action); if ($application_id === '' || $action === 'keep') { continue; } if ($action === 'approved') { $forum->review_moderator_application($application_id, AKSRT_Forum::APPLICATION_APPROVED, '', get_current_user_id()); } elseif ($action === 'rejected') { $forum->review_moderator_application($application_id, AKSRT_Forum::APPLICATION_REJECTED, '', get_current_user_id()); } } } /** * @param mixed $raw * @return list */ private function sanitize_role_list($raw): array { if (!is_array($raw)) { return []; } $valid = array_keys($this->wp_role_options()); $out = []; foreach ($raw as $role) { $role = sanitize_key((string) $role); if ($role !== '' && in_array($role, $valid, true)) { $out[] = $role; } } return array_values(array_unique($out)); } /** * @return array */ private function wp_role_options(): array { $out = []; if (!function_exists('wp_roles')) { return $out; } foreach (wp_roles()->roles as $slug => $data) { $out[sanitize_key((string) $slug)] = translate_user_role((string) ($data['name'] ?? $slug)); } return $out; } }