576 lines
21 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|
|
|