319 lines
14 KiB
C#
319 lines
14 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 小程序进入房间时间统计
|
||
/// </summary>
|
||
[MapHub("/hubs/weminpro")]
|
||
public class WeMinProHub : Hub<IWeMinProHub>
|
||
{
|
||
private readonly IHubContext<WeMinProHub, IWeMinProHub> _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<WeMinProHub, IWeMinProHub> hubContext,
|
||
SysCacheService cache,
|
||
IServiceScopeFactory scopeFactory)
|
||
{
|
||
_hubContext = hubContext;
|
||
_cache = cache;
|
||
_scopeFactory = scopeFactory;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户连接时,首次上线或重连
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
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<SysOnlineUser>(cacheKey);
|
||
|
||
if (cachedUser == null)
|
||
{
|
||
using var scope = _scopeFactory.CreateScope();
|
||
var _onlineUserService = scope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysOnlineUser>>();
|
||
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<SqlSugarRepository<SysWeChatUserExtend>>();
|
||
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();
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 退出房间
|
||
/// </summary>
|
||
/// <param name="userId"></param>
|
||
/// <returns></returns>
|
||
[NonAction]
|
||
private async Task OfflineRoom(string userId)
|
||
{
|
||
var roomId = _cache.Get<string>(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<SqlSugarRepository<SysOnlineUser>>();
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 强制下线用户
|
||
/// </summary>
|
||
/// <param name="input"></param>
|
||
/// <returns></returns>
|
||
//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<SqlSugarRepository<SysOnlineUser>>();
|
||
// 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("已强制退出");
|
||
// }
|
||
|
||
//}
|
||
|
||
/// <summary>
|
||
/// 监控用户在线时长并提醒(此处后面可将用户vip信息写入redis缓存)
|
||
/// </summary>
|
||
/// <param name="user"></param>
|
||
/// <returns></returns>
|
||
private async Task MonitorUserOnlineAsync(SysOnlineUser user)
|
||
{
|
||
var connectionId = user.ConnectionId;
|
||
var isVip = _cache.Get<bool?>("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<SysOnlineUser>(CACHE_PREFIX + user.UserId);
|
||
DingDingHook.DingTalkHookMessage("weminHub0", $"cachedUser:" + cachedUser.ToJson());
|
||
if (cachedUser == null) break;
|
||
|
||
using (var scope = _scopeFactory.CreateScope())
|
||
{
|
||
var _onlineUserService = scope.ServiceProvider.GetRequiredService<SqlSugarRepository<SysOnlineUser>>();
|
||
var userInDb = await _onlineUserService.GetFirstAsync(c => c.UserId == user.UserId);
|
||
if (userInDb == null) break;
|
||
|
||
// 检查心跳时间
|
||
var lastPing = _cache.Get<DateTime>(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秒检查一次
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 心跳
|
||
/// 缓存中存在,则更新时间,否则删除
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送信息给小程序用户
|
||
/// </summary>
|
||
/// <param name="message"></param>
|
||
/// <returns></returns>
|
||
public async Task ClientsSendMessage(MinProMessageInput message)
|
||
{
|
||
await _hubContext.Clients.Client(message.ConnectionId).SendWarn(message.Content);
|
||
}
|
||
}
|
||
}
|