version 3
This commit is contained in:
@@ -35,14 +35,28 @@ public sealed class AlertService : IAlertService
|
||||
|
||||
public async Task CheckAndSendAlertsAsync(int deviceId, TelemetryDto telemetry, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device with settings and user
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.User)
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null || device.User == null)
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device or user not found: DeviceId={DeviceId}", deviceId);
|
||||
logger.LogWarning("Device not found: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for device: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,7 +101,7 @@ public sealed class AlertService : IAlertService
|
||||
if (allRulesMatch && condition.Rules.Any())
|
||||
{
|
||||
// All rules passed, send alert if cooldown period has passed
|
||||
await SendAlertForConditionAsync(condition, device, telemetry, cancellationToken);
|
||||
await SendAlertForConditionAsync(condition, device, usersToAlert, telemetry, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +133,7 @@ public sealed class AlertService : IAlertService
|
||||
private async Task SendAlertForConditionAsync(
|
||||
Domain.AlertCondition condition,
|
||||
Domain.Device device,
|
||||
List<Domain.User> usersToAlert,
|
||||
TelemetryDto telemetry,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -127,68 +142,95 @@ public sealed class AlertService : IAlertService
|
||||
? condition.CallCooldownMinutes
|
||||
: condition.SmsCooldownMinutes;
|
||||
|
||||
// Check if alert was sent recently
|
||||
var cooldownTime = DateTime.UtcNow.AddMinutes(-cooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == device.Id &&
|
||||
a.UserId == device.User.Id &&
|
||||
a.AlertConditionId == condition.Id &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (recentAlert != null)
|
||||
{
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||
device.Id, condition.Id, condition.NotificationType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build alert message
|
||||
// Build alert message once
|
||||
var message = BuildAlertMessage(condition, device.DeviceName, telemetry);
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
// Send notification
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
// Send alert to each user
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
// Check if alert was sent recently to this user
|
||||
var cooldownTime = sentAt.AddMinutes(-cooldownMinutes);
|
||||
var recentAlert = await dbContext.AlertNotifications
|
||||
.Where(a => a.DeviceId == device.Id &&
|
||||
a.UserId == user.Id &&
|
||||
a.AlertConditionId == condition.Id &&
|
||||
a.SentAt >= cooldownTime)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||
if (recentAlert != null)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||
logger.LogInformation("Alert skipped due to cooldown: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
continue;
|
||||
}
|
||||
else // Call
|
||||
|
||||
// Send notification
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(device.User.Mobile, device.DeviceName, message, cancellationToken);
|
||||
if (condition.NotificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else // Call
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, UserId={UserId}, ConditionId={ConditionId}",
|
||||
device.Id, user.Id, condition.Id);
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send alert: DeviceId={DeviceId}, ConditionId={ConditionId}, Type={Type}",
|
||||
device.Id, condition.Id, condition.NotificationType);
|
||||
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification to database (old table for backwards compatibility)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
AlertType = Domain.AlertType.Condition,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
|
||||
// Save notification to database
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = device.User.Id,
|
||||
AlertConditionId = condition.Id,
|
||||
NotificationType = condition.NotificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = DateTime.UtcNow,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -323,5 +365,131 @@ public sealed class AlertService : IAlertService
|
||||
return (false, null, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendPowerOutageAlertAsync(int deviceId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get device with all users who should receive alerts
|
||||
var device = await dbContext.Devices
|
||||
.Include(d => d.DeviceUsers.Where(du => du.ReceiveAlerts))
|
||||
.ThenInclude(du => du.User)
|
||||
.FirstOrDefaultAsync(d => d.Id == deviceId, cancellationToken);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
logger.LogWarning("Device not found for power outage alert: DeviceId={DeviceId}", deviceId);
|
||||
throw new InvalidOperationException($"دستگاه با شناسه {deviceId} یافت نشد");
|
||||
}
|
||||
|
||||
// Get all users who should receive alerts
|
||||
var usersToAlert = device.DeviceUsers
|
||||
.Where(du => du.ReceiveAlerts)
|
||||
.Select(du => du.User)
|
||||
.ToList();
|
||||
|
||||
if (usersToAlert.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No users with ReceiveAlerts enabled for power outage: DeviceId={DeviceId}", deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = $"⚠️ هشدار قطع برق! دستگاه {device.DeviceName} از برق قطع شده است.";
|
||||
var sentAt = DateTime.UtcNow;
|
||||
|
||||
// Send to all users (both SMS and Call for power outage - it's critical!)
|
||||
foreach (var user in usersToAlert)
|
||||
{
|
||||
// Send SMS
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.SMS,
|
||||
cancellationToken);
|
||||
|
||||
// Send Call (important alert)
|
||||
await SendPowerOutageNotificationAsync(
|
||||
device, user, message, sentAt,
|
||||
Domain.AlertNotificationType.Call,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Power outage alerts sent to {Count} users for device {DeviceId}",
|
||||
usersToAlert.Count, deviceId);
|
||||
}
|
||||
|
||||
private async Task SendPowerOutageNotificationAsync(
|
||||
Domain.Device device,
|
||||
Domain.User user,
|
||||
string message,
|
||||
DateTime sentAt,
|
||||
Domain.AlertNotificationType notificationType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
string? messageOutboxIds = null;
|
||||
string? errorMessage = null;
|
||||
bool isSent = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (notificationType == Domain.AlertNotificationType.SMS)
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendSmsAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
(isSent, messageOutboxIds, errorMessage) = await SendCallAlertAsync(
|
||||
user.Mobile, device.DeviceName, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Exception: {ex.Message}";
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorMessage += $" | InnerException: {ex.InnerException.Message}";
|
||||
}
|
||||
isSent = false;
|
||||
logger.LogError(ex, "Failed to send power outage alert: DeviceId={DeviceId}, UserId={UserId}, Type={Type}",
|
||||
device.Id, user.Id, notificationType);
|
||||
}
|
||||
|
||||
var processingTime = (long)(DateTime.UtcNow - startTime).TotalMilliseconds;
|
||||
|
||||
// Save notification (old table)
|
||||
var notification = new Domain.AlertNotification
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
MessageOutboxIds = messageOutboxIds,
|
||||
ErrorMessage = errorMessage,
|
||||
SentAt = sentAt,
|
||||
IsSent = isSent
|
||||
};
|
||||
|
||||
dbContext.AlertNotifications.Add(notification);
|
||||
|
||||
// Log the alert
|
||||
var alertLog = new Domain.AlertLog
|
||||
{
|
||||
DeviceId = device.Id,
|
||||
UserId = user.Id,
|
||||
AlertConditionId = null,
|
||||
AlertType = Domain.AlertType.PowerOutage,
|
||||
NotificationType = notificationType,
|
||||
Message = message,
|
||||
Status = isSent ? Domain.AlertStatus.Success : Domain.AlertStatus.Failed,
|
||||
ErrorMessage = errorMessage,
|
||||
PhoneNumber = user.Mobile,
|
||||
SentAt = sentAt,
|
||||
ProcessingTimeMs = processingTime
|
||||
};
|
||||
|
||||
dbContext.AlertLogs.Add(alertLog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user