| | | 1 | | using Microsoft.AspNetCore.SignalR; |
| | | 2 | | using Microsoft.AspNetCore.Identity; |
| | | 3 | | using System; |
| | | 4 | | using System.Collections.Generic; |
| | | 5 | | using System.Threading.Tasks; |
| | | 6 | | using System.Collections.Concurrent; |
| | | 7 | | using System.Linq; |
| | | 8 | | using OzonRepositories.Context.Identity; |
| | | 9 | | |
| | | 10 | | namespace Services.SignalRServices |
| | | 11 | | { |
| | | 12 | | public class UserSessionInfo |
| | | 13 | | { |
| | | 14 | | public string UserName { get; set; } = null!; |
| | | 15 | | public DateTime ConnectedAt { get; set; } |
| | | 16 | | public string CurrentPage { get; set; } = null!; |
| | | 17 | | } |
| | | 18 | | |
| | | 19 | | public class NotificationHub : Hub |
| | | 20 | | { |
| | 0 | 21 | | private static readonly ConcurrentDictionary<string, UserSessionInfo> ConnectedUsers = new(); |
| | 0 | 22 | | private static readonly ConcurrentDictionary<string, (List<object> Result, DateTime ExpiresAt)> UsersWithRolesCa |
| | 0 | 23 | | private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(60); |
| | | 24 | | |
| | | 25 | | private readonly UserManager<CustomIdentityUser> _userManager; |
| | | 26 | | |
| | 0 | 27 | | public NotificationHub(UserManager<CustomIdentityUser> userManager) |
| | | 28 | | { |
| | 0 | 29 | | _userManager = userManager; |
| | 0 | 30 | | } |
| | | 31 | | |
| | | 32 | | public override async Task OnConnectedAsync() |
| | | 33 | | { |
| | 0 | 34 | | var connectionId = Context.ConnectionId; |
| | 0 | 35 | | var userName = Context.User?.Identity?.Name ?? "Anonymous"; |
| | 0 | 36 | | ConnectedUsers.TryAdd(connectionId, new UserSessionInfo |
| | 0 | 37 | | { |
| | 0 | 38 | | UserName = userName, |
| | 0 | 39 | | ConnectedAt = DateTime.UtcNow, |
| | 0 | 40 | | CurrentPage = "Неизвестно" |
| | 0 | 41 | | }); |
| | 0 | 42 | | await base.OnConnectedAsync(); |
| | | 43 | | // Отправляем обновленный список пользователей всем клиентам |
| | 0 | 44 | | var usersList = await GetUsersWithRoles(); |
| | 0 | 45 | | await Clients.All.SendAsync("UserConnected", ConnectedUsers.Count, usersList); |
| | 0 | 46 | | } |
| | | 47 | | |
| | | 48 | | public override async Task OnDisconnectedAsync(Exception? exception) |
| | | 49 | | { |
| | 0 | 50 | | var connectionId = Context.ConnectionId; |
| | 0 | 51 | | ConnectedUsers.TryRemove(connectionId, out _); |
| | 0 | 52 | | await base.OnDisconnectedAsync(exception); |
| | | 53 | | // Отправляем обновленный список пользователей всем клиентам |
| | 0 | 54 | | var usersList = await GetUsersWithRoles(); |
| | 0 | 55 | | await Clients.All.SendAsync("UserDisconnected", ConnectedUsers.Count, usersList); |
| | 0 | 56 | | } |
| | | 57 | | |
| | | 58 | | // Метод для обновления текущей страницы пользователя |
| | | 59 | | public async Task UpdateCurrentPage(string pagePath) |
| | | 60 | | { |
| | 0 | 61 | | var connectionId = Context.ConnectionId; |
| | 0 | 62 | | if (ConnectedUsers.TryGetValue(connectionId, out var sessionInfo)) |
| | | 63 | | { |
| | 0 | 64 | | sessionInfo.CurrentPage = pagePath ?? "Неизвестно"; |
| | | 65 | | // Отправляем обновленный список всем клиентам |
| | 0 | 66 | | var usersList = await GetUsersWithRoles(); |
| | 0 | 67 | | await Clients.All.SendAsync("UsersListUpdated", usersList); |
| | | 68 | | } |
| | 0 | 69 | | } |
| | | 70 | | |
| | | 71 | | public Task<int> GetConnectedUsersCount() |
| | | 72 | | { |
| | 0 | 73 | | return Task.FromResult(ConnectedUsers.Count); |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | public Task<IEnumerable<string>> GetConnectedUsers() |
| | | 77 | | { |
| | | 78 | | // Возвращаем только уникальные имена пользователей (без анонимных) |
| | 0 | 79 | | return Task.FromResult(ConnectedUsers.Values |
| | 0 | 80 | | .Where(u => !string.IsNullOrEmpty(u.UserName) && u.UserName != "Anonymous") |
| | 0 | 81 | | .Select(u => u.UserName) |
| | 0 | 82 | | .Distinct() |
| | 0 | 83 | | .AsEnumerable()); |
| | | 84 | | } |
| | | 85 | | |
| | | 86 | | public async Task<IEnumerable<object>> GetConnectedUsersWithRoles() |
| | | 87 | | { |
| | | 88 | | // Проверяем, является ли текущий пользователь админом |
| | 0 | 89 | | var isAdmin = false; |
| | 0 | 90 | | if (Context.User?.Identity?.IsAuthenticated == true) |
| | | 91 | | { |
| | 0 | 92 | | var currentUser = await _userManager.GetUserAsync(Context.User); |
| | 0 | 93 | | if (currentUser != null) |
| | | 94 | | { |
| | 0 | 95 | | var roles = await _userManager.GetRolesAsync(currentUser); |
| | 0 | 96 | | isAdmin = roles.Contains("Admin"); |
| | | 97 | | } |
| | | 98 | | } |
| | 0 | 99 | | return await GetUsersWithRoles(isAdmin); |
| | 0 | 100 | | } |
| | | 101 | | |
| | | 102 | | private async Task<List<object>> GetUsersWithRoles(bool includeSessionInfo = false) |
| | | 103 | | { |
| | | 104 | | // Группируем по имени пользователя, берем последнюю сессию |
| | 0 | 105 | | var uniqueUserSessions = ConnectedUsers.Values |
| | | 106 | | .Where(u => !string.IsNullOrEmpty(u.UserName) && u.UserName != "Anonymous") |
| | | 107 | | .GroupBy(u => u.UserName) |
| | | 108 | | .Select(g => g.OrderByDescending(u => u.ConnectedAt).First()) |
| | 0 | 109 | | .ToList(); |
| | | 110 | | |
| | | 111 | | // Кэш только для includeSessionInfo=false (роли не меняются часто; session info меняется при UpdateCurrentP |
| | 0 | 112 | | if (!includeSessionInfo && uniqueUserSessions.Count > 0) |
| | | 113 | | { |
| | | 114 | | var cacheKey = string.Join(",", uniqueUserSessions.OrderBy(u => u.UserName).Select(u => u.UserName)); |
| | 0 | 115 | | if (UsersWithRolesCache.TryGetValue(cacheKey, out var cached) && cached.ExpiresAt > DateTime.UtcNow) |
| | | 116 | | { |
| | 0 | 117 | | return cached.Result; |
| | | 118 | | } |
| | | 119 | | } |
| | | 120 | | |
| | 0 | 121 | | var usersWithRoles = new List<object>(); |
| | | 122 | | |
| | 0 | 123 | | foreach (var sessionInfo in uniqueUserSessions) |
| | | 124 | | { |
| | | 125 | | try |
| | | 126 | | { |
| | 0 | 127 | | var user = await _userManager.FindByNameAsync(sessionInfo.UserName); |
| | 0 | 128 | | if (user != null) |
| | | 129 | | { |
| | 0 | 130 | | var roles = await _userManager.GetRolesAsync(user); |
| | 0 | 131 | | var role = roles.Contains("Admin") ? "Admin" : (roles.Contains("User") ? "User" : "None"); |
| | | 132 | | |
| | 0 | 133 | | var userInfo = new { |
| | 0 | 134 | | name = sessionInfo.UserName, |
| | 0 | 135 | | role = role |
| | 0 | 136 | | }; |
| | | 137 | | |
| | | 138 | | // Добавляем информацию о сеансе только для админов |
| | 0 | 139 | | if (includeSessionInfo) |
| | | 140 | | { |
| | 0 | 141 | | var sessionDuration = DateTime.UtcNow - sessionInfo.ConnectedAt; |
| | 0 | 142 | | var durationString = FormatDuration(sessionDuration); |
| | | 143 | | |
| | | 144 | | // Используем dynamic для добавления свойств |
| | 0 | 145 | | var dynamicUserInfo = new System.Dynamic.ExpandoObject() as IDictionary<string, object>; |
| | 0 | 146 | | dynamicUserInfo["name"] = sessionInfo.UserName; |
| | 0 | 147 | | dynamicUserInfo["role"] = role; |
| | 0 | 148 | | dynamicUserInfo["sessionDuration"] = durationString; |
| | 0 | 149 | | dynamicUserInfo["currentPage"] = sessionInfo.CurrentPage; |
| | 0 | 150 | | dynamicUserInfo["connectedAt"] = sessionInfo.ConnectedAt.ToString("yyyy-MM-dd HH:mm:ss"); |
| | | 151 | | |
| | 0 | 152 | | usersWithRoles.Add(dynamicUserInfo); |
| | | 153 | | } |
| | | 154 | | else |
| | | 155 | | { |
| | 0 | 156 | | usersWithRoles.Add(userInfo); |
| | | 157 | | } |
| | | 158 | | } |
| | | 159 | | else |
| | | 160 | | { |
| | 0 | 161 | | if (includeSessionInfo) |
| | | 162 | | { |
| | 0 | 163 | | var dynamicUserInfo = new System.Dynamic.ExpandoObject() as IDictionary<string, object>; |
| | 0 | 164 | | dynamicUserInfo["name"] = sessionInfo.UserName; |
| | 0 | 165 | | dynamicUserInfo["role"] = "None"; |
| | 0 | 166 | | dynamicUserInfo["sessionDuration"] = FormatDuration(DateTime.UtcNow - sessionInfo.ConnectedA |
| | 0 | 167 | | dynamicUserInfo["currentPage"] = sessionInfo.CurrentPage; |
| | 0 | 168 | | usersWithRoles.Add(dynamicUserInfo); |
| | | 169 | | } |
| | | 170 | | else |
| | | 171 | | { |
| | 0 | 172 | | usersWithRoles.Add(new { name = sessionInfo.UserName, role = "None" }); |
| | | 173 | | } |
| | | 174 | | } |
| | 0 | 175 | | } |
| | 0 | 176 | | catch |
| | | 177 | | { |
| | 0 | 178 | | if (includeSessionInfo) |
| | | 179 | | { |
| | 0 | 180 | | var dynamicUserInfo = new System.Dynamic.ExpandoObject() as IDictionary<string, object>; |
| | 0 | 181 | | dynamicUserInfo["name"] = sessionInfo.UserName; |
| | 0 | 182 | | dynamicUserInfo["role"] = "None"; |
| | 0 | 183 | | dynamicUserInfo["sessionDuration"] = FormatDuration(DateTime.UtcNow - sessionInfo.ConnectedAt); |
| | 0 | 184 | | dynamicUserInfo["currentPage"] = sessionInfo.CurrentPage; |
| | 0 | 185 | | usersWithRoles.Add(dynamicUserInfo); |
| | | 186 | | } |
| | | 187 | | else |
| | | 188 | | { |
| | 0 | 189 | | usersWithRoles.Add(new { name = sessionInfo.UserName, role = "None" }); |
| | | 190 | | } |
| | 0 | 191 | | } |
| | 0 | 192 | | } |
| | | 193 | | |
| | 0 | 194 | | if (!includeSessionInfo && uniqueUserSessions.Count > 0) |
| | | 195 | | { |
| | | 196 | | var cacheKey = string.Join(",", uniqueUserSessions.OrderBy(u => u.UserName).Select(u => u.UserName)); |
| | 0 | 197 | | UsersWithRolesCache.AddOrUpdate(cacheKey, |
| | 0 | 198 | | _ => (usersWithRoles, DateTime.UtcNow + CacheTtl), |
| | 0 | 199 | | (_, _) => (usersWithRoles, DateTime.UtcNow + CacheTtl)); |
| | | 200 | | // Удаляем устаревшие записи, чтобы не накапливать кэш |
| | 0 | 201 | | var now = DateTime.UtcNow; |
| | 0 | 202 | | foreach (var kvp in UsersWithRolesCache) |
| | | 203 | | { |
| | 0 | 204 | | if (kvp.Value.ExpiresAt < now) |
| | 0 | 205 | | UsersWithRolesCache.TryRemove(kvp.Key, out _); |
| | | 206 | | } |
| | | 207 | | } |
| | | 208 | | |
| | 0 | 209 | | return usersWithRoles; |
| | 0 | 210 | | } |
| | | 211 | | |
| | | 212 | | private string FormatDuration(TimeSpan duration) |
| | | 213 | | { |
| | 0 | 214 | | if (duration.TotalDays >= 1) |
| | | 215 | | { |
| | 0 | 216 | | return $"{(int)duration.TotalDays}д {duration.Hours}ч {duration.Minutes}м"; |
| | | 217 | | } |
| | 0 | 218 | | else if (duration.TotalHours >= 1) |
| | | 219 | | { |
| | 0 | 220 | | return $"{duration.Hours}ч {duration.Minutes}м"; |
| | | 221 | | } |
| | | 222 | | else |
| | | 223 | | { |
| | 0 | 224 | | return $"{duration.Minutes}м {duration.Seconds}с"; |
| | | 225 | | } |
| | | 226 | | } |
| | | 227 | | } |
| | | 228 | | } |