diff --git a/.gitignore b/.gitignore index a26257fc..dbb8672e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ !/tests/widget-system/user-badges-contract.test.cjs !/tests/widget-system/user-features-contract.test.cjs !/tests/widget-system/notification-preferences-contract.test.cjs +!/tests/widget-system/message-center-contract.test.cjs !/tests/widget-system/asset-ledger-contract.test.cjs !/tests/widget-system/financial-flow-contract.test.cjs !/tests/widget-system/pay-orders-filter-contract.test.cjs diff --git a/assets/admin/src/pages/content/NotificationCenterSettings.vue b/assets/admin/src/pages/content/NotificationCenterSettings.vue index 9693cad2..510dd461 100644 --- a/assets/admin/src/pages/content/NotificationCenterSettings.vue +++ b/assets/admin/src/pages/content/NotificationCenterSettings.vue @@ -21,18 +21,6 @@ - - - - - - - - - - - - @@ -96,10 +84,6 @@ const settings = reactive({ comment_reply: true, comment_approved: true, download_ready: true, - asset_points_increase: true, - asset_points_decrease: true, - asset_balance_increase: true, - asset_balance_decrease: true, verify_email: true, pending_comments: true, upload_moderation: true, diff --git a/assets/admin/src/pages/users/UsersMessages.vue b/assets/admin/src/pages/users/UsersMessages.vue index 02728490..6b25a2b2 100644 --- a/assets/admin/src/pages/users/UsersMessages.vue +++ b/assets/admin/src/pages/users/UsersMessages.vue @@ -163,10 +163,6 @@ function getTypeTag(type) { system: 'info', comment_reply: 'success', comment_approved: 'success', - asset_points_increase: 'warning', - asset_points_decrease: 'warning', - asset_balance_increase: 'success', - asset_balance_decrease: 'error', download_ready: 'primary', verify_email: 'info', pending_comments: 'warning', @@ -180,10 +176,6 @@ function getTypeLabel(type) { system: '系统通知', comment_reply: '评论回复', comment_approved: '评论审核', - asset_points_increase: '积分增加', - asset_points_decrease: '积分扣减', - asset_balance_increase: '余额增加', - asset_balance_decrease: '余额扣减', download_ready: '下载就绪', verify_email: '邮箱验证', pending_comments: '待审评论', diff --git a/assets/css/frontend/messages.css b/assets/css/frontend/messages.css index 27fc7eac..0113e0d5 100644 --- a/assets/css/frontend/messages.css +++ b/assets/css/frontend/messages.css @@ -101,6 +101,13 @@ /* ---- 右侧内容 ---- */ .messages-center__content { min-width: 0; + outline: none; + transition: opacity 0.15s ease; +} + +.messages-center.is-loading .messages-center__content { + opacity: 0.55; + pointer-events: none; } .messages-list { @@ -166,6 +173,20 @@ gap: 8px; } +button.message-item__title { + width: 100%; + padding: 0; + border: 0; + background: transparent; + font-family: inherit; + text-align: left; + cursor: pointer; +} + +button.message-item__title:hover { + color: var(--color-primary, #3b82f6); +} + .message-item__new-badge { display: inline-block; padding: 1px 6px; @@ -177,6 +198,94 @@ line-height: 1.4; } +/* ---- 消息详情(右侧 DOM 视图,不改变地址) ---- */ +.message-detail { + min-height: 360px; + padding: 28px 32px; + background: var(--surface-card, #fff); + border-radius: 12px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.message-detail__header { + position: relative; + padding: 48px 0 22px; + border-bottom: 1px solid var(--color-line); + text-align: center; +} + +.message-detail__back { + position: absolute; + top: 0; + left: 0; + padding: 7px 10px; + border: 0; + border-radius: 6px; + background: var(--surface-hover, rgba(0, 0, 0, 0.04)); + color: var(--color-ink-3, #666); + cursor: pointer; +} + +.message-detail__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + margin-bottom: 14px; + border-radius: 50%; + background: var(--color-primary-light, rgba(59, 130, 246, 0.1)); + color: var(--color-primary, #3b82f6); + font-size: 20px; +} + +.message-detail__title { + margin: 0 0 8px; + color: var(--color-ink); + font-size: 22px; +} + +.message-detail__meta { + margin: 0; + color: var(--color-ink-4, #999); + font-size: 12px; +} + +.message-detail__content { + min-height: 140px; + padding: 28px 4px; + color: var(--color-ink-2, #444); + font-size: 15px; + line-height: 1.9; + overflow-wrap: anywhere; +} + +.message-detail__content p { + margin: 0 0 1em; +} + +.message-detail__actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding-top: 18px; + border-top: 1px solid var(--color-line); +} + +.message-detail__primary { + padding: 8px 16px; + border-radius: 6px; + background: var(--color-primary, #3b82f6); + color: #fff; + text-decoration: none; +} + +html.dark .message-detail { + background: var(--surface-1); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + .message-item__desc { font-size: 13px; color: var(--color-ink-3, #666); @@ -301,6 +410,25 @@ flex-direction: column; gap: 4px; } + + .message-detail { + min-height: 300px; + padding: 20px 16px; + } + + .message-detail__title { + font-size: 18px; + } + + .message-detail__actions { + align-items: stretch; + flex-direction: column; + } + + .message-detail__primary, + .message-detail__actions .message-item__action-btn { + text-align: center; + } } /* ---- 暗色模式 ---- */ diff --git a/assets/js/messages.js b/assets/js/messages.js index 28cf4880..38dc7f1f 100644 --- a/assets/js/messages.js +++ b/assets/js/messages.js @@ -1,175 +1,172 @@ /** - * AKSRT messages center interactions. + * AKSRT message center DOM navigation. * * @package AKSRT */ (function () { 'use strict'; + var root = document.querySelector('[data-message-center]'); + if (!root) return; + var nonce = typeof aksrtData !== 'undefined' ? aksrtData.nonce : ''; var ajaxUrl = typeof aksrtData !== 'undefined' ? aksrtData.ajaxUrl : '/wp-admin/admin-ajax.php'; - var pollTimer = null; + var content = root.querySelector('[data-message-content]'); + var nav = root.querySelector('[data-message-nav]'); + var state = { view: root.dataset.view || 'unread', page: 1, detail: 0 }; var pollInterval = 60000; function post(action, data) { var body = new URLSearchParams({ action: action, nonce: nonce }); - if (data) { - Object.keys(data).forEach(function (key) { - var value = data[key]; - if (Array.isArray(value)) { - value.forEach(function (item) { - body.append(key + '[]', item); - }); - return; - } + Object.keys(data || {}).forEach(function (key) { + var value = data[key]; + if (Array.isArray(value)) { + value.forEach(function (item) { body.append(key + '[]', item); }); + } else { body.append(key, value); - }); - } - + } + }); return fetch(ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' }) .then(function (response) { return response.json(); }); } - document.addEventListener('click', function (event) { - var item = event.target.closest('.messages-nav__item[data-type]'); - if (!item) return; + function routeFor(view) { + var link = nav.querySelector('[data-message-view="' + view + '"]'); + return link ? link.href : window.location.href; + } + function updateHeaderBadge(unread) { + var count = Math.max(0, Number(unread) || 0); + var headerBadge = document.querySelector('.aksrt-notification-badge'); + var avatarDot = document.querySelector('.nav-user__dot'); + if (headerBadge) { + headerBadge.textContent = String(count); + headerBadge.style.display = count > 0 ? '' : 'none'; + } + if (avatarDot) avatarDot.style.display = count > 0 ? '' : 'none'; + } + + function renderError(message) { + content.innerHTML = '

'; + content.querySelector('.messages-empty__text').textContent = message || '加载失败,请稍后重试'; + } + + function load(next, options) { + var requested = { + view: next.view || state.view, + page: next.page || 1, + detail: next.detail || 0, + }; + var settings = options || {}; + root.classList.add('is-loading'); + content.setAttribute('aria-busy', 'true'); + + return post('aksrt_load_message_center', { + view: requested.view, + page: requested.page, + notification_id: requested.detail, + }).then(function (result) { + if (!result.success || !result.data) { + throw new Error(result.data && result.data.message ? result.data.message : '加载失败,请稍后重试'); + } + state = requested; + root.dataset.view = state.view; + nav.innerHTML = result.data.nav; + content.innerHTML = result.data.content; + updateHeaderBadge(result.data.unread); + + if (settings.history === 'push') { + window.history.pushState(state, '', routeFor(state.view)); + } else if (settings.history === 'replace') { + window.history.replaceState(state, '', routeFor(state.view)); + } + content.focus({ preventScroll: true }); + }).catch(function (error) { + renderError(error.message); + }).finally(function () { + root.classList.remove('is-loading'); + content.removeAttribute('aria-busy'); + }); + } + + function reloadList() { + return load({ view: state.view, page: state.page, detail: 0 }); + } + + document.addEventListener('click', function (event) { + var tab = event.target.closest('[data-message-view]'); + if (!tab || !root.contains(tab) || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); - var url = new URL(window.location.href); - url.searchParams.set('msg_type', item.dataset.type); - url.searchParams.delete('msg_page'); - window.location.href = url.toString(); + load({ view: tab.dataset.messageView, page: 1, detail: 0 }, { history: 'push' }); + }); + + document.addEventListener('click', function (event) { + var button = event.target.closest('.js-msg-detail'); + if (!button || !root.contains(button)) return; + event.preventDefault(); + load({ view: button.dataset.view || state.view, page: state.page, detail: button.dataset.id }); + }); + + document.addEventListener('click', function (event) { + var button = event.target.closest('[data-message-back]'); + if (!button || !root.contains(button)) return; + load({ view: button.dataset.view || state.view, page: state.page, detail: 0 }); + }); + + document.addEventListener('click', function (event) { + var button = event.target.closest('[data-message-page]'); + if (!button || !root.contains(button)) return; + load({ view: state.view, page: button.dataset.messagePage, detail: 0 }); }); document.addEventListener('click', function (event) { var button = event.target.closest('.js-msg-mark-read'); - if (!button) return; - - var id = button.dataset.id; - var msgItem = button.closest('.message-item'); - post('aksrt_mark_notification_read', { notification_id: id }).then(function (result) { - if (!result.success) return; - - if (msgItem) { - msgItem.classList.remove('is-unread'); - } - button.remove(); - updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread()); + if (!button || !root.contains(button)) return; + post('aksrt_mark_notification_read', { notification_id: button.dataset.id }).then(function (result) { + if (result.success) reloadList(); }); }); document.addEventListener('click', function (event) { var button = event.target.closest('.js-msg-delete'); - if (!button) return; - - var id = button.dataset.id; - var msgItem = button.closest('.message-item'); - post('aksrt_delete_notification', { notification_id: id }).then(function (result) { - if (!result.success) return; - - if (msgItem) { - msgItem.style.opacity = '0'; - msgItem.style.transform = 'translateX(20px)'; - msgItem.style.transition = 'all 0.2s'; - setTimeout(function () { msgItem.remove(); }, 200); - } - - updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread()); - checkEmpty(); + if (!button || !root.contains(button)) return; + post('aksrt_delete_notification', { notification_id: button.dataset.id }).then(function (result) { + if (result.success) reloadList(); }); }); document.addEventListener('click', function (event) { var button = event.target.closest('.js-msg-mark-all-read'); - if (!button) return; - + if (!button || !root.contains(button)) return; post('aksrt_mark_notification_read', { mark_all: '1' }).then(function (result) { - if (!result.success) return; - - document.querySelectorAll('.message-item.is-unread').forEach(function (item) { - item.classList.remove('is-unread'); - }); - document.querySelectorAll('.js-msg-mark-read').forEach(function (item) { - item.remove(); - }); - updateBadges(0); + if (result.success) reloadList(); }); }); document.addEventListener('click', function (event) { var button = event.target.closest('.js-msg-clear-read'); - if (!button) return; - - var readItems = document.querySelectorAll('.message-item:not(.is-unread)'); + if (!button || !root.contains(button)) return; var ids = []; - readItems.forEach(function (item) { - var id = item.dataset.notificationId; - if (id) ids.push(id); + root.querySelectorAll('.message-item:not(.is-unread)[data-notification-id]').forEach(function (item) { + if (/^\d+$/.test(item.dataset.notificationId)) ids.push(item.dataset.notificationId); }); if (!ids.length) return; - post('aksrt_delete_notifications', { notification_ids: ids }).then(function (result) { - if (!result.success) return; - - readItems.forEach(function (item) { - item.style.opacity = '0'; - item.style.transform = 'translateX(20px)'; - item.style.transition = 'all 0.2s'; - setTimeout(function () { item.remove(); }, 200); - }); - setTimeout(checkEmpty, 250); - updateBadges(result.data && result.data.unread !== undefined ? result.data.unread : countUnread()); + if (result.success) reloadList(); }); }); - function countUnread() { - return document.querySelectorAll('.message-item.is-unread').length; - } - - function updateBadges(unread) { - var nextUnread = unread === undefined ? countUnread() : unread; - - document.querySelectorAll('.messages-nav__badge[data-type="unread"]').forEach(function (badge) { - badge.textContent = String(nextUnread); - badge.dataset.count = String(nextUnread); - badge.style.display = nextUnread > 0 ? '' : 'none'; - }); - - // Numeric badge inside the user dropdown only — never target the pure avatar - // .nav-user__dot.aksrt-notification-badge (no textContent on a dot indicator). - var headerBadge = document.querySelector('.aksrt-notification-badge'); - if (headerBadge) { - headerBadge.textContent = String(nextUnread); - headerBadge.style.display = nextUnread > 0 ? '' : 'none'; + window.addEventListener('popstate', function (event) { + var previous = event.state; + if (previous && previous.view) { + load(previous); } + }); - var avatarDot = document.querySelector('.nav-user__dot'); - if (avatarDot) { - avatarDot.style.display = nextUnread > 0 ? '' : 'none'; - } - } - - function checkEmpty() { - var list = document.querySelector('.messages-list'); - if (list && list.children.length === 0) { - list.outerHTML = '

暂无消息

'; - } - } - - function startPolling() { - pollTimer = setInterval(function () { - post('aksrt_check_new_notifications', {}).then(function (result) { - if (!result.success || !result.data) return; - if (result.data.unread !== undefined) { - updateBadges(result.data.unread); - return; - } - if (result.data.count !== undefined) { - updateBadges(result.data.count); - } - }).catch(function () {}); - }, pollInterval); - } - - startPolling(); + window.history.replaceState(state, '', routeFor(state.view)); + window.setInterval(function () { + post('aksrt_check_new_notifications', {}).then(function (result) { + if (result.success && result.data) updateHeaderBadge(result.data.unread); + }).catch(function () {}); + }, pollInterval); })(); diff --git a/functions.php b/functions.php index 60f04c2c..8141cc93 100644 --- a/functions.php +++ b/functions.php @@ -249,9 +249,6 @@ if (class_exists('\AKSRT\Ai\AI')) { if (class_exists('\AKSRT\Payment\WeChatNotify')) { \AKSRT\Payment\WeChatNotify::get_instance()->init(); } -if (class_exists('\AKSRT\Messages\Message')) { - \AKSRT\Messages\Message::get_instance()->init(); -} if (class_exists('\AKSRT\Messages\ReportCenter')) { \AKSRT\Messages\ReportCenter::get_instance()->init(); } diff --git a/inc/bootstrap.php b/inc/bootstrap.php index cb979f24..e3b096a7 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -76,6 +76,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-shortcodes.php'; require_once AKSRT_DIR . '/inc/modules/captcha.php'; require_once AKSRT_DIR . '/inc/modules/user-hub.php'; require_once AKSRT_DIR . '/inc/modules/user-notifications.php'; +require_once AKSRT_DIR . '/inc/modules/message-center.php'; require_once AKSRT_DIR . '/inc/modules/creator-revenue.php'; // Stage 3: core hooks diff --git a/inc/modules/message-center.php b/inc/modules/message-center.php new file mode 100644 index 00000000..c584e1c7 --- /dev/null +++ b/inc/modules/message-center.php @@ -0,0 +1,291 @@ + */ +function aksrt_message_center_views(): array { + return [ + 'unread' => __('未读消息', 'aksrt'), + 'posts' => __('文章评论', 'aksrt'), + 'like' => __('点赞喜欢', 'aksrt'), + 'system' => __('系统通知', 'aksrt'), + ]; +} + +function aksrt_normalize_message_view(string $view): string { + $view = sanitize_key($view); + return array_key_exists($view, aksrt_message_center_views()) ? $view : 'unread'; +} + +function aksrt_message_category_for_type(string $type): string { + $type = sanitize_key($type); + if (str_starts_with($type, 'asset_')) { + return ''; + } + if ( + in_array($type, ['like', 'post_like', 'comment_like', 'follow', 'followed', 'hot'], true) + || str_starts_with($type, 'like_') + || str_starts_with($type, 'follow_') + || str_starts_with($type, 'followed_') + ) { + return 'like'; + } + if ( + in_array($type, ['posts', 'post', 'comment', 'comment_reply', 'comment_approved', 'pending_comments', 'favorite', 'post_favorite'], true) + || str_starts_with($type, 'comment_') + || str_starts_with($type, 'favorite_') + ) { + return 'posts'; + } + return 'system'; +} + +/** @return array> */ +function aksrt_get_message_center_items(int $user_id, string $view): array { + $view = aksrt_normalize_message_view($view); + $items = function_exists('aksrt_get_user_all_notifications') + ? aksrt_get_user_all_notifications($user_id) + : []; + + return array_values(array_filter($items, static function (array $item) use ($view): bool { + $category = aksrt_message_category_for_type((string) ($item['type'] ?? 'notice')); + if ($category === '') { + return false; + } + if ($view === 'unread') { + return empty($item['read']); + } + return $category === $view; + })); +} + +/** @return array{unread:int,posts:int,like:int,system:int} */ +function aksrt_get_message_center_counts(int $user_id): array { + $counts = ['unread' => 0, 'posts' => 0, 'like' => 0, 'system' => 0]; + $items = function_exists('aksrt_get_user_all_notifications') + ? aksrt_get_user_all_notifications($user_id) + : []; + + foreach ($items as $item) { + $category = aksrt_message_category_for_type((string) ($item['type'] ?? 'notice')); + if ($category === '' || !empty($item['read'])) { + continue; + } + $counts['unread']++; + $counts[$category]++; + } + return $counts; +} + +/** @return array|null */ +function aksrt_get_message_center_item(int $user_id, int $notification_id): ?array { + if ($user_id <= 0 || $notification_id <= 0) { + return null; + } + global $wpdb; + $table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : ''; + if ($table === '') return null; + $row = $wpdb->get_row($wpdb->prepare( + "SELECT id, type, title, description, target_url, is_read, created_at, read_at FROM {$table} WHERE id = %d AND user_id = %d", + $notification_id, + $user_id + ), ARRAY_A); + if (!is_array($row) || aksrt_message_category_for_type((string) ($row['type'] ?? 'notice')) === '') return null; + return [ + 'id' => (string) $row['id'], + 'type' => (string) $row['type'], + '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'] ?? ''), + ]; +} + +function aksrt_message_icon_class(string $type): string { + $icons = [ + 'comment_reply' => 'fa fas fa-comment-dots', + 'comment_approved' => 'fa fas fa-check-circle', + 'pending_comments' => 'fa fas fa-clock', + 'post_favorite' => 'fa fas fa-star', + 'post_like' => 'fa fas fa-heart', + 'comment_like' => 'fa fas fa-thumbs-up', + 'followed' => 'fa fas fa-user-plus', + 'download_ready' => 'fa fas fa-download', + 'upload_moderation'=> 'fa fas fa-image', + 'verify_email' => 'fa fas fa-envelope-open', + 'pay' => 'fa fas fa-file-alt', + 'order_paid' => 'fa fas fa-file-alt', + 'order_refund' => 'fa fas fa-undo', + 'vip' => 'fa fas fa-certificate', + 'pay_vip' => 'fa fas fa-certificate', + 'medal' => 'fa fas fa-certificate', + 'badge' => 'fa fas fa-certificate', + 'ban_appeal' => 'fa fas fa-gavel', + 'user_ban' => 'fa fas fa-ban', + 'auth_apply' => 'fa fas fa-id-badge', + 'auth_apply_reply' => 'fa fas fa-id-badge', + ]; + return $icons[$type] ?? 'fa fas fa-bell'; +} + +function aksrt_message_time(array $item): string { + if (!empty($item['stateful'])) { + return __('状态提醒', 'aksrt'); + } + $created_at = (string) ($item['created_at'] ?? ''); + return $created_at !== '' ? get_date_from_gmt($created_at, 'Y-m-d H:i') : ''; +} + +function aksrt_render_message_center_nav(string $view, array $counts): void { + $view = aksrt_normalize_message_view($view); + ?> + +
+ + +
+ +
+ +
+ + + +
+ +
+
+
+
+ + + + +
+
+ +

+ +
+ +
+ 1) : ?> + + +
+
+ + +

+

·

+
+
+
+ + +
+
+ __('请先登录', 'aksrt')], 401); + } + $nonce = isset($_POST['nonce']) ? (string) wp_unslash($_POST['nonce']) : ''; + if (!wp_verify_nonce($nonce, 'aksrt_public_nonce')) { + wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); + } + + $user_id = get_current_user_id(); + $view = aksrt_normalize_message_view((string) ($_POST['view'] ?? 'unread')); + $page = max(1, absint($_POST['page'] ?? 1)); + $notification_id = absint($_POST['notification_id'] ?? 0); + $detail = null; + if ($notification_id > 0) { + $detail = aksrt_get_message_center_item($user_id, $notification_id); + if (!$detail) { + wp_send_json_error(['message' => __('消息不存在或已删除', 'aksrt')], 404); + } + aksrt_mark_user_notification_read($user_id, (string) $notification_id); + $detail['read'] = 1; + } + + $counts = aksrt_get_message_center_counts($user_id); + ob_start(); + aksrt_render_message_center_nav($view, $counts); + $nav_html = (string) ob_get_clean(); + ob_start(); + if ($detail) { + aksrt_render_message_center_detail($detail, $view); + } else { + aksrt_render_message_center_list($user_id, $view, $page); + } + $content_html = (string) ob_get_clean(); + + wp_send_json_success([ + 'view' => $view, + 'page' => $page, + 'detail' => $notification_id, + 'nav' => $nav_html, + 'content' => $content_html, + 'unread' => $counts['unread'], + ]); +}); diff --git a/inc/modules/user-notifications.php b/inc/modules/user-notifications.php index a2afe24e..a727819e 100644 --- a/inc/modules/user-notifications.php +++ b/inc/modules/user-notifications.php @@ -56,11 +56,12 @@ function aksrt_notification_type_label(string $type): string { $map = [ 'comment_reply' => __('评论回复', 'aksrt'), 'comment_approved' => __('评论审核', 'aksrt'), + 'comment' => __('文章评论', 'aksrt'), 'download_ready' => __('下载就绪', 'aksrt'), - 'asset_points_increase' => __('积分增加', 'aksrt'), - 'asset_points_decrease' => __('积分扣减', 'aksrt'), - 'asset_balance_increase' => __('余额增加', 'aksrt'), - 'asset_balance_decrease' => __('余额扣减', 'aksrt'), + 'post_favorite' => __('文章收藏', 'aksrt'), + 'post_like' => __('文章点赞', 'aksrt'), + 'comment_like' => __('评论点赞', 'aksrt'), + 'followed' => __('新增关注', 'aksrt'), 'pending_comments' => __('待审核评论', 'aksrt'), 'notice' => __('通知', 'aksrt'), ]; @@ -88,38 +89,18 @@ function aksrt_get_user_inbox_notifications(int $user_id = 0): array { 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; - } + 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 50", - $user_id + LIMIT %d", + $user_id, + $limit ), ARRAY_A); if (!is_array($rows)) return []; @@ -220,34 +201,6 @@ 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 { @@ -264,20 +217,7 @@ function aksrt_add_user_notification(int $user_id, array $payload): void { $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; - } + if ($table === '') return; $wpdb->insert($table, [ 'user_id' => $user_id, @@ -298,17 +238,7 @@ function aksrt_mark_user_notification_read(int $user_id, string $notification_id 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 ($table === '') return; if ($mark_all) { $wpdb->update( @@ -337,14 +267,7 @@ function aksrt_delete_user_notification(int $user_id, string $notification_id): 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; - } + if ($table === '') return; $wpdb->delete($table, ['id' => absint($notification_id), 'user_id' => $user_id], ['%d', '%d']); } endif; @@ -358,14 +281,7 @@ function aksrt_delete_user_notifications(int $user_id, array $notification_ids): 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; - } + if ($table === '') return; $wpdb->query( "DELETE FROM {$table} WHERE user_id = " . (int) $user_id . " AND id IN (" . implode(',', $ids) . ")" @@ -379,7 +295,7 @@ 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++; + if (empty($item['read']) && !str_starts_with((string) ($item['type'] ?? ''), 'asset_')) $count++; } return $count; } @@ -498,16 +414,33 @@ add_action('wp_ajax_aksrt_check_new_notifications', function () { wp_send_json_error(['message' => __('安全验证失败', 'aksrt')], 403); } - $unread = aksrt_get_user_unread_notification_count(get_current_user_id()); + $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_parent <= 0) 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); @@ -540,6 +473,7 @@ add_action('transition_comment_status', function ($new_status, $old_status, $com ]); 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; @@ -558,38 +492,55 @@ add_action('transition_comment_status', function ($new_status, $old_status, $com 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); +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); diff --git a/inc/modules/user-parts/routing.php b/inc/modules/user-parts/routing.php index a8a00d6d..2aa0d624 100644 --- a/inc/modules/user-parts/routing.php +++ b/inc/modules/user-parts/routing.php @@ -16,6 +16,7 @@ add_filter('query_vars', function ($vars) { $vars[] = 'aksrt_user_center'; $vars[] = 'aksrt_user_center_tab'; $vars[] = 'aksrt_author_identifier'; + $vars[] = 'aksrt_message_view'; return $vars; }); @@ -31,6 +32,13 @@ add_action('template_redirect', function () { nocache_headers(); return; } + if (!get_query_var('aksrt_message_view') && is_page() && get_page_template_slug(get_queried_object_id()) === 'templates/pages/page-messages.php') { + global $wp_query; + if ($wp_query instanceof WP_Query) { $wp_query->set_404(); } + status_header(404); + nocache_headers(); + return; + } $resolved_path = aksrt_get_current_special_template_path(); if ($resolved_path === '') { @@ -68,6 +76,9 @@ add_action('init', function () { add_rewrite_tag('%aksrt_user_center%', '([^&]+)'); add_rewrite_tag('%aksrt_user_center_tab%', '([^/]+)'); add_rewrite_tag('%aksrt_author_identifier%', '([^/]+)'); + add_rewrite_tag('%aksrt_message_view%', '(unread|posts|like|system)'); + add_rewrite_rule('^message/?$', 'index.php?aksrt_message_view=unread', 'top'); + add_rewrite_rule('^message/(posts|like|system)/?$', 'index.php?aksrt_message_view=$matches[1]', 'top'); add_rewrite_rule('^user/([a-z0-9_-]+)/?$', 'index.php?aksrt_user_center=1&aksrt_user_center_tab=$matches[1]', 'top'); add_rewrite_rule('^author/([^/]+)/?$', 'index.php?aksrt_author_identifier=$matches[1]', 'top'); }); @@ -177,7 +188,7 @@ add_action('init', function () { } // 统一 flush 检查 — bump when user-home rewrite / author override changes - $rewrite_version = AKSRT_VERSION . ':user-center-author-routing-v6'; + $rewrite_version = AKSRT_VERSION . ':message-routing-v1'; $flushed = get_option('aksrt_user_rules_flushed'); $rules = get_option('rewrite_rules'); $need_flush = ($flushed !== $rewrite_version) diff --git a/inc/modules/user-parts/template-registry.php b/inc/modules/user-parts/template-registry.php index c37195b2..261c9b0c 100644 --- a/inc/modules/user-parts/template-registry.php +++ b/inc/modules/user-parts/template-registry.php @@ -37,12 +37,6 @@ $_aksrt_special_templates = [ 'title' => '个人中心', 'fallback' => '/profile/', ], - 'messages' => [ - 'file' => 'templates/pages/page-messages.php', - 'slug' => 'messages', - 'title' => '消息中心', - 'fallback' => '/messages/', - ], 'submit' => [ 'file' => 'templates/pages/page-submit.php', 'slug' => 'submit', @@ -135,7 +129,10 @@ function aksrt_resolve_page_template_file(string $template_file = '', string $po $post_slug = sanitize_title($post_slug); if ($template_file !== '') { - if ($template_file === 'templates/pages/page-sign.php') { + if ($template_file === 'templates/pages/page-messages.php') { + return ''; + } + if ($template_file === 'templates/pages/page-sign.php') { return $template_file; } @@ -223,6 +220,11 @@ function aksrt_get_user_home_template_path(): string { * Resolve the active special-page template path for the current request. */ function aksrt_get_current_special_template_path(): string { + if (get_query_var('aksrt_message_view')) { + $message_template = aksrt_locate_template_file('templates/pages/page-messages.php'); + return $message_template ?: ''; + } + if (get_query_var('aksrt_author_identifier')) { return aksrt_get_user_home_template_path(); } diff --git a/inc/modules/user-parts/urls.php b/inc/modules/user-parts/urls.php index 65204bc3..642e28d3 100644 --- a/inc/modules/user-parts/urls.php +++ b/inc/modules/user-parts/urls.php @@ -55,16 +55,19 @@ function aksrt_get_new_post_url(int $edit_id = 0): string { endif; if (!function_exists('aksrt_get_messages_page_url')) : -/** - * 消息中心页面 URL(模板 templates/pages/page-messages.php)。 - */ -function aksrt_get_messages_page_url(): string { +/** 消息中心 URL。 */ +function aksrt_get_messages_page_url(string $view = 'unread'): string { if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::message_enabled()) { return ''; } - return function_exists('aksrt_get_template_url') - ? aksrt_get_template_url('messages') - : home_url('/messages/'); + $paths = [ + 'unread' => '/message/', + 'posts' => '/message/posts/', + 'like' => '/message/like/', + 'system' => '/message/system/', + ]; + $view = sanitize_key($view); + return home_url($paths[$view] ?? $paths['unread']); } endif; diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index 4849e8db..b85252af 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -568,7 +568,7 @@ trait ScriptsPart1b { } // 消息中心页 - if (is_page_template('templates/pages/page-messages.php')) { + if (get_query_var('aksrt_message_view')) { $messages_css = get_template_directory() . '/assets/css/frontend/messages.css'; if (file_exists($messages_css)) { wp_enqueue_style( diff --git a/src/Core/Templates.php b/src/Core/Templates.php index 3c9f6ef3..ad522dbc 100644 --- a/src/Core/Templates.php +++ b/src/Core/Templates.php @@ -46,7 +46,6 @@ class Templates { 'templates/pages/page-posts-navs.php' => __('AKSRT - 文章导航', 'aksrt'), 'templates/pages/page-friends.php' => __('AKSRT - 友情链接', 'aksrt'), 'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'), - 'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'), ]; } diff --git a/src/Messages/Message.php b/src/Messages/Message.php index 1fd35072..3b97b10c 100644 --- a/src/Messages/Message.php +++ b/src/Messages/Message.php @@ -16,33 +16,21 @@ class Message { 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", + return (int) $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0 AND type NOT LIKE 'asset\\_%'", $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); + return 0; } /** @@ -64,12 +52,7 @@ class Message { 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; + return false; } /** @@ -90,8 +73,7 @@ class Message { ); } - update_user_meta($user_id, 'aksrt_unread_message_count', 0); - return true; + return $table !== ''; } /** @@ -107,22 +89,18 @@ class Message { 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); + if ($table === '') return false; + global $wpdb; + $inserted = $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']); + if ($inserted === false) return false; // 触发通知钩子 do_action('aksrt_notification_sent', $to_user_id, $type, $data); @@ -181,36 +159,4 @@ class Message { 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)]); - } } diff --git a/src/Settings/Integration.php b/src/Settings/Integration.php index ec9f6160..3f703142 100644 --- a/src/Settings/Integration.php +++ b/src/Settings/Integration.php @@ -286,10 +286,6 @@ class Integration { 'comment_reply' => true, 'comment_approved' => true, 'download_ready' => true, - 'asset_points_increase' => true, - 'asset_points_decrease' => true, - 'asset_balance_increase' => true, - 'asset_balance_decrease' => true, 'pending_comments' => true, 'upload_moderation' => true, ], diff --git a/src/User/NotificationPreferences.php b/src/User/NotificationPreferences.php index 6ea1092d..c6a06ad5 100644 --- a/src/User/NotificationPreferences.php +++ b/src/User/NotificationPreferences.php @@ -69,21 +69,20 @@ final class NotificationPreferences { public static function category_for_type(string $notification_type): string { $type = sanitize_key($notification_type); if ( - in_array($type, ['posts', 'post', 'comment', 'favorite', 'pending_comments'], true) - || str_starts_with($type, 'post_') - || str_starts_with($type, 'comment_') - || str_starts_with($type, 'favorite_') - ) { - return 'posts'; - } - if ( - in_array($type, ['like', 'follow', 'followed', 'hot'], true) + in_array($type, ['like', 'post_like', 'comment_like', 'follow', 'followed', 'hot'], true) || str_starts_with($type, 'like_') || str_starts_with($type, 'follow_') || str_starts_with($type, 'followed_') ) { return 'like'; } + if ( + in_array($type, ['posts', 'post', 'comment', 'favorite', 'post_favorite', 'pending_comments'], true) + || str_starts_with($type, 'comment_') + || str_starts_with($type, 'favorite_') + ) { + return 'posts'; + } return 'system'; } diff --git a/templates/pages/page-messages.php b/templates/pages/page-messages.php index 6b61cdf0..dd0a4872 100644 --- a/templates/pages/page-messages.php +++ b/templates/pages/page-messages.php @@ -1,275 +1,51 @@ -
-
-
- -

-

-
-
-
- null, // special: !read - 'all' => null, // special: all - 'comment' => ['comment_reply', 'comment_approved', 'pending_comments'], - 'credits' => ['asset_points_increase', 'asset_points_decrease', 'asset_balance_increase', 'asset_balance_decrease'], - 'pay' => ['pay', 'order_paid', 'order_refund', 'shop_order', 'after_sale'], - 'vip' => ['vip', 'pay_vip', 'vip_expire', 'vip_upgrade'], - 'medal' => ['medal', 'badge', 'level_up'], - 'ban_appeal' => ['ban_appeal', 'user_ban', 'restriction'], - 'system' => ['notice', 'download_ready', 'upload_moderation', 'verify_email', 'auth_apply', 'auth_apply_reply'], -]; - -$allowed_types = array_keys($type_groups); -if (!in_array($current_type, $allowed_types, true)) { - $mapped = 'all'; - foreach ($type_groups as $group => $types) { - if (is_array($types) && in_array($current_type, $types, true)) { - $mapped = $group; - break; - } - } - $current_type = $mapped; -} - -$filtered = $all_notifications; -if ($current_type === 'unread') { - $filtered = array_filter($filtered, static fn($n) => empty($n['read'])); -} elseif ($current_type !== 'all') { - $types = $type_groups[$current_type] ?? []; - $filtered = array_filter($filtered, static function ($n) use ($types) { - $t = (string) ($n['type'] ?? 'notice'); - return in_array($t, $types, true); - }); -} -$filtered = array_values($filtered); - -$per_page = 20; -$current_page = max(1, (int) ($_GET['msg_page'] ?? 1)); -$total = count($filtered); -$total_pages = max(1, (int) ceil($total / $per_page)); -$offset = ($current_page - 1) * $per_page; -$paged_items = array_slice($filtered, $offset, $per_page); - -// 统计 - $type_counts = [ - 'all' => count($all_notifications), - 'unread' => 0, - 'comment' => 0, - 'credits' => 0, - 'pay' => 0, - 'vip' => 0, - 'medal' => 0, - 'ban_appeal' => 0, - 'system' => 0, - ]; - foreach ($all_notifications as $n) { - $t = (string) ($n['type'] ?? 'notice'); - if (empty($n['read'])) { - $type_counts['unread']++; - } - foreach (['comment', 'credits', 'pay', 'vip', 'medal', 'ban_appeal', 'system'] as $group) { - if (!empty($type_groups[$group]) && is_array($type_groups[$group]) && in_array($t, $type_groups[$group], true)) { - $type_counts[$group]++; - } - } - } - -$nav_items = [ - 'unread' => __('未读消息', 'aksrt'), - 'all' => __('全部消息', 'aksrt'), - 'comment' => __('评论互动', 'aksrt'), - 'credits' => __('资产动态', 'aksrt'), - 'pay' => __('交易通知', 'aksrt'), - 'vip' => __('会员动态', 'aksrt'), - 'medal' => __('徽章等级', 'aksrt'), - 'ban_appeal' => __('禁封申诉', 'aksrt'), - 'system' => __('系统通知', 'aksrt'), - ]; - - $icon_map = [ - 'comment_reply' => 'fa fas fa-comment-dots', - 'comment_approved' => 'fa fas fa-check-circle', - 'asset_points_increase' => 'fa fas fa-coins', - 'asset_points_decrease' => 'fa fas fa-coins', - 'asset_balance_increase' => 'fa fas fa-wallet', - 'asset_balance_decrease' => 'fa fas fa-wallet', - 'download_ready' => 'fa fas fa-download', - 'upload_moderation' => 'fa fas fa-image', - 'verify_email' => 'fa fas fa-envelope-open', - 'pending_comments' => 'fa fas fa-clock', - 'pay' => 'fa fas fa-file-alt', - 'order_paid' => 'fa fas fa-file-alt', - 'order_refund' => 'fa fas fa-undo', - 'after_sale' => 'fa fas fa-undo', - 'vip' => 'fa fas fa-certificate', - 'pay_vip' => 'fa fas fa-certificate', - 'medal' => 'fa fas fa-certificate', - 'badge' => 'fa fas fa-certificate', - 'ban_appeal' => 'fa fas fa-gavel', - 'user_ban' => 'fa fas fa-ban', - 'auth_apply' => 'fa fas fa-id-badge', - 'auth_apply_reply' => 'fa fas fa-id-badge', - 'notice' => 'fa fas fa-bell', -]; - -$messages_base = function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : get_permalink(); -if (!$messages_base) { - $messages_base = get_permalink(); -} +$current_view = aksrt_normalize_message_view((string) get_query_var('aksrt_message_view')); ?>
-

-
- - -
- -
- -
- - - -
-
- - - - -
- -
- - -
- -
-
- - - - - - - - - -
-
- -
- - 1) : ?> - - - - -
- -

-
- -
-
+
+ +
+ +
+ +
+
+
diff --git a/tests/widget-system/message-center-contract.test.cjs b/tests/widget-system/message-center-contract.test.cjs new file mode 100644 index 00000000..9705461d --- /dev/null +++ b/tests/widget-system/message-center-contract.test.cjs @@ -0,0 +1,77 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const root = path.resolve(__dirname, '..', '..'); +const read = (file) => fs.readFileSync(path.join(root, file), 'utf8'); + +test('message center exposes only the four canonical parameter-free routes', () => { + const routing = read('inc/modules/user-parts/routing.php'); + const urls = read('inc/modules/user-parts/urls.php'); + const registry = read('inc/modules/user-parts/template-registry.php'); + + assert.match(routing, /add_rewrite_rule\('\^message\/\?\$',[\s\S]*?aksrt_message_view=unread/); + assert.match(routing, /add_rewrite_rule\('\^message\/\(posts\|like\|system\)\/\?\$'/); + assert.doesNotMatch(routing, /message\/\(\[0-9]|aksrt_message_id/); + for (const route of ['/message/', '/message/posts/', '/message/like/', '/message/system/']) { + assert.ok(urls.includes(route), `${route} should be canonical`); + } + const messageUrlFunction = urls.slice( + urls.indexOf('function aksrt_get_messages_page_url'), + urls.indexOf('endif;', urls.indexOf('function aksrt_get_messages_page_url')), + ); + assert.doesNotMatch(messageUrlFunction, /add_query_arg|\/messages\//); + assert.doesNotMatch(registry, /'messages'\s*=>/); + assert.match(registry, /page-messages\.php'\)\s*\{[\s\S]*?return ''/); + assert.match(routing, /get_page_template_slug[\s\S]*?page-messages\.php[\s\S]*?set_404/); +}); + +test('tabs, pagination, actions, and detail all replace DOM fragments', () => { + const center = read('inc/modules/message-center.php'); + const script = read('assets/js/messages.js'); + const template = read('templates/pages/page-messages.php'); + + for (const label of ['未读消息', '文章评论', '点赞喜欢', '系统通知']) { + assert.ok(center.includes(label)); + } + assert.match(center, /wp_ajax_aksrt_load_message_center/); + assert.match(center, /aksrt_render_message_center_detail/); + assert.match(template, /data-message-content/); + assert.match(script, /nav\.innerHTML = result\.data\.nav/); + assert.match(script, /content\.innerHTML = result\.data\.content/); + assert.match(script, /\.js-msg-detail/); + assert.match(script, /data-message-back/); + assert.doesNotMatch(script, /searchParams|msg_type|msg_page|location\.href\s*=/); + assert.doesNotMatch(script, /pushState\([^\n]+detail/); +}); + +test('asset notifications are absent and interaction notification hooks are present', () => { + const notifications = read('inc/modules/user-notifications.php'); + const settings = read('src/Settings/Integration.php'); + const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue'); + const messageAdmin = read('assets/admin/src/pages/users/UsersMessages.vue'); + const combined = [notifications, settings, adminSettings, messageAdmin].join('\n'); + + for (const retired of [ + 'asset_points_increase', + 'asset_points_decrease', + 'asset_balance_increase', + 'asset_balance_decrease', + ]) { + assert.equal(combined.includes(retired), false, `${retired} should be absent`); + } + assert.doesNotMatch(notifications, /add_action\('aksrt_asset_transaction_committed'/); + for (const hook of ['aksrt_post_favorited', 'aksrt_post_liked', 'aksrt_comment_liked', 'aksrt_user_followed']) { + assert.ok(notifications.includes(`add_action('${hook}'`)); + } +}); + +test('notification storage has no message-center legacy metadata fallback', () => { + const notifications = read('inc/modules/user-notifications.php'); + const service = read('src/Messages/Message.php'); + const combined = `${notifications}\n${service}`; + + assert.doesNotMatch(combined, /aksrt_notifications|aksrt_unread_message_count/); + assert.doesNotMatch(combined, /legacy|兼容|降级/i); +});