prefix . 'aksrt_conversation'; } private function table_msg(): string { global $wpdb; return $wpdb->prefix . 'aksrt_conversation_message'; } /* ------------------------------------------------------------------ * Conversation CRUD * ----------------------------------------------------------------*/ /** * Get or create a 1-to-1 conversation between two users. */ public function get_or_create_conversation(int $user_a, int $user_b): int { global $wpdb; if ($user_a <= 0 || $user_b <= 0 || $user_a === $user_b) return 0; // Ensure consistent ordering $min_id = min($user_a, $user_b); $max_id = max($user_a, $user_b); $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$this->table_conv()} WHERE user_a = %d AND user_b = %d", $min_id, $max_id )); if ($existing) return (int) $existing; $wpdb->insert($this->table_conv(), [ 'user_a' => $min_id, 'user_b' => $max_id, 'last_message' => '', 'last_message_at' => current_time('mysql'), 'unread_a' => 0, 'unread_b' => 0, 'created_at' => current_time('mysql'), ], ['%d', '%d', '%s', '%s', '%d', '%d', '%s']); return (int) $wpdb->insert_id; } /** * List conversations for a user (most recent first). * * @param int $user_id * @param int $limit * @return array */ public function get_conversations(int $user_id, int $limit = 20): array { global $wpdb; if ($user_id <= 0) return []; $table_conv = $this->table_conv(); $table_msg = $this->table_msg(); $rows = $wpdb->get_results($wpdb->prepare( "SELECT c.*, CASE WHEN c.user_a = %d THEN c.unread_a ELSE c.unread_b END AS my_unread, CASE WHEN c.user_a = %d THEN c.user_b ELSE c.user_a END AS other_user_id FROM {$table_conv} c WHERE c.user_a = %d OR c.user_b = %d ORDER BY c.last_message_at DESC LIMIT %d", $user_id, $user_id, $user_id, $user_id, $limit ), ARRAY_A); $conversations = []; foreach ((array) $rows as $row) { $other_id = (int) $row['other_user_id']; $other_user = get_userdata($other_id); $conversations[] = [ 'id' => (int) $row['id'], 'other_user_id' => $other_id, 'other_name' => $other_user ? $other_user->display_name : __('用户', 'aksrt'), 'other_avatar' => $other_user ? get_avatar_url($other_id, ['size' => 48]) : '', 'last_message' => $row['last_message'] ?? '', 'last_message_at' => $row['last_message_at'] ?? '', 'unread' => (int) $row['my_unread'], ]; } return $conversations; } /* ------------------------------------------------------------------ * Message CRUD * ----------------------------------------------------------------*/ /** * Send a message in a conversation. * * @param int $conversation_id * @param int $sender_id * @param string $content * @return array{success: bool, message_id?: int, message: string} */ public function send(int $conversation_id, int $sender_id, string $content): array { global $wpdb; if ($conversation_id <= 0 || $sender_id <= 0) { return ['success' => false, 'message' => __('参数无效', 'aksrt')]; } $content = sanitize_textarea_field($content); if ($content === '') { return ['success' => false, 'message' => __('消息不能为空', 'aksrt')]; } // Verify user is a participant $conv = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->table_conv()} WHERE id = %d", $conversation_id ), ARRAY_A); if (!$conv) { return ['success' => false, 'message' => __('会话不存在', 'aksrt')]; } if ((int) $conv['user_a'] !== $sender_id && (int) $conv['user_b'] !== $sender_id) { return ['success' => false, 'message' => __('无权操作', 'aksrt')]; } $now = current_time('mysql'); // Insert message $wpdb->insert($this->table_msg(), [ 'conversation_id' => $conversation_id, 'sender_id' => $sender_id, 'content' => $content, 'created_at' => $now, ], ['%d', '%d', '%s', '%s']); $message_id = (int) $wpdb->insert_id; if (!$message_id) { return ['success' => false, 'message' => __('发送失败', 'aksrt')]; } // Update conversation metadata $is_a = (int) $conv['user_a'] === $sender_id; $unread_field = $is_a ? 'unread_b' : 'unread_a'; $wpdb->update($this->table_conv(), [ 'last_message' => mb_substr($content, 0, 200), 'last_message_at' => $now, $unread_field => (int) $conv[$unread_field] + 1, ], ['id' => $conversation_id]); // Create notification for the recipient $recipient_id = $is_a ? (int) $conv['user_b'] : (int) $conv['user_a']; if (class_exists('AKSRT_Message')) { $sender_name = ''; $sender_user = get_userdata($sender_id); if ($sender_user) { $sender_name = $sender_user->display_name; } AKSRT_Message::get_instance()->send( $recipient_id, 'conversation', mb_substr($content, 0, 100), [ 'title' => sprintf(__('%s 发来私信', 'aksrt'), $sender_name), 'target_url' => add_query_arg(['msg_type' => 'conversation']), ] ); } do_action('aksrt_private_message_sent', $conversation_id, $sender_id, $recipient_id, $message_id); return ['success' => true, 'message_id' => $message_id, 'message' => __('已发送', 'aksrt')]; } /** * Get messages in a conversation (paginated, newest first). * * @param int $conversation_id * @param int $user_id Must be a participant. * @param int $page * @param int $per_page * @return array */ public function get_thread(int $conversation_id, int $user_id, int $page = 1, int $per_page = 30): array { global $wpdb; if ($conversation_id <= 0 || $user_id <= 0) return ['messages' => [], 'total' => 0]; // Verify participation $conv = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->table_conv()} WHERE id = %d", $conversation_id ), ARRAY_A); if (!$conv || ((int) $conv['user_a'] !== $user_id && (int) $conv['user_b'] !== $user_id)) { return ['messages' => [], 'total' => 0]; } $offset = max(0, ($page - 1) * $per_page); $total = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$this->table_msg()} WHERE conversation_id = %d", $conversation_id )); $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$this->table_msg()} WHERE conversation_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d", $conversation_id, $per_page, $offset ), ARRAY_A); $messages = []; foreach ((array) $rows as $row) { $sender = get_userdata((int) $row['sender_id']); $messages[] = [ 'id' => (int) $row['id'], 'sender_id' => (int) $row['sender_id'], 'sender_name' => $sender ? $sender->display_name : __('用户', 'aksrt'), 'sender_avatar' => $sender ? get_avatar_url((int) $row['sender_id'], ['size' => 32]) : '', 'content' => $row['content'] ?? '', 'is_mine' => (int) $row['sender_id'] === $user_id, 'created_at' => $row['created_at'] ?? '', ]; } // Mark as read for this user $this->mark_conversation_read($conversation_id, $user_id); return [ 'messages' => $messages, 'total' => $total, 'page' => $page, 'pages' => max(1, (int) ceil($total / $per_page)), ]; } /** * Mark all messages in a conversation as read for a user. */ public function mark_conversation_read(int $conversation_id, int $user_id): void { global $wpdb; $conv = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->table_conv()} WHERE id = %d", $conversation_id ), ARRAY_A); if (!$conv) return; $is_a = (int) $conv['user_a'] === $user_id; $field = $is_a ? 'unread_a' : 'unread_b'; $wpdb->update($this->table_conv(), [$field => 0], ['id' => $conversation_id]); } /** * Get total unread conversation count for a user. */ public function get_unread_count(int $user_id): int { global $wpdb; if ($user_id <= 0) return 0; $count = $wpdb->get_var($wpdb->prepare( "SELECT SUM(CASE WHEN user_a = %d THEN unread_a ELSE unread_b END) FROM {$this->table_conv()} WHERE user_a = %d OR user_b = %d", $user_id, $user_id, $user_id )); return max(0, (int) $count); } /* ------------------------------------------------------------------ * AJAX Handlers * ----------------------------------------------------------------*/ public function ajax_send(): void { check_ajax_referer('aksrt_pm', '_nonce'); $user_id = get_current_user_id(); if ($user_id <= 0) wp_send_json_error(['message' => __('请先登录', 'aksrt')]); $recipient_id = (int) ($_POST['recipient_id'] ?? 0); $content = sanitize_textarea_field((string) ($_POST['content'] ?? '')); if ($recipient_id <= 0) wp_send_json_error(['message' => __('收件人无效', 'aksrt')]); $conv_id = $this->get_or_create_conversation($user_id, $recipient_id); if (!$conv_id) wp_send_json_error(['message' => __('无法创建会话', 'aksrt')]); $result = $this->send($conv_id, $user_id, $content); if ($result['success']) { wp_send_json_success($result); } wp_send_json_error($result); } public function ajax_conversations(): void { check_ajax_referer('aksrt_pm', '_nonce'); $user_id = get_current_user_id(); if ($user_id <= 0) wp_send_json_error(['message' => __('请先登录', 'aksrt')]); $limit = max(1, min(50, (int) ($_GET['limit'] ?? 20))); $conversations = $this->get_conversations($user_id, $limit); wp_send_json_success(['conversations' => $conversations]); } public function ajax_thread(): void { check_ajax_referer('aksrt_pm', '_nonce'); $user_id = get_current_user_id(); if ($user_id <= 0) wp_send_json_error(['message' => __('请先登录', 'aksrt')]); $conv_id = (int) ($_GET['conversation_id'] ?? 0); $page = max(1, (int) ($_GET['page'] ?? 1)); $result = $this->get_thread($conv_id, $user_id, $page); wp_send_json_success($result); } public function ajax_mark_read(): void { check_ajax_referer('aksrt_pm', '_nonce'); $user_id = get_current_user_id(); if ($user_id <= 0) wp_send_json_error(['message' => __('请先登录', 'aksrt')]); $conv_id = (int) ($_POST['conversation_id'] ?? 0); $this->mark_conversation_read($conv_id, $user_id); wp_send_json_success(['unread' => $this->get_unread_count($user_id)]); } public function ajax_unread_count(): void { check_ajax_referer('aksrt_pm', '_nonce'); $user_id = get_current_user_id(); if ($user_id <= 0) wp_send_json_error(['message' => __('请先登录', 'aksrt')]); wp_send_json_success(['unread' => $this->get_unread_count($user_id)]); } }