using Admin.NET.Core; using Furion.InstantMessaging; using Microsoft.AspNetCore.SignalR; using UAParser; using System.Security.Claims; using Admin.NET.Core.Service; using MongoDB.Driver.Core.Connections; using MaxMind.GeoIP2.Model; using Flurl.Http; using System.Security.Cryptography; namespace AAdmin.NET.Core { /// /// 小程序进入房间时间统计 /// [MapHub("/hubs/weminpro")] public class WeMinProHub : Hub { private readonly IHubContext _hubContext; private readonly SysCacheService _cache; private readonly IServiceScopeFactory _scopeFactory; // private const int MAX_MINUTES = 60; private const int WARN_MINUTES = 15; private const string CACHE_PREFIX = "MPUSER_"; public const string CACHE_ROOM = "ROOM_"; public WeMinProHub( IHubContext hubContext, SysCacheService cache, IServiceScopeFactory scopeFactory) { _hubContext = hubContext; _cache = cache; _scopeFactory = scopeFactory; } /// /// 用户连接时,首次上线或重连 /// /// public override async Task OnConnectedAsync() { var context = Context.GetHttpContext(); var token = context.Request.Query["access_token"]; var roomId = context.Request.Query["roomId"]; var claims = JWTEncryption.ReadJwtToken(token)?.Claims; var client = Parser.GetDefault().Parse(context.Request.Headers["User-Agent"]); var userIdStr = claims?.FirstOrDefault(c => c.Type == ClaimConst.UserId)?.Value; var tenantIdStr = claims?.FirstOrDefault(c => c.Type == ClaimConst.TenantId)?.Value; if (!long.TryParse(userIdStr, out var userId)) return; var tenantId = string.IsNullOrWhiteSpace(tenantIdStr) ? 0 : 1300000000001; // 检查缓存中是否已有在线用户信息 var cacheKey = CACHE_PREFIX + userId; var cachedUser = _cache.Get(cacheKey); if (cachedUser == null) { using var scope = _scopeFactory.CreateScope(); var _onlineUserService = scope.ServiceProvider.GetRequiredService>(); var user = await _onlineUserService.GetByIdAsync(userId); if (user == null) { user = new SysOnlineUser { UserId = userId, TenantId = tenantId, UserName = claims?.FirstOrDefault(c => c.Type == ClaimConst.Account)?.Value, RealName = claims?.FirstOrDefault(c => c.Type == ClaimConst.RealName)?.Value, Time = DateTime.Now, ConnectionId = Context.ConnectionId, Ip = context.Connection.RemoteIpAddress?.MapToIPv4().ToString(), Browser = client.UA.Family + client.UA.Major, Os = client.OS.Family + client.OS.Major, TotalOnlineSeconds = 0 }; await _onlineUserService.InsertAsync(user); } else { if (user.LastOfflineTime.HasValue) { var lastOnlineSpan = (user.LastOfflineTime.Value - user.Time.GetValueOrDefault()).TotalSeconds; user.TotalOnlineSeconds += (int)Math.Max(0, lastOnlineSpan); } user.ConnectionId = Context.ConnectionId; user.LastOfflineTime = null; await _onlineUserService.UpdateAsync(user); } _cache.Set(cacheKey, user, TimeSpan.FromHours(1)); cachedUser = user; var _weChatUserScope = scope.ServiceProvider.GetRequiredService>(); var wechatUserinfo = await _weChatUserScope.GetFirstAsync(c => c.WxId == user.UserId); if (wechatUserinfo != null) { _cache.Set("WxVIP_" + user.UserId, wechatUserinfo.IsVIP, TimeSpan.FromHours(1)); } if (!_cache.ExistKey(CACHE_ROOM + user.UserId)) { _cache.Set(CACHE_ROOM + user.UserId, roomId, TimeSpan.FromHours(1)); } } // 启动监控线程 // _ = MonitorUserOnlineAsync(cachedUser); await base.OnConnectedAsync(); } /// /// 退出房间 /// /// /// [NonAction] private async Task OfflineRoom(string userId) { var roomId = _cache.Get(CACHE_ROOM + userId); if (!string.IsNullOrWhiteSpace(roomId)) { await "https://aigc.ycymedu.com/api/proxyaigc?Name=stop&Action=StopVoiceChat&Version=2024-12-01".PostJsonAsync(new WeMinProOfficeDto() { AppId = "67e11a296ff39301ed7429aa", RoomId = roomId, TaskId = userId }); _cache.Remove(CACHE_ROOM + userId); } } public override async Task OnDisconnectedAsync(Exception exception) { var context = Context.GetHttpContext(); var token = context.Request.Query["access_token"]; var claims = JWTEncryption.ReadJwtToken(token)?.Claims; var userIdStr = claims?.FirstOrDefault(c => c.Type == ClaimConst.UserId)?.Value; if (!long.TryParse(userIdStr, out var userId)) return; var connectionId = Context.ConnectionId; _cache.Remove(CACHE_PREFIX + userId); using (var scope = _scopeFactory.CreateScope()) { var _onlineUserService = scope.ServiceProvider.GetRequiredService>(); var user = await _onlineUserService.GetFirstAsync(c => c.UserId == userId); if (user != null) { var onlineSpan = (DateTime.Now - user.Time.GetValueOrDefault()).TotalSeconds; user.TotalOnlineSeconds += (int)onlineSpan; user.LastOfflineTime = DateTime.Now; await _onlineUserService.UpdateAsync(user); } } await base.OnDisconnectedAsync(exception); } /// /// 强制下线用户 /// /// /// //public async Task ForceOffline(OnlineUserHubInput input) //{ // var context = Context.GetHttpContext(); // var token = context.Request.Query["access_token"]; // var claims = JWTEncryption.ReadJwtToken(token)?.Claims; // var userIdStr = claims?.FirstOrDefault(c => c.Type == ClaimConst.UserId)?.Value; // if (!long.TryParse(userIdStr, out var userId)) return; // _cache.Remove(CACHE_PREFIX + input.ConnectionId); // using (var scope = _scopeFactory.CreateScope()) // { // var _onlineUserService = scope.ServiceProvider.GetRequiredService>(); // var user = await _onlineUserService.GetFirstAsync(c => c.ConnectionId == input.ConnectionId); // if (user != null) // { // user.LastOfflineTime = DateTime.Now; // await _onlineUserService.UpdateAsync(user); // } // await OfflineRoom(user.UserId.ToString()); // await _hubContext.Clients.Client(input.ConnectionId).ForceOffline("已强制退出"); // } //} /// /// 监控用户在线时长并提醒(此处后面可将用户vip信息写入redis缓存) /// /// /// private async Task MonitorUserOnlineAsync(SysOnlineUser user) { var connectionId = user.ConnectionId; var isVip = _cache.Get("WxVIP_" + user.UserId) ?? false; var dbWriteKey = "DbWrite_" + user.UserId; var writeDbInterval = TimeSpan.FromMinutes(1); // 最小数据库写入间隔 var lastDbWrite = DateTime.Now; var pingKey = $"{CACHE_PREFIX}{user.UserId}_ping"; DingDingHook.DingTalkHookMessage("weminHub", $"CACHE_PREFIX:{CACHE_PREFIX + user.UserId};ExistKey:{_cache.ExistKey(CACHE_PREFIX + user.UserId)}"); while (_cache.ExistKey(CACHE_PREFIX + user.UserId)) { var cachedUser = _cache.Get(CACHE_PREFIX + user.UserId); DingDingHook.DingTalkHookMessage("weminHub0", $"cachedUser:" + cachedUser.ToJson()); if (cachedUser == null) break; using (var scope = _scopeFactory.CreateScope()) { var _onlineUserService = scope.ServiceProvider.GetRequiredService>(); var userInDb = await _onlineUserService.GetFirstAsync(c => c.UserId == user.UserId); if (userInDb == null) break; // 检查心跳时间 var lastPing = _cache.Get(pingKey); DingDingHook.DingTalkHookMessage("weminHub0", $"TotalSeconds:" + (DateTime.Now - lastPing).TotalSeconds); if (lastPing != DateTime.MinValue && (DateTime.Now - lastPing).TotalSeconds > 10) { // 掉线处理 await _hubContext.Clients.Client(connectionId).ForceOffline("长时间未响应,系统自动下线"); _cache.Remove(CACHE_PREFIX + user.UserId); _cache.Remove(pingKey); // 计算本次在线时长 var currentOnlineSpan = (DateTime.Now - userInDb.Time.GetValueOrDefault()).TotalSeconds; userInDb.LastOfflineTime = DateTime.Now; // 更新总在线时长 userInDb.TotalOnlineSeconds += (int)currentOnlineSpan; // 更新用户上线时间为当前时间,避免重复计算 userInDb.Time = DateTime.Now; await _onlineUserService.UpdateAsync(userInDb); await OfflineRoom(userInDb.UserId.ToString()); break; } // 计算本次新增的在线时长 var newOnlineSpan = (DateTime.Now - (userInDb.Time ?? DateTime.Now)).TotalSeconds; cachedUser.TotalOnlineSeconds = userInDb.TotalOnlineSeconds + (int)newOnlineSpan; cachedUser.Time = userInDb.Time; // 先判断缓存项是否存在,不存在时才设置 if (!_cache.ExistKey(CACHE_PREFIX + user.UserId)) { _cache.Set(CACHE_PREFIX + user.UserId, cachedUser, TimeSpan.FromHours(1)); } var totalMinutes = cachedUser.TotalOnlineSeconds / 60.0; int MAX_MINUTES = isVip ? 30 : 1; if (totalMinutes >= MAX_MINUTES) { // DingDingHook.DingTalkHookMessage("weminHub", $"totalMinutes:" + totalMinutes); await _hubContext.Clients.Client(connectionId).ForceOffline("您的体验时间已到,系统自动下线"); _cache.Remove(CACHE_PREFIX + user.UserId); _cache.Remove(pingKey); userInDb.LastOfflineTime = DateTime.Now; userInDb.TotalOnlineSeconds = cachedUser.TotalOnlineSeconds; userInDb.Time = DateTime.Now; // 更新时间 await _onlineUserService.UpdateAsync(userInDb); break; } else if (totalMinutes >= WARN_MINUTES) { await _hubContext.Clients.Client(connectionId).SendWarn("您即将被系统下线!"); } // 每5分钟写一次数据库 if ((DateTime.Now - lastDbWrite) >= writeDbInterval) { userInDb.TotalOnlineSeconds = cachedUser.TotalOnlineSeconds; userInDb.Time = DateTime.Now; // 更新时间 await _onlineUserService.UpdateAsync(userInDb); lastDbWrite = DateTime.Now; } } // DingDingHook.DingTalkHookMessage("Ping", $"MonitorUserOnline:{Context.ConnectionId}|Ping:" + DateTime.Now.ToString("yyyyMMddHHmmssfff")); await Task.Delay(TimeSpan.FromSeconds(5)); // 每5秒检查一次 } } /// /// 心跳 /// 缓存中存在,则更新时间,否则删除 /// /// public async Task Ping() { var context = Context.GetHttpContext(); var token = context.Request.Query["access_token"]; var claims = JWTEncryption.ReadJwtToken(token)?.Claims; var userIdStr = claims?.FirstOrDefault(c => c.Type == ClaimConst.UserId)?.Value; if (!long.TryParse(userIdStr, out var userId)) return; // DingDingHook.DingTalkHookMessage("Ping", $"ConnectionId:{Context.ConnectionId}|Ping:" + DateTime.Now.ToString()); var key = $"{CACHE_PREFIX}{userId}_ping"; _cache.Set(key, DateTime.Now, TimeSpan.FromMinutes(2)); } /// /// 发送信息给小程序用户 /// /// /// public async Task ClientsSendMessage(MinProMessageInput message) { await _hubContext.Clients.Client(message.ConnectionId).SendWarn(message.Content); } } }