*/ function aksrt_get_user_notifications(int $user_id = 0): array { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) { return []; } $items = []; $email_verified = true; if (class_exists('\AKSRT\Auth\Service')) { $email_verified = \AKSRT\Auth\Service::user_is_email_verified($user_id); } if (!$email_verified && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('verify_email'))) { $items[] = [ 'type' => 'verify_email', 'title' => __('邮箱待验证', 'aksrt'), 'desc' => __('请尽快完成邮箱验证,部分功能需要验证后才能使用。', 'aksrt'), 'url' => function_exists('aksrt_get_verify_email_url') ? aksrt_get_verify_email_url($user_id) : home_url('/'), 'count' => 1, ]; } $pending_comments = (int) get_comments([ 'user_id' => $user_id, 'count' => true, 'status' => 'hold', ]); if ($pending_comments > 0 && (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::notification_enabled('pending_comments'))) { $items[] = [ 'type' => 'pending_comments', 'title' => __('评论待审核', 'aksrt'), 'desc' => sprintf(__('你有 %d 条评论正在等待审核。', 'aksrt'), $pending_comments), 'url' => aksrt_get_user_center_url('comments'), 'count' => $pending_comments, ]; } return $items; } endif; if (!function_exists('aksrt_notification_type_label')) : /** 通知类型中文标签 */ function aksrt_notification_type_label(string $type): string { $map = [ 'comment_reply' => __('评论回复', 'aksrt'), 'comment_approved' => __('评论审核', 'aksrt'), 'download_ready' => __('下载就绪', 'aksrt'), 'credits_added' => __('积分增加', 'aksrt'), 'credits_deducted' => __('积分扣减', 'aksrt'), 'verify_email' => __('邮箱验证', 'aksrt'), 'pending_comments' => __('待审核评论', 'aksrt'), 'notice' => __('通知', 'aksrt'), ]; return $map[$type] ?? $type; } endif; if (!function_exists('aksrt_get_user_notification_count')) : /** 获取用户提醒总数 */ function aksrt_get_user_notification_count(int $user_id = 0): int { $items = aksrt_get_user_notifications($user_id); $count = 0; foreach ($items as $item) { $count += max(0, (int) ($item['count'] ?? 0)); } return $count; } endif; if (!function_exists('aksrt_get_user_inbox_notifications')) : /** 获取用户通知收件箱 */ function aksrt_get_user_inbox_notifications(int $user_id = 0): array { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) return []; global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { $items = get_user_meta($user_id, 'aksrt_notifications', true); return is_array($items) ? array_values($items) : []; } static $migrated = []; if (empty($migrated[$user_id])) { $legacy = get_user_meta($user_id, 'aksrt_notifications', true); if (is_array($legacy) && !empty($legacy)) { foreach ($legacy as $item) { aksrt_add_user_notification($user_id, [ 'id' => $item['id'] ?? '', 'type' => $item['type'] ?? 'notice', 'title' => $item['title'] ?? '', 'desc' => $item['desc'] ?? '', 'url' => $item['url'] ?? '', 'read' => !empty($item['read']), 'created_at' => $item['created_at'] ?? current_time('mysql', true), ]); } delete_user_meta($user_id, 'aksrt_notifications'); } $migrated[$user_id] = true; } $rows = $wpdb->get_results($wpdb->prepare( "SELECT id, type, title, description, target_url, is_read, created_at, read_at FROM {$table} WHERE user_id = %d ORDER BY created_at DESC, id DESC LIMIT 50", $user_id ), ARRAY_A); if (!is_array($rows)) return []; return array_map(static function ($row) { return [ 'id' => (string) ($row['id'] ?? ''), 'type' => (string) ($row['type'] ?? 'notice'), 'title' => (string) ($row['title'] ?? ''), 'desc' => (string) ($row['description'] ?? ''), 'url' => (string) ($row['target_url'] ?? ''), 'read' => !empty($row['is_read']) ? 1 : 0, 'created_at' => (string) ($row['created_at'] ?? ''), 'read_at' => (string) ($row['read_at'] ?? ''), ]; }, $rows); } endif; if (!function_exists('aksrt_schedule_notification_cleanup')) : /** 注册通知清理定时任务 */ function aksrt_schedule_notification_cleanup(): void { if (!wp_next_scheduled('aksrt_notification_cleanup')) { $next = (new DateTimeImmutable('tomorrow 04:10', wp_timezone()))->getTimestamp(); wp_schedule_event($next, 'daily', 'aksrt_notification_cleanup'); } } endif; if (!function_exists('aksrt_cleanup_notifications')) : /** * 清理通知表: * - 已读通知保留 30 天 * - 未读通知保留 180 天 * - 每个用户最多保留最近 200 条通知 */ function aksrt_cleanup_notifications(): void { global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { return; } $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_notification_settings() : [ 'read_retention_days' => 30, 'unread_retention_days' => 180, 'max_per_user' => 200, ]; $read_retention_days = max(1, (int) ($settings['read_retention_days'] ?? 30)); $unread_retention_days = max(7, (int) ($settings['unread_retention_days'] ?? 180)); $max_per_user = max(20, (int) ($settings['max_per_user'] ?? 200)); $read_cutoff = (new DateTimeImmutable('-' . $read_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $unread_cutoff = (new DateTimeImmutable('-' . $unread_retention_days . ' days', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE is_read = 1 AND created_at < %s", $read_cutoff )); $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE is_read = 0 AND created_at < %s", $unread_cutoff )); $user_ids = $wpdb->get_col("SELECT DISTINCT user_id FROM {$table}"); if (empty($user_ids)) { return; } foreach ($user_ids as $user_id) { $user_id = (int) $user_id; if ($user_id <= 0) continue; $ids = $wpdb->get_col($wpdb->prepare( "SELECT id FROM {$table} WHERE user_id = %d ORDER BY created_at DESC, id DESC LIMIT %d, 1000", $user_id, $max_per_user )); if (empty($ids)) { continue; } $ids = array_map('absint', $ids); $ids = array_filter($ids, static function ($id) { return $id > 0; }); if (empty($ids)) { continue; } $wpdb->query("DELETE FROM {$table} WHERE id IN (" . implode(',', $ids) . ")"); } } endif; add_action('wp', 'aksrt_schedule_notification_cleanup'); add_action('aksrt_notification_cleanup', 'aksrt_cleanup_notifications'); if (!function_exists('aksrt_write_user_inbox_notifications')) : /** 写入用户通知收件箱 */ function aksrt_write_user_inbox_notifications(int $user_id, array $items): void { if ($user_id <= 0) return; global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { update_user_meta($user_id, 'aksrt_notifications', array_values(array_slice($items, 0, 50))); return; } $wpdb->delete($table, ['user_id' => $user_id], ['%d']); $items = array_values(array_slice($items, 0, 50)); foreach ($items as $item) { $wpdb->insert($table, [ 'user_id' => $user_id, 'type' => sanitize_key((string) ($item['type'] ?? 'notice')), 'title' => sanitize_text_field((string) ($item['title'] ?? '')), 'description' => sanitize_text_field((string) ($item['desc'] ?? '')), 'target_url' => esc_url_raw((string) ($item['url'] ?? '')), 'is_read' => !empty($item['read']) ? 1 : 0, 'created_at' => (string) ($item['created_at'] ?? current_time('mysql', true)), 'read_at' => !empty($item['read']) ? current_time('mysql', true) : null, ], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']); } } endif; if (!function_exists('aksrt_add_user_notification')) : /** 添加一条用户通知 */ function aksrt_add_user_notification(int $user_id, array $payload): void { if ($user_id <= 0) return; $type = sanitize_key((string) ($payload['type'] ?? 'notice')); if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_enabled($type)) { return; } global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; $created_at = (string) ($payload['created_at'] ?? current_time('mysql', true)); if ($table === '') { $items = aksrt_get_user_inbox_notifications($user_id); array_unshift($items, [ 'id' => isset($payload['id']) && $payload['id'] !== '' ? sanitize_text_field((string) $payload['id']) : uniqid('notif_', true), 'type' => $type, 'title' => sanitize_text_field((string) ($payload['title'] ?? '')), 'desc' => sanitize_text_field((string) ($payload['desc'] ?? '')), 'url' => esc_url_raw((string) ($payload['url'] ?? '')), 'read' => !empty($payload['read']) ? 1 : 0, 'created_at' => $created_at, ]); aksrt_write_user_inbox_notifications($user_id, $items); return; } $wpdb->insert($table, [ 'user_id' => $user_id, 'type' => $type, 'title' => sanitize_text_field((string) ($payload['title'] ?? '')), 'description' => sanitize_text_field((string) ($payload['desc'] ?? '')), 'target_url' => esc_url_raw((string) ($payload['url'] ?? '')), 'is_read' => !empty($payload['read']) ? 1 : 0, 'created_at' => $created_at, 'read_at' => !empty($payload['read']) ? current_time('mysql', true) : null, ], ['%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s']); } endif; if (!function_exists('aksrt_mark_user_notification_read')) : /** 标记通知已读 */ function aksrt_mark_user_notification_read(int $user_id, string $notification_id = '', bool $mark_all = false): void { if ($user_id <= 0) return; global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { $items = aksrt_get_user_inbox_notifications($user_id); foreach ($items as &$item) { if ($mark_all || (($item['id'] ?? '') === $notification_id)) { $item['read'] = 1; } } unset($item); aksrt_write_user_inbox_notifications($user_id, $items); return; } if ($mark_all) { $wpdb->update( $table, ['is_read' => 1, 'read_at' => current_time('mysql', true)], ['user_id' => $user_id, 'is_read' => 0], ['%d', '%s'], ['%d', '%d'] ); return; } $wpdb->update( $table, ['is_read' => 1, 'read_at' => current_time('mysql', true)], ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%s'], ['%d', '%d'] ); } endif; if (!function_exists('aksrt_delete_user_notification')) : /** 删除一条用户通知 */ function aksrt_delete_user_notification(int $user_id, string $notification_id): void { if ($user_id <= 0 || $notification_id === '') return; global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { $items = aksrt_get_user_inbox_notifications($user_id); $items = array_values(array_filter($items, static function ($item) use ($notification_id) { return (string) ($item['id'] ?? '') !== $notification_id; })); aksrt_write_user_inbox_notifications($user_id, $items); return; } $wpdb->delete($table, ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%d']); } endif; if (!function_exists('aksrt_delete_user_notifications')) : /** 批量删除用户通知 */ function aksrt_delete_user_notifications(int $user_id, array $notification_ids): void { if ($user_id <= 0 || empty($notification_ids)) return; $ids = array_values(array_filter(array_map('absint', $notification_ids), static function ($id) { return $id > 0; })); if (empty($ids)) return; global $wpdb; $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; if ($table === '') { $items = aksrt_get_user_inbox_notifications($user_id); $items = array_values(array_filter($items, static function ($item) use ($ids) { return !in_array(absint($item['id'] ?? 0), $ids, true); })); aksrt_write_user_inbox_notifications($user_id, $items); return; } $wpdb->query( "DELETE FROM {$table} WHERE user_id = " . (int) $user_id . " AND id IN (" . implode(',', $ids) . ")" ); } endif; if (!function_exists('aksrt_get_user_unread_notification_count')) : /** 获取用户未读通知数 */ function aksrt_get_user_unread_notification_count(int $user_id = 0): int { $items = aksrt_get_user_inbox_notifications($user_id); $count = 0; foreach ($items as $item) { if (empty($item['read'])) $count++; } return $count; } endif; if (!function_exists('aksrt_get_user_tab_badge_count')) : /** user 底部 Tab 徽标计数:未读持久化通知 + 即时状态提醒 */ function aksrt_get_user_tab_badge_count(int $user_id = 0): int { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) { return 0; } if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::notification_user_badge_enabled()) { return 0; } $count = aksrt_get_user_unread_notification_count($user_id); if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::notification_badge_include_stateful()) { $count += aksrt_get_user_notification_count($user_id); } return $count; } endif; if (!function_exists('aksrt_get_user_all_notifications')) : /** 获取用户完整通知列表(持久化通知 + 即时提醒) */ function aksrt_get_user_all_notifications(int $user_id = 0): array { $user_id = $user_id > 0 ? $user_id : get_current_user_id(); if ($user_id <= 0) return []; $inbox = aksrt_get_user_inbox_notifications($user_id); $alerts = aksrt_get_user_notifications($user_id); $normalized_alerts = []; foreach ($alerts as $alert) { $normalized_alerts[] = [ 'id' => 'state_' . sanitize_key((string) ($alert['type'] ?? 'notice')), 'type' => sanitize_key((string) ($alert['type'] ?? 'notice')), 'title' => sanitize_text_field((string) ($alert['title'] ?? '')), 'desc' => sanitize_text_field((string) ($alert['desc'] ?? '')), 'url' => esc_url_raw((string) ($alert['url'] ?? '')), 'read' => 0, 'created_at' => current_time('mysql', true), 'count' => max(0, (int) ($alert['count'] ?? 0)), 'stateful' => 1, ]; } return array_merge($inbox, $normalized_alerts); } endif; add_action('wp_ajax_aksrt_mark_notification_read', function () { if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } $nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : ''; if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) { wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); } $notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? '')); $mark_all = !empty($_POST['mark_all']); aksrt_mark_user_notification_read(get_current_user_id(), $notification_id, $mark_all); wp_send_json_success([ 'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()), ]); }); add_action('wp_ajax_aksrt_delete_notification', function () { if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } $nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : ''; if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) { wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); } $notification_id = sanitize_text_field((string) ($_POST['notification_id'] ?? '')); if ($notification_id === '') { wp_send_json_error(['message' => __('参数错误', 'aksrt')], 400); } aksrt_delete_user_notification(get_current_user_id(), $notification_id); wp_send_json_success([ 'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()), ]); }); add_action('wp_ajax_aksrt_delete_notifications', function () { if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } $nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : ''; if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) { wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); } $ids = isset($_POST['notification_ids']) ? (array) $_POST['notification_ids'] : []; aksrt_delete_user_notifications(get_current_user_id(), $ids); wp_send_json_success([ 'unread' => aksrt_get_user_unread_notification_count(get_current_user_id()), ]); }); add_action('wp_ajax_aksrt_check_new_notifications', function () { if (!is_user_logged_in()) { wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401); } $nonce = isset($_POST['nonce']) ? (string) $_POST['nonce'] : ''; if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) { wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); } $unread = aksrt_get_user_unread_notification_count(get_current_user_id()); wp_send_json_success([ 'count' => $unread, 'unread' => $unread, ]); }); add_action('wp_insert_comment', function ($comment_id, $comment) { if (!$comment instanceof WP_Comment) return; if ((int) $comment->comment_parent <= 0) return; if ((int) $comment->comment_approved !== 1) return; if (get_comment_meta($comment_id, 'aksrt_reply_notified', true)) return; $parent = get_comment((int) $comment->comment_parent); if (!$parent instanceof WP_Comment) return; $target_user_id = (int) $parent->user_id; if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return; $post_id = (int) $comment->comment_post_ID; $url = get_comment_link($comment); aksrt_add_user_notification($target_user_id, [ 'type' => 'comment_reply', 'title' => __('评论收到回复', 'aksrt'), 'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)), 'url' => $url ?: get_permalink($post_id), ]); update_comment_meta($comment_id, 'aksrt_reply_notified', 1); }, 20, 2); add_action('transition_comment_status', function ($new_status, $old_status, $comment) { if (!$comment instanceof WP_Comment) return; if ($new_status !== 'approve' || $old_status === 'approve') return; $comment_owner_id = (int) $comment->user_id; if ($comment_owner_id > 0 && !get_comment_meta($comment->comment_ID, 'aksrt_approved_notified', true)) { aksrt_add_user_notification($comment_owner_id, [ 'type' => 'comment_approved', 'title' => __('评论已通过审核', 'aksrt'), 'desc' => __('你的评论已通过审核,现已公开展示。', 'aksrt'), 'url' => get_comment_link($comment), ]); update_comment_meta($comment->comment_ID, 'aksrt_approved_notified', 1); } if ((int) $comment->comment_parent <= 0) return; if (get_comment_meta($comment->comment_ID, 'aksrt_reply_notified', true)) return; $parent = get_comment((int) $comment->comment_parent); if (!$parent instanceof WP_Comment) return; $target_user_id = (int) $parent->user_id; if ($target_user_id <= 0 || $target_user_id === (int) $comment->user_id) return; aksrt_add_user_notification($target_user_id, [ 'type' => 'comment_reply', 'title' => __('评论收到回复', 'aksrt'), 'desc' => sprintf(__('“%s” 回复了你的评论。', 'aksrt'), get_comment_author($comment)), 'url' => get_comment_link($comment), ]); update_comment_meta($comment->comment_ID, 'aksrt_reply_notified', 1); }, 20, 3); add_action('aksrt_credits_added', function ($user_id, $delta, $reason, $new_balance) { if ($user_id <= 0 || $delta <= 0) return; aksrt_add_user_notification((int) $user_id, [ 'type' => 'credits_added', 'title' => __('积分增加', 'aksrt'), 'desc' => sprintf(__('积分变动:+%1$d,当前余额 %2$d。', 'aksrt'), (int) $delta, (int) $new_balance), 'url' => aksrt_get_user_center_url('credits'), ]); }, 10, 4); add_action('aksrt_credits_deducted', function ($user_id, $cost, $reason, $new_balance) { if ($user_id <= 0 || $cost <= 0) return; aksrt_add_user_notification((int) $user_id, [ 'type' => 'credits_deducted', 'title' => __('积分扣减', 'aksrt'), 'desc' => sprintf(__('积分变动:-%1$d,当前余额 %2$d。', 'aksrt'), (int) $cost, (int) $new_balance), 'url' => aksrt_get_user_center_url('credits'), ]); }, 10, 4);