diff --git a/ZR.Admin.WebApi/Hubs/MessageHub.cs b/ZR.Admin.WebApi/Hubs/MessageHub.cs new file mode 100644 index 0000000..49451fc --- /dev/null +++ b/ZR.Admin.WebApi/Hubs/MessageHub.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Infrastructure.Model; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using ZR.Admin.WebApi.Filters; +using ZR.Model; + +namespace ZR.Admin.WebApi.Hubs +{ + [Verify] + public class MessageHub : Hub + { + //创建用户集合,用于存储所有链接的用户数据 + private static readonly List clientUsers = new(); + + #region 客户端连接 + + /// + /// 客户端连接的时候调用 + /// + /// + public override Task OnConnectedAsync() + { + //name 获取不到有待研究 + var name = Context.User.Identity.Name; + var user = clientUsers.Any(u => u.ConnnectionId == Context.ConnectionId); + //判断用户是否存在,否则添加集合 + if (!user) + { + clientUsers.Add(new OnlineUsers(Context.ConnectionId, Context.User.Identity.Name)); + Console.WriteLine($"{DateTime.Now}:{Context.User.Identity.Name},{Context.ConnectionId}连接服务端success,当前已连接{clientUsers.Count}个"); + } + + Clients.All.SendAsync("onlineNum", clientUsers.Count); + return base.OnConnectedAsync(); + } + + /// + /// 连接终止时调用。 + /// + /// + public override Task OnDisconnectedAsync(Exception exception) + { + var user = clientUsers.Where(p => p.ConnnectionId == Context.ConnectionId).FirstOrDefault(); + //判断用户是否存在,否则添加集合 + if (user != null) + { + Console.WriteLine($"用户{user?.Name}离开了,当前已连接{clientUsers.Count}个"); + clientUsers.Remove(user); + + Clients.All.SendAsync("onlineNum", clientUsers.Count); + } + return base.OnDisconnectedAsync(exception); + } + + #endregion + } +} diff --git a/ZR.Admin.WebApi/Hubs/OnlineUsers.cs b/ZR.Admin.WebApi/Hubs/OnlineUsers.cs new file mode 100644 index 0000000..78aefe2 --- /dev/null +++ b/ZR.Admin.WebApi/Hubs/OnlineUsers.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ZR.Model +{ + public class OnlineUsers + { + /// + /// 客户端连接Id + /// + public string ConnnectionId { get; set; } + /// + /// 用户id + /// + public int Userid { get; set; } + public string Name { get; set; } + public DateTime LoginTime { get; set; } + + public OnlineUsers(string clientid, string name) + { + ConnnectionId = clientid; + Name = name; + LoginTime = DateTime.Now; + } + } +} diff --git a/ZR.Admin.WebApi/Startup.cs b/ZR.Admin.WebApi/Startup.cs index 1eb0689..82887e2 100644 --- a/ZR.Admin.WebApi/Startup.cs +++ b/ZR.Admin.WebApi/Startup.cs @@ -15,8 +15,8 @@ using System.Threading.Tasks; using ZR.Admin.WebApi.Extensions; using ZR.Admin.WebApi.Filters; using ZR.Admin.WebApi.Framework; +using ZR.Admin.WebApi.Hubs; using ZR.Admin.WebApi.Middleware; -using ZR.Common.Cache; namespace ZR.Admin.WebApi { @@ -43,6 +43,14 @@ namespace ZR.Admin.WebApi .AllowAnyMethod();//ⷽ }); }); + //עSignalRʵʱͨѶĬjson + services.AddSignalR(options => + { + //ͻ˷󵽷Ĭ30룬ij4ӣҳconnection.keepAliveIntervalInMilliseconds = 12e4;2 + //options.ClientTimeoutInterval = TimeSpan.FromMinutes(4); + //˷󵽿ͻ˼Ĭ15룬ij2ӣҳconnection.serverTimeoutInMilliseconds = 24e4;4 + //options.KeepAliveInterval = TimeSpan.FromMinutes(2); + }); //Error unprotecting the session cookie services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection")); @@ -96,7 +104,7 @@ namespace ZR.Admin.WebApi }); //ʾ̬ļ/wwwrootĿ¼ļҪUseRoutingǰ app.UseStaticFiles(); - + //·ɷ app.UseRouting(); app.UseCors("Policy");//Ҫapp.UseEndpointsǰ @@ -107,12 +115,13 @@ namespace ZR.Admin.WebApi app.UseAuthentication(); //2.ٿȨ app.UseAuthorization(); + //session app.UseSession(); + // app.UseResponseCaching(); - - // ָ/ + //ָ/ app.UseAddTaskSchedulers(); - + //ʹȫ쳣м app.UseMiddleware(); app.UseEndpoints(endpoints => @@ -120,6 +129,9 @@ namespace ZR.Admin.WebApi endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); + + //socket + endpoints.MapHub("/msgHub"); }); } diff --git a/ZR.Vue/.env.development b/ZR.Vue/.env.development index 8616d91..349a4c3 100644 --- a/ZR.Vue/.env.development +++ b/ZR.Vue/.env.development @@ -7,6 +7,9 @@ VUE_APP_TITLE = 'ZrAdmin.NET后台管理' # 开发环境 VUE_APP_BASE_API = '/dev-api' +#socket API +VUE_APP_SOCKET_API = '/msgHub' + # 路由前缀 VUE_APP_ROUTER_PREFIX = '/' diff --git a/ZR.Vue/.env.production b/ZR.Vue/.env.production index 7158a65..9a8657a 100644 --- a/ZR.Vue/.env.production +++ b/ZR.Vue/.env.production @@ -7,8 +7,11 @@ VUE_APP_TITLE = 'ZrAdmin.NET后台管理' # 生产环境 VUE_APP_BASE_API = '/prod-api' +#socket API +VUE_APP_SOCKET_API = '/msgHub' + # 路由前缀 VUE_APP_ROUTER_PREFIX = '/' # 默认上传地址 -VUE_APP_UPLOAD_URL = '/Common/UploadFile' +VUE_APP_UPLOAD_URL = '/Common/UploadFile' \ No newline at end of file diff --git a/ZR.Vue/.env.staging b/ZR.Vue/.env.staging index 14b1559..76909f2 100644 --- a/ZR.Vue/.env.staging +++ b/ZR.Vue/.env.staging @@ -7,6 +7,9 @@ VUE_APP_TITLE = 'ZrAdmin.NET后台管理' # 测试环境 VUE_APP_BASE_API = '/stage-api' +#socket API +VUE_APP_SOCKET_API = '/msgHub' + # 路由前缀 VUE_APP_ROUTER_PREFIX = '/' diff --git a/ZR.Vue/package.json b/ZR.Vue/package.json index 2318552..056cd16 100644 --- a/ZR.Vue/package.json +++ b/ZR.Vue/package.json @@ -19,6 +19,7 @@ ] }, "dependencies": { + "@microsoft/signalr": "^6.0.2", "@riophae/vue-treeselect": "0.4.0", "axios": "^0.21.4", "clipboard": "2.0.8", diff --git a/ZR.Vue/src/main.js b/ZR.Vue/src/main.js index d669df8..a58fc6a 100644 --- a/ZR.Vue/src/main.js +++ b/ZR.Vue/src/main.js @@ -12,6 +12,7 @@ import store from './store' import router from './router' import permission from './directive/permission' import plugins from './plugins' // plugins +import signalR from '@/utils/signalR' import './assets/icons' // icon import './permission' // permission control @@ -43,6 +44,9 @@ Vue.prototype.selectDictLabels = selectDictLabels Vue.prototype.download = download Vue.prototype.handleTree = handleTree +signalR.init(process.env.VUE_APP_SOCKET_API); +Vue.prototype.signalr = signalR + Vue.prototype.msgSuccess = function (msg) { this.$message({ showClose: true, message: msg, type: "success" }); } diff --git a/ZR.Vue/src/store/getters.js b/ZR.Vue/src/store/getters.js index ec52d5f..27d6b45 100644 --- a/ZR.Vue/src/store/getters.js +++ b/ZR.Vue/src/store/getters.js @@ -1,20 +1,21 @@ const getters = { - sidebar: state => state.app.sidebar, - size: state => state.app.size, - device: state => state.app.device, - visitedViews: state => state.tagsView.visitedViews, - cachedViews: state => state.tagsView.cachedViews, - token: state => state.user.token, - avatar: state => state.user.avatar, - name: state => state.user.name, - userId: state => state.user.userInfo.userId, - introduction: state => state.user.introduction, - roles: state => state.user.roles, - permissions: state => state.user.permissions, - permission_routes: state => state.permission.routes, - userinfo: state => state.user.userInfo, - topbarRouters: state => state.permission.topbarRouters, - defaultRoutes: state => state.permission.defaultRoutes, - sidebarRouters: state => state.permission.sidebarRouters, + sidebar: state => state.app.sidebar, + size: state => state.app.size, + device: state => state.app.device, + visitedViews: state => state.tagsView.visitedViews, + cachedViews: state => state.tagsView.cachedViews, + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + userId: state => state.user.userInfo.userId, + introduction: state => state.user.introduction, + roles: state => state.user.roles, + permissions: state => state.user.permissions, + permission_routes: state => state.permission.routes, + userinfo: state => state.user.userInfo, + topbarRouters: state => state.permission.topbarRouters, + defaultRoutes: state => state.permission.defaultRoutes, + sidebarRouters: state => state.permission.sidebarRouters, + onlineUserNum: state => state.socket.onlineNum } export default getters \ No newline at end of file diff --git a/ZR.Vue/src/store/index.js b/ZR.Vue/src/store/index.js index 300a9c8..ecbbcd7 100644 --- a/ZR.Vue/src/store/index.js +++ b/ZR.Vue/src/store/index.js @@ -5,6 +5,7 @@ import user from './modules/user' import tagsView from './modules/tagsView' import permission from './modules/permission' import settings from './modules/settings' +import socket from './modules/socket' import getters from './getters' Vue.use(Vuex) @@ -21,7 +22,8 @@ const store = new Vuex.Store({ user, tagsView, permission, - settings + settings, + socket }, state: state,//这里放全局参数 getters diff --git a/ZR.Vue/src/store/modules/socket.js b/ZR.Vue/src/store/modules/socket.js new file mode 100644 index 0000000..68fb47c --- /dev/null +++ b/ZR.Vue/src/store/modules/socket.js @@ -0,0 +1,22 @@ +const state = { + onlineNum: 0 +} +const mutations = { + SET_ONLINEUSER_NUM: (state, num) => { + state.onlineNum = num + }, +} + +const actions = { + //更新在线人数 + changeOnlineNum({ commit }, data) { + commit('SET_ONLINEUSER_NUM', data) + }, +} + +export default { + namespaced: true, + state, + mutations, + actions +} \ No newline at end of file diff --git a/ZR.Vue/src/utils/signalR.js b/ZR.Vue/src/utils/signalR.js new file mode 100644 index 0000000..c7185b3 --- /dev/null +++ b/ZR.Vue/src/utils/signalR.js @@ -0,0 +1,49 @@ +// 官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/javascript-client?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2&tabs=visual-studio +import * as signalR from '@microsoft/signalr' +import store from '../store' + +export default { + // signalR对象 + SR: {}, + // 失败连接重试次数 + failNum: 4, + baseUrl: '', + init(url) { + const connection = new signalR.HubConnectionBuilder() + .withUrl(url) + .build(); + // console.log('conn', connection); + this.SR = connection; + + // 断线重连 + connection.onclose(async () => { + await this.SR.start(); + }) + + connection.on("onlineNum", (data) => { + store.dispatch("socket/changeOnlineNum", data); + }); + // 启动 + this.start(); + }, + async start() { + var that = this; + + try { + //使用async和await 或 promise的then 和catch 处理来自服务端的异常 + + await this.SR.start(); + + //console.assert(this.SR.state === signalR.HubConnectionState.Connected); + console.log('signalR 连接成功了', this.SR.state); + } catch (error) { + that.failNum--; + console.log(`失败重试剩余次数${that.failNum}`, error) + if (that.failNum > 0) { + setTimeout(async () => { + await this.SR.start() + }, 5000); + } + } + } +} \ No newline at end of file diff --git a/ZR.Vue/src/views/dashboard/PanelGroup.vue b/ZR.Vue/src/views/dashboard/PanelGroup.vue index 1a1081f..16b4735 100644 --- a/ZR.Vue/src/views/dashboard/PanelGroup.vue +++ b/ZR.Vue/src/views/dashboard/PanelGroup.vue @@ -7,9 +7,9 @@
- 访客 + 在线用户
- +
@@ -56,18 +56,21 @@