sozsoft-platform/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/NotificationManager.cs
2026-09-20 23:23:39 +03:00

202 lines
8.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Sozsoft.Notifications.Entities;
using Sozsoft.Notifications.Enums;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Identity;
namespace Sozsoft.Notifications.Domain;
public interface INotificationManager : ITransientDependency
{
/// <summary>
/// Bir bildirim tipinin tum kurallarini isletir ve her alici icin kanal basina bir bildirim
/// kaydi uretir. Kayitlarin kanaldan cikisi <c>NotificationWorker</c>'a aittir.
/// </summary>
Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage);
}
public class NotificationManager : INotificationManager
{
private readonly IReadOnlyRepository<NotificationRule> repository;
private readonly IReadOnlyRepository<IdentityUser> repositoryUser;
private readonly IReadOnlyRepository<IdentityRole> repositoryRole;
private readonly IRepository<Notification, Guid> repositoryNotification;
private readonly INotificationIdentifierProvider notificationIdentifierProvider;
private readonly IOrganizationUnitRepository repositoryOrganizationUnit;
private readonly INotificationRealtimeNotifier realtimeNotifier;
public NotificationManager(
IReadOnlyRepository<NotificationRule> repository,
IReadOnlyRepository<IdentityUser> repositoryUser,
IReadOnlyRepository<IdentityRole> repositoryRole,
IRepository<Notification, Guid> repositoryNotification,
INotificationIdentifierProvider notificationIdentifierProvider,
IOrganizationUnitRepository repositoryOrganizationUnit,
INotificationRealtimeNotifier realtimeNotifier
)
{
this.repository = repository;
this.repositoryUser = repositoryUser;
this.repositoryRole = repositoryRole;
this.repositoryNotification = repositoryNotification;
this.notificationIdentifierProvider = notificationIdentifierProvider;
this.repositoryOrganizationUnit = repositoryOrganizationUnit;
this.realtimeNotifier = realtimeNotifier;
}
public async Task SendNotificationAsync(Guid NotificationTypeId, string NotificationMessage)
{
var rules = await repository.GetListAsync(a => a.NotificationTypeId == NotificationTypeId);
// Kullanici tercihleri (profil ekranindan yazilan IsCustomized kayitlari) alici kumesini
// belirlemez; yalnizca sahibinin o kanaldaki aboneligini kapatir. Tercihi kural gibi
// isletmek, bir kullanicinin kendi tercihini yazmasiyla ayni tur/kanaldaki herkesin
// bildirimini kesmesi anlamina gelirdi.
var optedOut = rules
.Where(rule => rule.IsCustomized
&& !rule.IsActive
&& rule.RecipientType == NotificationRecipientTypes.User
&& rule.RecipientId != null)
.Select(rule => (rule.Channel, UserName: rule.RecipientId!))
.ToHashSet();
// Ayni kullaniciyi birden fazla kural hedefleyebilir (All + Role gibi); kanal basina tek
// kayit uretilir.
var delivered = new HashSet<(string Channel, string Identifier)>();
var notifications = new List<Notification>();
var queryUsers = await repositoryUser.GetQueryableAsync();
foreach (var notificationRule in rules.Where(rule => rule.IsActive && !rule.IsCustomized))
{
// "Custom" alicida RecipientId adresin kendisidir (telefon, e-posta, chat id);
// sistemde bir kullaniciya karsilik gelmeyebilir, bu yuzden kullanici aramasi yapilmaz.
if (notificationRule.RecipientType == NotificationRecipientTypes.Custom)
{
if (notificationRule.RecipientId.IsNullOrWhiteSpace()
|| !delivered.Add((notificationRule.Channel, notificationRule.RecipientId!)))
{
continue;
}
notifications.Add(BuildNotification(notificationRule, notificationRule.RecipientId!, userId: null, NotificationMessage));
continue;
}
var userIds = await ResolveRecipientUserIdsAsync(notificationRule, queryUsers);
if (userIds.Count == 0)
{
continue;
}
var users = await queryUsers
.Where(user => userIds.Contains(user.Id))
.Select(user => new { user.Id, user.UserName })
.ToListAsync();
foreach (var user in users)
{
// Zorunlu bildirimde (IsFixed) kullanici tercihi dikkate alinmaz.
if (!notificationRule.IsFixed && optedOut.Contains((notificationRule.Channel, user.UserName)))
{
continue;
}
var identifier = await notificationIdentifierProvider.GetIdentifier(user.Id, notificationRule.Channel);
if (identifier.IsNullOrWhiteSpace() || !delivered.Add((notificationRule.Channel, identifier!)))
{
continue;
}
notifications.Add(BuildNotification(notificationRule, identifier!, user.Id, NotificationMessage));
}
}
if (notifications.Count == 0)
{
return;
}
await repositoryNotification.InsertManyAsync(notifications, autoSave: true);
// Duyuru kayit yazildiktan sonra yapilir; istemci duyuruyu alip listesini okudugunda
// kaydi mutlaka gormeli. Duyuru gitmezse yedek yoklama ayni kaydi getirir.
await realtimeNotifier.NotifyAsync(
[.. notifications
.Where(notification => notification.UserId.HasValue)
.Select(notification => new NotificationDelivery(notification.UserId!.Value, notification.NotificationChannel))]);
}
/// <summary>Kuralin hedefledigi kullanicilar; <c>Custom</c> alici bu yoldan gecmez.</summary>
private async Task<List<Guid>> ResolveRecipientUserIdsAsync(NotificationRule notificationRule, IQueryable<IdentityUser> queryUsers)
{
List<Guid> userIds = [];
if (notificationRule.RecipientType == NotificationRecipientTypes.All)
{
userIds.AddRange(await queryUsers
.Where(user => user.IsActive)
.Select(user => user.Id)
.ToListAsync());
}
else if (notificationRule.RecipientType == NotificationRecipientTypes.User)
{
var user = await queryUsers.FirstOrDefaultAsync(user => user.UserName == notificationRule.RecipientId);
if (user != null)
{
userIds.Add(user.Id);
}
}
else if (notificationRule.RecipientType == NotificationRecipientTypes.Role)
{
var role = await repositoryRole.FirstOrDefaultAsync(role => role.Name == notificationRule.RecipientId);
if (role != null)
{
userIds.AddRange(await queryUsers
.Where(user => user.Roles.Any(userRole => userRole.RoleId == role.Id) && user.IsActive)
.Select(user => user.Id)
.ToListAsync());
}
}
else if (notificationRule.RecipientType == NotificationRecipientTypes.OrganizationUnit)
{
if (notificationRule.RecipientId.IsNullOrWhiteSpace())
{
return userIds;
}
var units = await repositoryOrganizationUnit.GetAllChildrenWithParentCodeAsync(notificationRule.RecipientId, null);
foreach (var unit in units)
{
userIds.AddRange(await repositoryOrganizationUnit.GetMemberIdsAsync(unit.Id));
}
}
return [.. userIds.Distinct()];
}
private static Notification BuildNotification(NotificationRule notificationRule, string identifier, Guid? userId, string message) => new()
{
NotificationTypeId = notificationRule.NotificationTypeId,
NotificationRuleId = notificationRule.Id,
NotificationChannel = notificationRule.Channel,
Identifier = identifier,
UserId = userId,
Message = message,
IsSent = IsDeliveredOnCreate(notificationRule.Channel),
IsRead = false
};
/// <summary>
/// Kanalin gonderimi kaydin kendisi mi. UiActivity'nin ayri bir gonderim adimi yoktur —
/// kayit dogrudan kullanicinin bildirim akisina yazilir, bu yuzden olusurken iletilmis
/// sayilir. Diger kanallar <c>NotificationWorker</c> ya da tarayici gosterimi ile isaretlenir;
/// isaretlenmeden okunamazlar.
/// </summary>
private static bool IsDeliveredOnCreate(string channel)
=> channel == NotificationChannels.UiActivity;
}