Files
citrus-cms/app/Functions/notification-helper.php
2026-04-28 21:14:25 +03:00

576 lines
21 KiB
PHP

<?php
use App\Models\Notification;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Send notification to users based on notification code and role mapping
*
* Performance optimizations:
* - Single query to get target roles from settings
* - Bulk insert for notifications (one query for all users)
* - Uses whereIn for efficient user filtering
* - Chunked inserts (500 per chunk) to avoid max packet size issues
*
* Note: Direct execution is fast enough (1-2 seconds for typical 10-50 users)
* Queue integration was removed to avoid unnecessary complexity and worker dependency.
*
* @param string $notificationCode Notification code from catalog
* @param string $message Notification message
* @param string|null $link Optional link for notification
* @param string|null $title Optional custom title (if null, uses default from code)
* @param array|null $filterParams Optional filter parameters for filtered list view
* @param int $itemCount Item count for batch notifications (default: 1)
* @return int Number of notifications created
*/
function sendNotification($notificationCode, $message, $link = null, $title = null, $filterParams = null, $itemCount = 1) {
try {
// Get target roles from settings
$targetRoles = j(setting($notificationCode));
// If no roles defined or empty, log warning and return early
if (!is_array($targetRoles) || empty($targetRoles)) {
Log::warning("Notification not sent: No roles defined in settings", [
'notification_code' => $notificationCode,
'message' => $message,
'title' => $title,
]);
return 0;
}
// Get active users with target roles in single query
// Performance: Uses whereIn for efficient filtering
$users = DB::table('users')
->select('id', 'level', 'name', 'email')
->whereIn('level', $targetRoles)
->whereNotNull('level') // Ensure level is not null
->get();
// Remove level, name, email from selection after logging (we only need id for insert)
$userIds = $users->pluck('id');
// Debug: Log all users found
Log::info("Notification target users found", [
'notification_code' => $notificationCode,
'target_roles' => $targetRoles,
'users_count' => $users->count(),
'user_ids' => $users->pluck('id')->toArray(),
'user_levels' => $users->pluck('level')->toArray(),
]);
// If no users found, log warning and return early
if ($users->isEmpty()) {
// Debug: Check what users exist with these levels
$allUsersWithLevels = DB::table('users')
->select('id', 'level', 'name', 'email')
->whereNotNull('level')
->get()
->groupBy('level');
Log::warning("Notification not sent: No users found with target roles", [
'notification_code' => $notificationCode,
'target_roles' => $targetRoles,
'message' => $message,
'available_levels' => $allUsersWithLevels->keys()->toArray(),
'users_by_level' => $allUsersWithLevels->map(function($group) {
return $group->pluck('id')->toArray();
})->toArray(),
]);
return 0;
}
// Generate title if not provided
if ($title === null) {
$title = ucwords(str_replace('_', ' ', $notificationCode));
}
// If filterParams provided, generate link to filtered list page
// We'll update link after insert with notification ID
$useFilteredList = !empty($filterParams);
if ($useFilteredList && $link === null) {
$link = '/admin/notifications/filtered-list'; // Will be updated with notification ID after insert
}
// Prepare bulk insert data
// Performance: Single insert query for all notifications
$now = now();
$notificationsData = [];
foreach ($userIds as $userId) {
$notificationsData[] = [
'user_id' => $userId,
'notification_code' => $notificationCode,
'title' => $title,
'message' => $message,
'link' => $link,
'is_read' => false,
'filter_params' => $filterParams ? json_encode($filterParams) : null,
'item_count' => $itemCount,
'notification_type' => $filterParams ? 'batch' : 'single',
'created_at' => $now,
'updated_at' => $now,
];
}
// Bulk insert notifications
// Performance: One query instead of N queries
if (!empty($notificationsData)) {
// Split into chunks to avoid max packet size issues
$chunks = array_chunk($notificationsData, 500);
$totalInserted = 0;
foreach ($chunks as $chunk) {
// If using filtered list, we need to update links with notification IDs after insert
if ($useFilteredList) {
$ids = [];
foreach ($chunk as $data) {
$id = DB::table('notifications')->insertGetId($data);
$ids[] = $id;
}
// Update links with notification IDs
foreach ($ids as $id) {
DB::table('notifications')
->where('id', $id)
->update(['link' => '/admin/notifications/filtered-list?id=' . $id]);
}
$totalInserted += count($chunk);
} else {
DB::table('notifications')->insert($chunk);
$totalInserted += count($chunk);
}
}
return $totalInserted;
}
return 0;
} catch (\Exception $e) {
// Log error but don't break the main flow
Log::error('Notification send error: ' . $e->getMessage(), [
'code' => $notificationCode,
'message' => $message,
]);
return 0;
}
}
/**
* Helper: Send batch notification with filter params and count-based message
*
* @param string $notificationCode
* @param string $title
* @param string $messageTemplate String with %d placeholder for count
* @param array $filterParams
* @param int $count
* @return int
*/
function sendBatchNotificationWithFilter($notificationCode, $title, $messageTemplate, array $filterParams, int $count)
{
$count = (int) $count;
if ($count <= 0) {
return 0;
}
$message = $messageTemplate ? sprintf($messageTemplate, $count) : sprintf('%d record(s) require attention.', $count);
return sendNotification($notificationCode, $message, null, $title, $filterParams, $count);
}
/**
* Send notification to specific users (by user IDs)
* Performance: Bulk insert for multiple users
*
* @param array $userIds Array of user IDs
* @param string $notificationCode Notification code
* @param string $message Notification message
* @param string|null $link Optional link
* @param string|null $title Optional title
* @return int Number of notifications created
*/
function sendNotificationToUsers($userIds, $notificationCode, $message, $link = null, $title = null) {
try {
if (empty($userIds)) {
return 0;
}
// Generate title if not provided
if ($title === null) {
$title = ucwords(str_replace('_', ' ', $notificationCode));
}
// Prepare bulk insert data
$now = now();
$notificationsData = [];
foreach ($userIds as $userId) {
$notificationsData[] = [
'user_id' => $userId,
'notification_code' => $notificationCode,
'title' => $title,
'message' => $message,
'link' => $link,
'is_read' => false,
'created_at' => $now,
'updated_at' => $now,
];
}
// Bulk insert
if (!empty($notificationsData)) {
$chunks = array_chunk($notificationsData, 500);
$totalInserted = 0;
foreach ($chunks as $chunk) {
DB::table('notifications')->insert($chunk);
$totalInserted += count($chunk);
}
return $totalInserted;
}
return 0;
} catch (\Exception $e) {
Log::error('Notification send error (specific users): ' . $e->getMessage());
return 0;
}
}
/**
* Send notification to single user
* Performance: Single insert query
*
* @param int $userId User ID
* @param string $notificationCode Notification code
* @param string $message Notification message
* @param string|null $link Optional link
* @param string|null $title Optional title
* @return bool Success status
*/
function sendNotificationToUser($userId, $notificationCode, $message, $link = null, $title = null) {
try {
if ($title === null) {
$title = ucwords(str_replace('_', ' ', $notificationCode));
}
Notification::create([
'user_id' => $userId,
'notification_code' => $notificationCode,
'title' => $title,
'message' => $message,
'link' => $link,
'is_read' => false,
]);
return true;
} catch (\Exception $e) {
Log::error('Notification send error (single user): ' . $e->getMessage());
return false;
}
}
/**
* Clean up old notifications
* Performance: Bulk delete query with date filter
*
* @param int $days Number of days to keep (default: 90)
* @return int Number of deleted notifications
*/
function cleanupOldNotifications($days = 90) {
try {
return Notification::deleteOldNotifications($days);
} catch (\Exception $e) {
Log::error('Notification cleanup error: ' . $e->getMessage());
return 0;
}
}
/**
* Copy existing notifications to new users based on their level
* DISABLED: This function was causing duplicate notifications and system issues
*
* @param string $notificationCode Notification code to copy
* @return int Always returns 0 (disabled)
*/
function copyNotificationsToNewUsers($notificationCode) {
// DISABLED: This function was causing excessive duplicate notifications
// All calls to this function should be removed from notification check commands
return 0;
}
/**
* Normalize filter_params for comparison
*
* @param mixed $filterParams
* @return string
*/
function normalizeFilterParams($filterParams): string
{
if (empty($filterParams)) {
return '';
}
$decoded = is_string($filterParams)
? json_decode($filterParams, true)
: $filterParams;
if ($decoded === null || !is_array($decoded)) {
return '';
}
ksort($decoded);
return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
/**
* Clean up duplicate notifications by notification_code + user_id + filter_params
* This is more aggressive and handles cases where same notification_code is sent multiple times
* Uses direct SQL queries with minimal memory usage
*
* @return array{total_deleted: int, notifications_before: int, notifications_after: int, error?: string}
*/
function cleanupDuplicateNotificationsByCode(): array
{
try {
$stats = [
'total_deleted' => 0,
'notifications_before' => 0,
'notifications_after' => 0,
];
$stats['notifications_before'] = DB::table('notifications')->count();
// Process in very small batches to avoid memory issues
$batchSize = 10; // Process 10 users at a time
$totalDeleted = 0;
$usersAffected = [];
// Get all notification codes one by one
$notificationCodes = DB::table('notifications')
->select('notification_code')
->distinct()
->pluck('notification_code');
foreach ($notificationCodes as $notificationCode) {
// Get user IDs for this code in batches
$offset = 0;
while (true) {
$userIds = DB::table('notifications')
->where('notification_code', $notificationCode)
->select('user_id')
->distinct()
->offset($offset)
->limit($batchSize)
->pluck('user_id');
if ($userIds->isEmpty()) {
break;
}
foreach ($userIds as $userId) {
// Get notification IDs for this code + user, ordered by created_at desc
// Process in small chunks
$notificationOffset = 0;
$notificationLimit = 100;
$seenFilterParams = [];
$idsToDelete = [];
while (true) {
$notifications = DB::table('notifications')
->where('notification_code', $notificationCode)
->where('user_id', $userId)
->select('id', 'filter_params', 'created_at')
->orderBy('created_at', 'desc')
->offset($notificationOffset)
->limit($notificationLimit)
->get();
if ($notifications->isEmpty()) {
break;
}
foreach ($notifications as $notification) {
// Normalize filter_params
$filterParams = normalizeFilterParams($notification->filter_params);
// If we've seen this filter_params before, it's a duplicate
if (isset($seenFilterParams[$filterParams])) {
$idsToDelete[] = $notification->id;
} else {
// First occurrence - keep it
$seenFilterParams[$filterParams] = true;
}
}
$notificationOffset += $notificationLimit;
// If we got less than limit, we're done
if ($notifications->count() < $notificationLimit) {
break;
}
}
// Delete duplicates for this user
if (!empty($idsToDelete)) {
$deleteChunks = array_chunk($idsToDelete, 500);
foreach ($deleteChunks as $chunk) {
$deleted = DB::table('notifications')->whereIn('id', $chunk)->delete();
$totalDeleted += $deleted;
}
if (!in_array($userId, $usersAffected)) {
$usersAffected[] = $userId;
}
}
// Free memory
unset($notifications, $idsToDelete, $seenFilterParams);
}
$offset += $batchSize;
}
}
// Clear cache for affected users
foreach ($usersAffected as $userId) {
Cache::forget("user_notifications_{$userId}");
Cache::forget("user_unread_type_count_{$userId}");
}
$stats['total_deleted'] = $totalDeleted;
$stats['notifications_after'] = DB::table('notifications')->count();
Log::info('Duplicate notifications cleanup by code completed', $stats);
return $stats;
} catch (\Exception $e) {
Log::error('Error cleaning up duplicate notifications by code: ' . $e->getMessage());
return [
'error' => $e->getMessage(),
'total_deleted' => 0,
];
}
}
/**
* Clean up duplicate notifications
* Removes duplicate notifications keeping only the most recent one for each user
* Duplicates are identified by: user_id + message + link + normalized filter_params
*
* Performance: Uses SQL-based approach with chunking to avoid memory issues
*
* @return array{total_deleted: int, users_affected: int, notifications_before: int, notifications_after: int, error?: string}
*/
function cleanupDuplicateNotifications(): array
{
try {
$stats = [
'total_deleted' => 0,
'users_affected' => 0,
'notifications_before' => 0,
'notifications_after' => 0,
];
// Get total count before cleanup
$stats['notifications_before'] = DB::table('notifications')->count();
// Process one user at a time to minimize memory usage
$userIds = DB::table('notifications')
->select('user_id')
->distinct()
->pluck('user_id');
$usersAffected = [];
$totalDeleted = 0;
foreach ($userIds as $userId) {
// Get notifications for this user, ordered by created_at desc (newest first)
// Process in smaller chunks to avoid memory issues
$offset = 0;
$limit = 500;
$userHasDuplicates = false;
$seen = [];
$idsToDelete = [];
while (true) {
$notifications = DB::table('notifications')
->where('user_id', $userId)
->select('id', 'message', 'link', 'filter_params', 'created_at')
->orderBy('created_at', 'desc')
->offset($offset)
->limit($limit)
->get();
if ($notifications->isEmpty()) {
break;
}
foreach ($notifications as $notification) {
// Normalize filter_params
$filterParams = normalizeFilterParams($notification->filter_params);
$key = $notification->message . '|' . ($notification->link ?? '') . '|' . $filterParams;
// If we've seen this key before, it's a duplicate
if (isset($seen[$key])) {
$idsToDelete[] = $notification->id;
$userHasDuplicates = true;
} else {
// First occurrence - keep it
$seen[$key] = true;
}
}
$offset += $limit;
// If we got less than limit, we're done with this user
if ($notifications->count() < $limit) {
break;
}
}
// Delete duplicates for this user
if (!empty($idsToDelete)) {
$deleteChunks = array_chunk($idsToDelete, 500);
foreach ($deleteChunks as $chunk) {
$deleted = DB::table('notifications')->whereIn('id', $chunk)->delete();
$totalDeleted += $deleted;
}
if ($userHasDuplicates) {
$usersAffected[] = $userId;
// Clear cache for this user
Cache::forget("user_notifications_{$userId}");
Cache::forget("user_unread_type_count_{$userId}");
}
}
// Free memory
unset($notifications, $idsToDelete, $seen);
}
$stats['total_deleted'] = $totalDeleted;
$stats['users_affected'] = count($usersAffected);
// Get total count after cleanup
$stats['notifications_after'] = DB::table('notifications')->count();
Log::info('Duplicate notifications cleanup completed', $stats);
return $stats;
} catch (\Exception $e) {
Log::error('Error cleaning up duplicate notifications: ' . $e->getMessage());
return [
'error' => $e->getMessage(),
'total_deleted' => 0,
];
}
}