Files
wordpress-theme-aksrt/inc/classes/class-aksrt-private-message.php
T

385 lines
13 KiB
PHP

<?php
/**
* AKSRT Private Message — 站内私信/会话系统
*
* 提供会话创建、消息发送、消息拉取、未读管理。
* 存储:{prefix}aksrt_conversation + {prefix}aksrt_conversation_message
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Private_Message {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public function init(): void {
add_action('wp_ajax_aksrt_pm_send', [$this, 'ajax_send']);
add_action('wp_ajax_aksrt_pm_conversations', [$this, 'ajax_conversations']);
add_action('wp_ajax_aksrt_pm_thread', [$this, 'ajax_thread']);
add_action('wp_ajax_aksrt_pm_mark_read', [$this, 'ajax_mark_read']);
add_action('wp_ajax_aksrt_pm_unread_count', [$this, 'ajax_unread_count']);
}
/* ------------------------------------------------------------------
* Table name helpers
* ----------------------------------------------------------------*/
private function table_conv(): string {
global $wpdb;
return $wpdb->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)]);
}
}