refactor: flatten src/AKSRT → src (drop theme-identifier dir)
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).
This commit is contained in:
1 parent
56f1a8175f
commit
8c5b1555c9
285 files changed
+26
-3600
No files matched your search
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Messages;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class 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_mark_message_read', [$this, 'ajax_mark_read']);
|
||||
add_action('wp_ajax_aksrt_get_unread_count', [$this, 'ajax_get_unread_count']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户未读消息数
|
||||
*/
|
||||
public function get_unread_count(int $user_id): int {
|
||||
if ($user_id <= 0) return 0;
|
||||
|
||||
// 优先从表中实时统计
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$count = (int) $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0",
|
||||
$user_id
|
||||
));
|
||||
// 同步到 user_meta 以兼容旧读取方
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', $count);
|
||||
return $count;
|
||||
}
|
||||
|
||||
// 降级:从 user_meta 读取
|
||||
$count = (int) get_user_meta($user_id, 'aksrt_unread_message_count', true);
|
||||
return max(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记单条通知为已读
|
||||
*/
|
||||
public function mark_read(int $user_id, string $message_id): bool {
|
||||
if ($user_id <= 0 || empty($message_id)) return false;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql')],
|
||||
['id' => (int) $message_id, 'user_id' => $user_id, 'is_read' => 0],
|
||||
['%d', '%s'],
|
||||
['%d', '%d', '%d']
|
||||
);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
// 降级:仅更新计数
|
||||
$current = $this->get_unread_count($user_id);
|
||||
if ($current > 0) {
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', max(0, $current - 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记用户所有通知为已读
|
||||
*/
|
||||
public function mark_all_read(int $user_id): bool {
|
||||
if ($user_id <= 0) return false;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$wpdb->update(
|
||||
$table,
|
||||
['is_read' => 1, 'read_at' => current_time('mysql')],
|
||||
['user_id' => $user_id, 'is_read' => 0],
|
||||
['%d', '%s'],
|
||||
['%d', '%d']
|
||||
);
|
||||
}
|
||||
|
||||
update_user_meta($user_id, 'aksrt_unread_message_count', 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送站内通知(持久化到 aksrt_notification 表)
|
||||
*
|
||||
* @param int $to_user_id 接收人 ID
|
||||
* @param string $type 通知类型 (comment_reply, download_ready, conversation, notice, ...)
|
||||
* @param string $content 通知内容摘要
|
||||
* @param array $data 附加数据 (title, target_url, ...)
|
||||
* @return bool
|
||||
*/
|
||||
public function send(int $to_user_id, string $type, string $content, array $data = []): bool {
|
||||
if ($to_user_id <= 0 || empty($content)) return false;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
$wpdb->insert($table, [
|
||||
'user_id' => $to_user_id,
|
||||
'type' => sanitize_key($type),
|
||||
'title' => sanitize_text_field((string) ($data['title'] ?? $content)),
|
||||
'description' => sanitize_textarea_field($content),
|
||||
'target_url' => esc_url_raw((string) ($data['target_url'] ?? '')),
|
||||
'is_read' => 0,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%s', '%s', '%s', '%d', '%s']);
|
||||
}
|
||||
|
||||
// 同步更新 user_meta 未读计数
|
||||
$current = (int) get_user_meta($to_user_id, 'aksrt_unread_message_count', true);
|
||||
update_user_meta($to_user_id, 'aksrt_unread_message_count', max(0, $current) + 1);
|
||||
|
||||
// 触发通知钩子
|
||||
do_action('aksrt_notification_sent', $to_user_id, $type, $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户通知列表
|
||||
*/
|
||||
public function get_notifications(int $user_id, array $args = []): array {
|
||||
if ($user_id <= 0) return [];
|
||||
|
||||
$table = $this->table();
|
||||
if ($table !== '') {
|
||||
global $wpdb;
|
||||
|
||||
$where = ['user_id = %d'];
|
||||
$params = [$user_id];
|
||||
|
||||
$type = sanitize_key((string) ($args['type'] ?? ''));
|
||||
if ($type !== '' && $type !== 'all') {
|
||||
if ($type === 'unread') {
|
||||
$where[] = 'is_read = 0';
|
||||
} else {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$where_sql = implode(' AND ', $where);
|
||||
$sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
return (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知收件箱完整视图(供 page-messages.php 使用)
|
||||
*/
|
||||
public function get_all_notifications(int $user_id): array {
|
||||
return $this->get_notifications($user_id, ['limit' => 200]);
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
if (class_exists('\AKSRT\Core\DBMigrator')) {
|
||||
return \AKSRT\Core\DBMigrator::table('notification');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 标记已读
|
||||
*/
|
||||
public function ajax_mark_read(): void {
|
||||
check_ajax_referer('aksrt_message', '_nonce');
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ($user_id <= 0) {
|
||||
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
|
||||
}
|
||||
|
||||
$message_id = sanitize_text_field((string) ($_POST['message_id'] ?? ''));
|
||||
if ($this->mark_read($user_id, $message_id)) {
|
||||
wp_send_json_success(['unread' => $this->get_unread_count($user_id)]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('操作失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX 获取未读数
|
||||
*/
|
||||
public function ajax_get_unread_count(): void {
|
||||
check_ajax_referer('aksrt_message', '_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)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
<?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)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Messages;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class ReportCenter {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) self::$instance = new self();
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function table(): string {
|
||||
return class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('report') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交举报
|
||||
*
|
||||
* @param int $reporter_id 举报人 ID
|
||||
* @param string $target_type 举报目标类型 (post/comment/user)
|
||||
* @param int $target_id 目标 ID
|
||||
* @param string $reason 举报原因
|
||||
* @param string $category 举报分类
|
||||
* @return int 举报 ID(0 表示失败)
|
||||
*/
|
||||
public function submit(int $reporter_id, string $target_type, int $target_id, string $reason, string $category = ''): int {
|
||||
if ($reporter_id <= 0 || $target_id <= 0) return 0;
|
||||
|
||||
$valid_types = ['post', 'comment', 'user'];
|
||||
if (!in_array($target_type, $valid_types, true)) return 0;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table === '') return 0;
|
||||
|
||||
global $wpdb;
|
||||
$wpdb->insert($table, [
|
||||
'reporter_id' => $reporter_id,
|
||||
'target_type' => sanitize_key($target_type),
|
||||
'target_id' => $target_id,
|
||||
'reason' => sanitize_textarea_field($reason),
|
||||
'category' => sanitize_key($category),
|
||||
'status' => 'pending',
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s']);
|
||||
|
||||
return $wpdb->insert_id ? (int) $wpdb->insert_id : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待处理举报数
|
||||
*/
|
||||
public function get_pending_count(): int {
|
||||
$table = $this->table();
|
||||
if ($table === '') return 0;
|
||||
|
||||
global $wpdb;
|
||||
return (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE status = 'pending'"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取举报列表
|
||||
*/
|
||||
public function get_reports(array $args = []): array {
|
||||
$table = $this->table();
|
||||
if ($table === '') return [];
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||||
|
||||
// 附加审核备注
|
||||
foreach ($rows as &$row) {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['reporter_id'] = (int) $row['reporter_id'];
|
||||
$row['target_id'] = (int) $row['target_id'];
|
||||
$row['notes'] = isset($row['notes_json']) ? json_decode($row['notes_json'], true) ?? [] : [];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新举报状态
|
||||
*/
|
||||
public function update_status(int $report_id, string $new_status, string $note = ''): bool {
|
||||
$valid_statuses = ['pending', 'reviewing', 'resolved', 'dismissed'];
|
||||
if (!in_array($new_status, $valid_statuses, true)) return false;
|
||||
|
||||
$table = $this->table();
|
||||
if ($table === '') return false;
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT id, notes_json FROM {$table} WHERE id = %d",
|
||||
$report_id
|
||||
));
|
||||
|
||||
if (!$existing) return false;
|
||||
|
||||
$update_data = [
|
||||
'status' => $new_status,
|
||||
'updated_at' => current_time('mysql'),
|
||||
];
|
||||
$format = ['%s', '%s'];
|
||||
|
||||
if ($note !== '') {
|
||||
$notes = json_decode($existing->notes_json ?? '[]', true) ?? [];
|
||||
$notes[] = [
|
||||
'note' => sanitize_textarea_field($note),
|
||||
'status' => $new_status,
|
||||
'operator' => get_current_user_id(),
|
||||
'timestamp' => current_time('mysql'),
|
||||
];
|
||||
$update_data['notes_json'] = wp_json_encode($notes);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
$result = $wpdb->update($table, $update_data, ['id' => $report_id], $format, ['%d']);
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status counts for the admin dashboard.
|
||||
*
|
||||
* @return array{pending: int, reviewing: int, resolved: int, dismissed: int, total: int}
|
||||
*/
|
||||
public function get_status_counts(): array {
|
||||
$table = $this->table();
|
||||
if ($table === '') {
|
||||
return ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0];
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$counts = ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0];
|
||||
$total = 0;
|
||||
foreach ((array) $rows as $row) {
|
||||
$s = $row['status'] ?? '';
|
||||
if (isset($counts[$s])) {
|
||||
$counts[$s] = (int) $row['cnt'];
|
||||
}
|
||||
$total += (int) $row['cnt'];
|
||||
}
|
||||
$counts['total'] = $total;
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性迁移:把 option 中的举报数据搬到表中。
|
||||
* 幂等 — 已存在的 ID 不会重复插入。
|
||||
*/
|
||||
public function migrate_from_option(): int {
|
||||
$table = $this->table();
|
||||
if ($table === '') return 0;
|
||||
|
||||
$reports = get_option('aksrt_reports', []);
|
||||
if (!is_array($reports) || empty($reports)) return 0;
|
||||
|
||||
global $wpdb;
|
||||
$migrated = 0;
|
||||
|
||||
foreach ($reports as $old_id => $report) {
|
||||
// 检查是否已迁移(通过 reporter_id + target_type + target_id + created_at 去重)
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE reporter_id = %d AND target_type = %s AND target_id = %d AND created_at = %s",
|
||||
(int) ($report['reporter_id'] ?? 0),
|
||||
sanitize_key((string) ($report['target_type'] ?? '')),
|
||||
(int) ($report['target_id'] ?? 0),
|
||||
(string) ($report['created_at'] ?? '')
|
||||
));
|
||||
if ($existing) continue;
|
||||
|
||||
$notes_json = '';
|
||||
if (!empty($report['notes']) && is_array($report['notes'])) {
|
||||
$notes_json = wp_json_encode($report['notes']);
|
||||
}
|
||||
|
||||
$wpdb->insert($table, [
|
||||
'reporter_id' => (int) ($report['reporter_id'] ?? 0),
|
||||
'target_type' => sanitize_key((string) ($report['target_type'] ?? '')),
|
||||
'target_id' => (int) ($report['target_id'] ?? 0),
|
||||
'reason' => sanitize_textarea_field((string) ($report['reason'] ?? '')),
|
||||
'category' => sanitize_key((string) ($report['category'] ?? '')),
|
||||
'status' => sanitize_key((string) ($report['status'] ?? 'pending')),
|
||||
'created_at' => (string) ($report['created_at'] ?? current_time('mysql')),
|
||||
'updated_at' => (string) ($report['updated_at'] ?? ''),
|
||||
'notes_json' => $notes_json,
|
||||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
|
||||
if ($wpdb->insert_id) $migrated++;
|
||||
}
|
||||
|
||||
if ($migrated > 0) {
|
||||
update_option('aksrt_report_migrated_to_table', time());
|
||||
}
|
||||
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize AJAX hooks for admin operations.
|
||||
*/
|
||||
public function init(): void {
|
||||
add_action('wp_ajax_aksrt_report_update_status', [$this, 'ajax_update_status']);
|
||||
add_action('wp_ajax_aksrt_report_list', [$this, 'ajax_list']);
|
||||
add_action('wp_ajax_aksrt_report_counts', [$this, 'ajax_counts']);
|
||||
|
||||
// 一次性迁移钩子
|
||||
add_action('admin_init', [$this, 'maybe_migrate'], 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动迁移旧 option 数据(幂等)
|
||||
*/
|
||||
public function maybe_migrate(): void {
|
||||
if (get_option('aksrt_report_migrated_to_table')) return;
|
||||
$this->migrate_from_option();
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Update report status (admin only).
|
||||
*/
|
||||
public function ajax_update_status(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
check_ajax_referer('aksrt_report', '_nonce');
|
||||
|
||||
$report_id = (int) ($_POST['report_id'] ?? 0);
|
||||
$new_status = sanitize_key((string) ($_POST['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($_POST['note'] ?? ''));
|
||||
|
||||
if ($report_id <= 0 || $new_status === '') {
|
||||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||||
}
|
||||
|
||||
$result = $this->update_status($report_id, $new_status, $note);
|
||||
if ($result) {
|
||||
wp_send_json_success(['message' => __('状态已更新', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('更新失败', 'aksrt')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get report list (admin only).
|
||||
*/
|
||||
public function ajax_list(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($_GET['status'] ?? ''));
|
||||
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($_GET['offset'] ?? 0));
|
||||
|
||||
$reports = $this->get_reports(['status' => $status, 'limit' => $limit, 'offset' => $offset]);
|
||||
$counts = $this->get_status_counts();
|
||||
|
||||
wp_send_json_success(['reports' => $reports, 'counts' => $counts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get status counts (admin only).
|
||||
*/
|
||||
public function ajax_counts(): void {
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||||
}
|
||||
|
||||
wp_send_json_success($this->get_status_counts());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Messages;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class SiteNotice {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('wp_footer', [$this, 'render_notice']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site notice is enabled, has content, not expired,
|
||||
* and current user matches the target audience (policy).
|
||||
*/
|
||||
private function is_active(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) return false;
|
||||
|
||||
$cfg = \AKSRT\Settings\Settings::get_system_notice_config();
|
||||
if (!$cfg['enabled'] || $cfg['content'] === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 身份策略检查
|
||||
$policy = $cfg['policy'];
|
||||
if ($policy !== '') {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
switch ($policy) {
|
||||
case 'signin':
|
||||
if ($user_id <= 0) return false;
|
||||
break;
|
||||
|
||||
case 'vip':
|
||||
if (!function_exists('aksrt_is_vip') || !aksrt_is_vip($user_id)) return false;
|
||||
break;
|
||||
|
||||
case 'free':
|
||||
if (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) return false;
|
||||
break;
|
||||
|
||||
case 'auth':
|
||||
if ($user_id <= 0) return false;
|
||||
$verified = get_user_meta($user_id, 'email_verified', true);
|
||||
if (!$verified) {
|
||||
$verified = get_user_meta($user_id, 'phone_verified', true);
|
||||
}
|
||||
if (!$verified) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 过期检查:expires 为小时数,0=永不过期
|
||||
$expires_hours = (int) $cfg['expires'];
|
||||
if ($expires_hours > 0 && isset($_COOKIE['aksrt_notice_seen'])) {
|
||||
$seen_ts = absint($_COOKIE['aksrt_notice_seen']);
|
||||
if ($seen_ts > 0 && (time() - $seen_ts) < ($expires_hours * 3600)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the notice popup in footer
|
||||
*/
|
||||
public function render_notice(): void {
|
||||
if (!$this->is_active()) return;
|
||||
|
||||
$cfg = \AKSRT\Settings\Settings::get_system_notice_config();
|
||||
$content = wp_kses_post($cfg['content']);
|
||||
$title = esc_html($cfg['title'] ?: __('站点公告', 'aksrt'));
|
||||
$extra_cls = $cfg['class'] ? ' ' . esc_attr($cfg['class']) : '';
|
||||
$radius = absint($cfg['radius']);
|
||||
$size_cls = $cfg['size'];
|
||||
|
||||
// 标题图标 / 样式
|
||||
$title_icon = $cfg['title_icon'];
|
||||
$title_cls = $cfg['title_class'] ? ' ' . esc_attr($cfg['title_class']) : '';
|
||||
$title_style = $cfg['title_style'] ? ' style="' . esc_attr($cfg['title_style']) . '"' : '';
|
||||
|
||||
// 弹窗尺寸 class
|
||||
$dialog_cls = 'aksrt-site-notice__dialog';
|
||||
if ($size_cls !== '') {
|
||||
$dialog_cls .= ' ' . esc_attr($size_cls);
|
||||
}
|
||||
|
||||
// 内联样式:圆角
|
||||
$dialog_style = 'display:none;';
|
||||
if ($radius > 0) {
|
||||
$dialog_style .= 'border-radius:' . $radius . 'px;';
|
||||
}
|
||||
|
||||
$buttons = $cfg['buttons'];
|
||||
?>
|
||||
<div id="aksrt-site-notice"
|
||||
class="aksrt-site-notice<?php echo $extra_cls; ?>"
|
||||
data-expires="<?php echo esc_attr($cfg['expires']); ?>"
|
||||
style="<?php echo esc_attr($dialog_style); ?>">
|
||||
<div class="aksrt-site-notice__overlay"></div>
|
||||
<div class="<?php echo $dialog_cls; ?>">
|
||||
<div class="aksrt-site-notice__head">
|
||||
<h3 class="aksrt-site-notice__title<?php echo $title_cls; ?>"<?php echo $title_style; ?>>
|
||||
<?php if ($title_icon): ?><i class="<?php echo esc_attr($title_icon); ?>"></i> <?php endif; ?>
|
||||
<?php echo $title; ?>
|
||||
</h3>
|
||||
<button type="button" class="aksrt-site-notice__close" aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="aksrt-site-notice__body">
|
||||
<?php echo $content; ?>
|
||||
</div>
|
||||
<?php if (!empty($buttons)): ?>
|
||||
<div class="aksrt-site-notice__foot">
|
||||
<?php foreach ($buttons as $btn): ?>
|
||||
<?php
|
||||
$btn_cls = 'aksrt-site-notice__btn aksrt-site-notice__btn--' . esc_attr($btn['color']);
|
||||
if ($btn['link']):
|
||||
?>
|
||||
<a href="<?php echo esc_url($btn['link']); ?>" class="<?php echo $btn_cls; ?>"><?php echo esc_html($btn['text']); ?></a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="<?php echo $btn_cls; ?>" data-dismiss="notice"><?php echo esc_html($btn['text']); ?></button>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user