diff --git a/Infrastructure/Log.cs b/Infrastructure/Log.cs new file mode 100644 index 0000000..309b544 --- /dev/null +++ b/Infrastructure/Log.cs @@ -0,0 +1,14 @@ +using System; + +namespace Infrastructure +{ + public class Log + { + public static void WriteLine(ConsoleColor color = ConsoleColor.Black, string msg = "") + { + Console.ForegroundColor = color; + Console.WriteLine($"{DateTime.Now} {msg}"); + Console.ResetColor(); + } + } +} diff --git a/Infrastructure/OptionsSetting.cs b/Infrastructure/OptionsSetting.cs index e451fee..7befae4 100644 --- a/Infrastructure/OptionsSetting.cs +++ b/Infrastructure/OptionsSetting.cs @@ -1,6 +1,4 @@ - -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Infrastructure { @@ -9,6 +7,10 @@ namespace Infrastructure /// public class OptionsSetting { + /// + /// 是否单点登录 + /// + public bool SingleLogin { get; set; } /// /// 是否演示模式 /// diff --git a/ZR.Admin.WebApi/Extensions/AppServiceExtensions.cs b/Infrastructure/WebExtensins/AppServiceExtensions.cs similarity index 94% rename from ZR.Admin.WebApi/Extensions/AppServiceExtensions.cs rename to Infrastructure/WebExtensins/AppServiceExtensions.cs index 466f372..ce694fa 100644 --- a/ZR.Admin.WebApi/Extensions/AppServiceExtensions.cs +++ b/Infrastructure/WebExtensins/AppServiceExtensions.cs @@ -1,8 +1,10 @@ -using Infrastructure; -using Infrastructure.Attribute; +using Infrastructure.Attribute; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; using System.Reflection; -namespace ZR.Admin.WebApi.Extensions +namespace Infrastructure { /// /// App服务注册 diff --git a/Infrastructure/Extensions/HttpContextExtension.cs b/Infrastructure/WebExtensins/HttpContextExtension.cs similarity index 98% rename from Infrastructure/Extensions/HttpContextExtension.cs rename to Infrastructure/WebExtensins/HttpContextExtension.cs index 28bf657..fabb64b 100644 --- a/Infrastructure/Extensions/HttpContextExtension.cs +++ b/Infrastructure/WebExtensins/HttpContextExtension.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using Infrastructure.Extensions; +using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; using System.IO; @@ -8,7 +9,7 @@ using System.Text; using System.Text.RegularExpressions; using UAParser; -namespace Infrastructure.Extensions +namespace Infrastructure.WebExtensins { /// /// HttpContext扩展类 diff --git a/ZR.Admin.WebApi/Controllers/BaseController.cs b/ZR.Admin.WebApi/Controllers/BaseController.cs index fb72640..bf44474 100644 --- a/ZR.Admin.WebApi/Controllers/BaseController.cs +++ b/ZR.Admin.WebApi/Controllers/BaseController.cs @@ -1,5 +1,4 @@ -using Infrastructure.Extensions; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; diff --git a/ZR.Admin.WebApi/Controllers/System/SysLoginController.cs b/ZR.Admin.WebApi/Controllers/System/SysLoginController.cs index bb52ce6..1cf7d76 100644 --- a/ZR.Admin.WebApi/Controllers/System/SysLoginController.cs +++ b/ZR.Admin.WebApi/Controllers/System/SysLoginController.cs @@ -2,10 +2,7 @@ using Lazy.Captcha.Core; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; -using SqlSugar; -using System.Diagnostics; using UAParser; -using ZR.Admin.WebApi.Extensions; using ZR.Admin.WebApi.Filters; using ZR.Admin.WebApi.Framework; using ZR.Model.System; @@ -31,7 +28,7 @@ namespace ZR.Admin.WebApi.Controllers.System private readonly ICaptcha SecurityCodeHelper; private readonly ISysConfigService sysConfigService; private readonly ISysRoleService roleService; - private readonly OptionsSetting jwtSettings; + private readonly OptionsSetting optionSettings; public SysLoginController( IHttpContextAccessor contextAccessor, @@ -42,7 +39,7 @@ namespace ZR.Admin.WebApi.Controllers.System ISysConfigService configService, ISysRoleService sysRoleService, ICaptcha captcha, - IOptions jwtSettings) + IOptions optionSettings) { httpContextAccessor = contextAccessor; SecurityCodeHelper = captcha; @@ -52,7 +49,7 @@ namespace ZR.Admin.WebApi.Controllers.System this.permissionService = permissionService; this.sysConfigService = configService; roleService = sysRoleService; - this.jwtSettings = jwtSettings.Value; + this.optionSettings = optionSettings.Value; } @@ -82,7 +79,6 @@ namespace ZR.Admin.WebApi.Controllers.System { return ToResponse(ResultCode.LOGIN_ERROR, $"你的账号已被锁,剩余{Math.Round(ts.TotalMinutes, 0)}分钟"); } - var user = sysLoginService.Login(loginBody, RecordLogInfo(httpContextAccessor.HttpContext)); List roles = roleService.SelectUserRoleListByUserId(user.UserId); @@ -91,7 +87,7 @@ namespace ZR.Admin.WebApi.Controllers.System LoginUser loginUser = new(user, roles, permissions); CacheService.SetUserPerms(GlobalConstant.UserPermKEY + user.UserId, permissions); - return SUCCESS(JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser), jwtSettings.JwtSettings)); + return SUCCESS(JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser), optionSettings.JwtSettings)); } /// @@ -131,6 +127,9 @@ namespace ZR.Admin.WebApi.Controllers.System //权限集合 eg *:*:*,system:user:list List permissions = permissionService.GetMenuPermission(user); user.WelcomeContent = GlobalConstant.WelcomeMessages[new Random().Next(0, GlobalConstant.WelcomeMessages.Length)]; + + //LoginUser loginUser = new(user, roleService.SelectUserRoleListByUserId(user.UserId), permissions); + //var token = JwtUtil.GenerateJwtToken(JwtUtil.AddClaims(loginUser), optionSettings.JwtSettings); return SUCCESS(new { user, roles, permissions }); } @@ -300,5 +299,6 @@ namespace ZR.Admin.WebApi.Controllers.System return ToResponse(ResultCode.FAIL, "二维码已失效"); } #endregion + } } diff --git a/ZR.Admin.WebApi/Controllers/System/monitor/SysUserOnlineController.cs b/ZR.Admin.WebApi/Controllers/System/monitor/SysUserOnlineController.cs index b51eba2..71e3d87 100644 --- a/ZR.Admin.WebApi/Controllers/System/monitor/SysUserOnlineController.cs +++ b/ZR.Admin.WebApi/Controllers/System/monitor/SysUserOnlineController.cs @@ -30,11 +30,11 @@ namespace ZR.Admin.WebApi.Controllers.monitor [HttpGet("list")] public IActionResult Index([FromQuery] PagerInfo parm) { - var result = MessageHub.clientUsers + var result = MessageHub.onlineClients .OrderByDescending(f => f.LoginTime) .Skip(parm.PageNum - 1).Take(parm.PageSize); - return SUCCESS(new { result, totalNum = MessageHub.clientUsers.Count }); + return SUCCESS(new { result, totalNum = MessageHub.onlineClients.Count }); } /// diff --git a/ZR.Admin.WebApi/Extensions/LogoExtension.cs b/ZR.Admin.WebApi/Extensions/LogoExtension.cs index ff908ce..71f86e1 100644 --- a/ZR.Admin.WebApi/Extensions/LogoExtension.cs +++ b/ZR.Admin.WebApi/Extensions/LogoExtension.cs @@ -1,7 +1,6 @@ using JinianNet.JNTemplate; -using ZR.Common; -namespace ZR.Admin.WebApi.Extensions +namespace Infrastructure { public static class LogoExtension { diff --git a/ZR.Admin.WebApi/GlobalUsing.cs b/ZR.Admin.WebApi/GlobalUsing.cs index 37d1b22..2bdf8f8 100644 --- a/ZR.Admin.WebApi/GlobalUsing.cs +++ b/ZR.Admin.WebApi/GlobalUsing.cs @@ -5,4 +5,5 @@ global using Infrastructure.Attribute; global using Infrastructure.Enums; global using Infrastructure.Model; global using Mapster; -global using Infrastructure.Extensions; \ No newline at end of file +global using Infrastructure.Extensions; +global using Infrastructure.WebExtensins; \ No newline at end of file diff --git a/ZR.Admin.WebApi/Hubs/MessageHub.cs b/ZR.Admin.WebApi/Hubs/MessageHub.cs index 6e6069f..a0ba360 100644 --- a/ZR.Admin.WebApi/Hubs/MessageHub.cs +++ b/ZR.Admin.WebApi/Hubs/MessageHub.cs @@ -13,8 +13,9 @@ namespace ZR.Admin.WebApi.Hubs public class MessageHub : Hub { //创建用户集合,用于存储所有链接的用户数据 - public static readonly List clientUsers = new(); - private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + public static readonly List onlineClients = new(); + public static List users = new(); + //private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private readonly ISysNoticeService SysNoticeService; public MessageHub(ISysNoticeService noticeService) @@ -43,14 +44,13 @@ namespace ZR.Admin.WebApi.Hubs ClientInfo clientInfo = HttpContextExtension.GetClientInfo(App.HttpContext); string device = clientInfo.ToString(); - var qs = HttpContextExtension.GetQueryString(App.HttpContext); - + string qs = HttpContextExtension.GetQueryString(App.HttpContext); string from = HttpUtility.ParseQueryString(qs).Get("from") ?? "web"; - + long userid = HttpContextExtension.GetUId(App.HttpContext); string uuid = device + userid + ip; - var user = clientUsers.Any(u => u.ConnnectionId == Context.ConnectionId); - var user2 = clientUsers.Any(u => u.Uuid == uuid); + var user = onlineClients.Any(u => u.ConnnectionId == Context.ConnectionId); + var user2 = onlineClients.Any(u => u.Uuid == uuid); //判断用户是否存在,否则添加集合!user2 && !user && if (!user2 && !user && Context.User.Identity.IsAuthenticated) @@ -61,14 +61,41 @@ namespace ZR.Admin.WebApi.Hubs Uuid = uuid, Platform = from }; - clientUsers.Add(onlineUser); - Console.WriteLine($"{DateTime.Now}:{name},{Context.ConnectionId}连接服务端success,当前已连接{clientUsers.Count}个"); + onlineClients.Add(onlineUser); + Log.WriteLine(msg: $"{DateTime.Now}:{name},{Context.ConnectionId}连接服务端success,当前已连接{onlineClients.Count}个"); //Clients.All.SendAsync("welcome", $"欢迎您:{name},当前时间:{DateTime.Now}"); Clients.Caller.SendAsync(HubsConstant.MoreNotice, SendNotice()); Clients.Caller.SendAsync(HubsConstant.ConnId, onlineUser.ConnnectionId); } + OnlineUsers? userInfo = GetUserById(userid); + if (userInfo == null) + { + userInfo = new OnlineUsers() { Userid = userid, Name = name, LoginTime = DateTime.Now }; + users.Add(userInfo); + } + else + { + if (userInfo.LoginTime <= Convert.ToDateTime(DateTime.Now.ToShortDateString())) + { + userInfo.LoginTime = DateTime.Now; + userInfo.TodayOnlineTime = 0; + } + var clientUser = onlineClients.Find(x => x.Userid == userid); + userInfo.TodayOnlineTime += clientUser?.OnlineTime ?? 0; + } + //给当前所有登录当前账号的用户下发登录时长 + var connIds = onlineClients.Where(f => f.Userid == userid).ToList(); + userInfo.ClientNum = connIds.Count; - Clients.Caller.SendAsync(HubsConstant.OnlineNum, clientUsers.Count); + Clients.Clients(connIds.Select(f => f.ConnnectionId)).SendAsync("onlineInfo", userInfo); + + Log.WriteLine(ConsoleColor.Blue, msg: $"用户{name}已连接,今日已在线{userInfo?.TodayOnlineTime}分钟,当前已连接{onlineClients.Count}个"); + //给所有用户更新在线人数 + Clients.All.SendAsync(HubsConstant.OnlineNum, new + { + num = onlineClients.Count, + onlineClients + }); return base.OnConnectedAsync(); } @@ -78,14 +105,25 @@ namespace ZR.Admin.WebApi.Hubs /// public override Task OnDisconnectedAsync(Exception? exception) { - var user = clientUsers.Where(p => p.ConnnectionId == Context.ConnectionId).FirstOrDefault(); - //判断用户是否存在,否则添加集合 + var user = onlineClients.Where(p => p.ConnnectionId == Context.ConnectionId).FirstOrDefault(); if (user != null) { - clientUsers.Remove(user); - Clients.All.SendAsync(HubsConstant.OnlineNum, clientUsers.Count); + onlineClients.Remove(user); + //给所有用户更新在线人数 + Clients.All.SendAsync(HubsConstant.OnlineNum, new + { + num = onlineClients.Count, + onlineClients, + leaveUser = user + }); - Console.WriteLine($"用户{user?.Name}离开了,当前已连接{clientUsers.Count}个"); + //累计用户时长 + OnlineUsers? userInfo = GetUserById(user.Userid); + if (userInfo != null) + { + userInfo.TodayOnlineTime += user?.OnlineTime ?? 0; + } + Log.WriteLine(ConsoleColor.Green, msg: $"用户{user?.Name}离开了,已在线{userInfo?.TodayOnlineTime}分,当前已连接{onlineClients.Count}个"); } return base.OnDisconnectedAsync(exception); } @@ -95,20 +133,54 @@ namespace ZR.Admin.WebApi.Hubs /// /// 发送信息 /// - /// - /// + /// 对方链接id + /// /// /// - [HubMethodName("SendMessage")] - public async Task SendMessage(string connectId, string userName, string message) + [HubMethodName("sendMessage")] + public async Task SendMessage(string toConnectId, long toUserId, string message) { - Console.WriteLine($"{connectId},message={message}"); + var userName = HttpContextExtension.GetName(App.HttpContext); + long userid = HttpContextExtension.GetUId(App.HttpContext); + var toUserList = onlineClients.Where(p => p.Userid == toUserId); + var toUserInfo = toUserList.FirstOrDefault(); + IList sendToUser = toUserList.Select(x => x.ConnnectionId).ToList(); + sendToUser.Add(GetConnectId()); + if (toUserInfo != null) + { + await Clients.Clients(sendToUser) + .SendAsync("receiveChat", new + { + msgType = 0,//文本 + chatid = Guid.NewGuid().ToString(), + userName, + userid, + toUserName = toUserInfo.Name, + toUserid = toUserInfo.Userid, + message, + chatTime = DateTime.Now + }); + } + else + { + //TODO 存储离线消息 + Console.WriteLine($"{toUserId}不在线"); + } - await Clients.Client(connectId).SendAsync("receiveChat", new { userName, message }); + Console.WriteLine($"用户{userName}对{toConnectId}-{toUserId}说:{message}"); + } + + private OnlineUsers GetUserByConnId(string connId) + { + return onlineClients.Where(p => p.ConnnectionId == connId).FirstOrDefault(); + } + private static OnlineUsers? GetUserById(long userid) + { + return users.Where(f => f.Userid == userid).FirstOrDefault(); } /// - /// 获取链接id + /// 移动端使用获取链接id /// /// [HubMethodName("getConnId")] @@ -116,5 +188,22 @@ namespace ZR.Admin.WebApi.Hubs { return Context.ConnectionId; } + + /// + /// 退出其他设备登录 + /// + /// + [HubMethodName("logOut")] + public async Task LogOut() + { + var singleLogin = AppSettings.Get("singleLogin"); + long userid = HttpContextExtension.GetUId(App.HttpContext); + if (singleLogin) + { + var onlineUsers = onlineClients.Where(p => p.ConnnectionId != Context.ConnectionId && p.Userid == userid); + await Clients.Clients(onlineUsers.Select(x => x.ConnnectionId)) + .SendAsync("logOut"); + } + } } } diff --git a/ZR.Admin.WebApi/Hubs/OnlineUsers.cs b/ZR.Admin.WebApi/Hubs/OnlineUsers.cs index 81aed2d..2af9fb7 100644 --- a/ZR.Admin.WebApi/Hubs/OnlineUsers.cs +++ b/ZR.Admin.WebApi/Hubs/OnlineUsers.cs @@ -9,10 +9,10 @@ /// /// 用户id /// - public long? Userid { get; set; } + public long Userid { get; set; } public string Name { get; set; } public DateTime LoginTime { get; set; } - public string UserIP { get; set; } + public string? UserIP { get; set; } /// /// 登录地点 /// @@ -21,17 +21,41 @@ /// /// 判断用户唯一 /// - public string? Uuid{ get; set; } + public string? Uuid { get; set; } /// /// 浏览器 /// - public string Browser { get; set; } + public string? Browser { get; set; } /// /// 平台 /// - public string Platform { get; set; } = string.Empty; - - public OnlineUsers(string clientid, string name, long? userid, string userip, string browser) + public string? Platform { get; set; } = string.Empty; + /// + /// 在线时长 + /// + public double OnlineTime + { + get + { + var ts = DateTime.Now - LoginTime; + return Math.Round(ts.TotalMinutes, 2); + } + } + /// + /// 今日在线时长 + /// + public double TodayOnlineTime { get; set; } + /// + /// 在线设备数 + /// + public int ClientNum { get; set; } + /// + /// + /// + public OnlineUsers() + { + } + public OnlineUsers(string clientid, string name, long userid, string userip, string browser) { ConnnectionId = clientid; Name = name; diff --git a/ZR.Admin.WebApi/appsettings.json b/ZR.Admin.WebApi/appsettings.json index 9a3cf89..2e10095 100644 --- a/ZR.Admin.WebApi/appsettings.json +++ b/ZR.Admin.WebApi/appsettings.json @@ -32,9 +32,10 @@ "Expire": 1440 //jwt登录过期时间(分) }, "InjectClass": [ "ZR.Repository", "ZR.Service", "ZR.Tasks" ], //自动注入类 - "ShowDbLog": true,//是否打印db日志 + "ShowDbLog": true, //是否打印db日志 "InitDb": false, //是否初始化db "DemoMode": false, //是否演示模式 + "SingleLogin": false,//是否单点登录 "Upload": { "uploadUrl": "http://localhost:8888", //本地存储资源访问路径 "localSavePath": "", //本地上传默认文件存储目录 wwwroot @@ -128,12 +129,7 @@ "Period": "3s", "Limit": 1 } - ], - "IpRateLimitPolicies": { - //ip规则 - "IpRules": [ - ] - } + ] }, //验证码配置 "CaptchaOptions": {