Files
wordpress-theme-aksrt/inc/modules/user-notifications.php
T
kate522 aaf458af1c refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
2026-07-17 13:29:22 +08:00

596 lines
23 KiB
PHP

<?php
/**
* AKSRT User Notifications
*
* Notification CRUD, AJAX handlers, and event-driven notification creators.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_user_notifications')) :
/**
* 获取用户提醒列表(即时提醒,不持久化)
*
* 当前通知源:
* - 邮箱待验证
* - 评论待审核
*
* @param int $user_id 用户 ID,默认当前用户
* @return array<int, array{type:string,title:string,desc:string,url:string,count:int}>
*/
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);