version 3

This commit is contained in:
2025-12-17 00:34:41 +03:30
parent 139924db94
commit 74e8480a68
38 changed files with 5399 additions and 70 deletions

View File

@@ -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);
}
}