*/ 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 = []; $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')) && (!class_exists('\AKSRT\User\NotificationPreferences') || \AKSRT\User\NotificationPreferences::allows($user_id, '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'), 'comment' => __('文章评论', 'aksrt'), 'download_ready' => __('下载就绪', 'aksrt'), 'post_favorite' => __('文章收藏', 'aksrt'), 'post_like' => __('文章点赞', 'aksrt'), 'comment_like' => __('评论点赞', 'aksrt'), 'followed' => __('新增关注', '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 === '') return []; $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_notification_settings() : []; $limit = max(20, min(5000, (int) ($settings['max_per_user'] ?? 200))); $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 %d", $user_id, $limit ), 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_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\User\NotificationPreferences') && !\AKSRT\User\NotificationPreferences::allows($user_id, $type)) { return; } 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 === '') return; $inserted = $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']); if ($inserted !== false) { do_action('aksrt_notification_sent', $user_id, $type, [ 'title' => (string) ($payload['title'] ?? ''), 'description' => (string) ($payload['desc'] ?? ''), 'target_url' => (string) ($payload['url'] ?? ''), ]); } } 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 === '') 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 === '') 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 === '') 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']) && !str_starts_with((string) ($item['type'] ?? ''), 'asset_')) $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); } $counts = function_exists('aksrt_get_message_center_counts') ? aksrt_get_message_center_counts(get_current_user_id()) : []; $unread = isset($counts['unread']) ? (int) $counts['unread'] : aksrt_get_user_unread_notification_count(get_current_user_id()); wp_send_json_success([ 'count' => $unread, 'unread' => $unread, ]); }); function aksrt_notify_post_author_of_comment(WP_Comment $comment): void { if ((int) $comment->comment_parent > 0 || get_comment_meta($comment->comment_ID, 'aksrt_post_comment_notified', true)) return; $post = get_post((int) $comment->comment_post_ID); $owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0; if ($owner_id <= 0 || $owner_id === (int) $comment->user_id) return; aksrt_add_user_notification($owner_id, [ 'type' => 'comment', 'title' => __('文章收到评论', 'aksrt'), 'desc' => sprintf(__('%1$s 评论了你的文章《%2$s》。', 'aksrt'), get_comment_author($comment), get_the_title($post)), 'url' => get_comment_link($comment), ]); update_comment_meta($comment->comment_ID, 'aksrt_post_comment_notified', 1); } add_action('wp_insert_comment', function ($comment_id, $comment) { if (!$comment instanceof WP_Comment) return; if ((int) $comment->comment_approved !== 1) return; aksrt_notify_post_author_of_comment($comment); if ((int) $comment->comment_parent <= 0) 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); } aksrt_notify_post_author_of_comment($comment); 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_post_favorited', function (int $post_id, int $actor_id, bool $active): void { $post = $active ? get_post($post_id) : null; $owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0; if ($owner_id <= 0 || $owner_id === $actor_id) return; $actor = get_userdata($actor_id); aksrt_add_user_notification($owner_id, [ 'type' => 'post_favorite', 'title' => __('文章被收藏', 'aksrt'), 'desc' => sprintf(__('%1$s 收藏了你的文章《%2$s》。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt'), get_the_title($post)), 'url' => get_permalink($post), ]); }, 30, 3); add_action('aksrt_post_liked', function (int $post_id, int $actor_id, bool $active): void { $post = $active ? get_post($post_id) : null; $owner_id = $post instanceof WP_Post ? (int) $post->post_author : 0; if ($owner_id <= 0 || $owner_id === $actor_id) return; $actor = get_userdata($actor_id); aksrt_add_user_notification($owner_id, [ 'type' => 'post_like', 'title' => __('文章收到点赞', 'aksrt'), 'desc' => sprintf(__('%1$s 点赞了你的文章《%2$s》。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt'), get_the_title($post)), 'url' => get_permalink($post), ]); }, 30, 3); add_action('aksrt_comment_liked', function (int $comment_id, int $actor_id, bool $active): void { $comment = $active ? get_comment($comment_id) : null; $owner_id = $comment instanceof WP_Comment ? (int) $comment->user_id : 0; if ($owner_id <= 0 || $owner_id === $actor_id) return; $actor = get_userdata($actor_id); aksrt_add_user_notification($owner_id, [ 'type' => 'comment_like', 'title' => __('评论收到点赞', 'aksrt'), 'desc' => sprintf(__('%s 点赞了你的评论。', 'aksrt'), $actor instanceof WP_User ? $actor->display_name : __('一位用户', 'aksrt')), 'url' => get_comment_link($comment), ]); }, 30, 3); add_action('aksrt_user_followed', function (int $follower_id, int $following_id): void { if ($following_id <= 0 || $following_id === $follower_id) return; $follower = get_userdata($follower_id); aksrt_add_user_notification($following_id, [ 'type' => 'followed', 'title' => __('收到新的关注', 'aksrt'), 'desc' => sprintf(__('%s 关注了你。', 'aksrt'), $follower instanceof WP_User ? $follower->display_name : __('一位用户', 'aksrt')), 'url' => function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($follower_id) : '', ]); }, 30, 2);