Files
wordpress-theme-aksrt/inc/modules/user-notifications.php
T

593 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 = [];
$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'),
'asset_points_increase' => __('积分增加', 'aksrt'),
'asset_points_decrease' => __('积分扣减', 'aksrt'),
'asset_balance_increase' => __('余额增加', 'aksrt'),
'asset_balance_decrease' => __('余额扣减', '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_asset_transaction_committed', function ($result, $command): void {
if (!$result instanceof \AKSRT\Ledger\LedgerResult || !$command instanceof \AKSRT\Ledger\LedgerCommand) {
return;
}
$changes = [];
foreach ($result->entries as $entry) {
$user_id = (int) ($entry['user_id'] ?? 0);
$asset = (string) ($entry['asset'] ?? '');
if ($user_id <= 0 || !in_array($asset, ['points', 'balance'], true)) continue;
$key = $user_id . ':' . $asset;
$changes[$key] ??= ['user_id' => $user_id, 'asset' => $asset, 'delta' => 0, 'balance' => 0];
$changes[$key]['delta'] += (int) ($entry['delta_minor'] ?? 0);
$changes[$key]['balance'] = (int) ($entry['balance_after_minor'] ?? 0);
}
foreach ($changes as $change) {
if ((int) $change['delta'] === 0) continue;
$is_points = $change['asset'] === 'points';
$positive = $change['delta'] > 0;
$amount = $is_points
? (string) abs((int) $change['delta'])
: \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, abs((int) $change['delta']))->decimal();
$balance = $is_points
? (string) $change['balance']
: \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, (int) $change['balance'])->decimal();
aksrt_add_user_notification((int) $change['user_id'], [
'type' => 'asset_' . $change['asset'] . '_' . ($positive ? 'increase' : 'decrease'),
'title' => $is_points ? __('积分变动', 'aksrt') : __('余额变动', 'aksrt'),
'desc' => sprintf(__('%1$s:%2$s%3$s,当前余额 %4$s。', 'aksrt'), \AKSRT\Ledger\LedgerPresentation::reason_label($command->reason), $positive ? '+' : '-', $amount, $balance),
'url' => aksrt_get_user_center_url('credits'),
]);
}
}, 10, 2);