Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
382 lines
13 KiB
PHP
382 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Messages;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class PrivateMessage {
|
|
|
|
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\\Messages\\Message')) {
|
|
$sender_name = '';
|
|
$sender_user = get_userdata($sender_id);
|
|
if ($sender_user) {
|
|
$sender_name = $sender_user->display_name;
|
|
}
|
|
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)]);
|
|
}
|
|
}
|