Bläddra i källkod

实时显示在线用户,可以强制用户下线(先迁移一下增加权限码,分配权限后才能强制用户下线)

skywolf627 3 år sedan
förälder
incheckning
fd22846275

+ 14 - 0
Admin.NET/Admin.NET.Core/Admin.NET.Core.xml

@@ -2989,6 +2989,13 @@
             <param name="exception"></param>
             <returns></returns>
         </member>
+        <member name="M:Admin.NET.Core.ChatHub.ForceExistUser(Admin.NET.Core.ForceExistUserRequest)">
+            <summary>
+            强制下线
+            </summary>
+            <param name="request"></param>
+            <returns></returns>
+        </member>
         <member name="M:Admin.NET.Core.ChatHub.ClientsSendMessage(Admin.NET.Core.MessageInput)">
             <summary>
             前端调用发送方法(发送信息给某个人)
@@ -3060,6 +3067,13 @@
             <param name="context"></param>
             <returns></returns>
         </member>
+        <member name="M:Admin.NET.Core.IChatClient.OnlineUserChanged(Admin.NET.Core.OnlineUserChangedDto)">
+            <summary>
+            在线用户变动
+            </summary>
+            <param name="context"></param>
+            <returns></returns>
+        </member>
         <member name="M:Admin.NET.Core.IChatClient.AppendNotice(Admin.NET.Core.SysNotice)">
             <summary>
             组合信息

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysOnlineUser.cs

@@ -36,7 +36,7 @@ public class SysOnlineUser : EntityBaseId
     /// 最后连接时间
     /// </summary>
     [SugarColumn(ColumnDescription = "最后连接时间")]
-    public DateTimeOffset LastTime { get; set; }
+    public DateTime LastTime { get; set; }
 
     /// <summary>
     /// 最后登录IP

+ 56 - 8
Admin.NET/Admin.NET.Core/Hub/ChatHub.cs

@@ -6,17 +6,24 @@ namespace Admin.NET.Core;
 /// <summary>
 /// 聊天集线器
 /// </summary>
-[MapHub("/hub/chathub")]
+[MapHub("/hubs/chathub")]
 public class ChatHub : Hub<IChatClient>
 {
     private readonly ISysCacheService _cache;
     private readonly IMessageService _sendMessageService;
+    private readonly SqlSugarRepository<SysOnlineUser> _sysOnlineUerRep;
+    private readonly IHubContext<ChatHub, IChatClient> _chatHubContext;
+
 
     public ChatHub(ISysCacheService cache,
-        IMessageService sendMessageService)
+        IMessageService sendMessageService,
+        SqlSugarRepository<SysOnlineUser> sysOnlineUerRep,
+        IHubContext<ChatHub, IChatClient> chatHubContext)
     {
         _cache = cache;
         _sendMessageService = sendMessageService;
+        _sysOnlineUerRep = sysOnlineUerRep;
+        _chatHubContext = chatHubContext;
     }
 
     /// <summary>
@@ -37,7 +44,7 @@ public class ChatHub : Hub<IChatClient>
         var name = claims.FirstOrDefault(e => e.Type == ClaimConst.RealName)?.Value;
         var tenantId = claims.FirstOrDefault(e => e.Type == ClaimConst.TenantId)?.Value;
         var onlineUsers = await _cache.GetAsync<List<SysOnlineUser>>(CacheConst.KeyOnlineUser) ?? new List<SysOnlineUser>();
-        onlineUsers.Add(new SysOnlineUser
+        var user = new SysOnlineUser
         {
             ConnectionId = Context.ConnectionId,
             UserId = long.Parse(userId),
@@ -48,8 +55,24 @@ public class ChatHub : Hub<IChatClient>
             Account = account,
             Name = name,
             TenantId = Convert.ToInt64(tenantId),
+        };
+        await _sysOnlineUerRep.AsInsertable(user).ExecuteCommandAsync();
+        //加入分组  以租户ID分组 方便后续通知
+        await _chatHubContext.Groups.AddToGroupAsync(Context.ConnectionId, $"{ChatHubPrefix.GROUP_ONLINE}{tenantId}");
+
+        var list = await _sysOnlineUerRep.AsQueryable().Filter("", true).Where(x => x.TenantId == user.TenantId).ToListAsync();
+        await _chatHubContext.Clients.Groups($"{ChatHubPrefix.GROUP_ONLINE}{user.TenantId}").OnlineUserChanged(new OnlineUserChangedDto
+        {
+            Name = user.Name,
+            Offline = false,
+            List = list
         });
-        await _cache.SetAsync(CacheConst.KeyOnlineUser, onlineUsers);
+
+        //onlineUsers.Add();
+        //await _cache.SetAsync($"{CacheConst.KeyOnlineUser}{ Context.ConnectionId}", user);
+
+
+        //await _sendMessageService.SendMessageToUserByConnectionId("asdasd但凡生得分", "下线吧", MessageTypeEnum.Offline, Context.ConnectionId);
     }
 
     /// <summary>
@@ -61,14 +84,38 @@ public class ChatHub : Hub<IChatClient>
     {
         if (!string.IsNullOrEmpty(Context.ConnectionId))
         {
-            var onlineUsers = await _cache.GetAsync<List<SysOnlineUser>>(CacheConst.KeyOnlineUser);
-            if (onlineUsers == null) return;
+            var user = await _sysOnlineUerRep.AsQueryable().Filter("", true).FirstAsync(x => x.ConnectionId == Context.ConnectionId);
+            if (user == null) return;
+
+            await _sysOnlineUerRep.DeleteAsync(x => x.Id == user.Id);
+            //通知当前组用户变动
+            var list = await _sysOnlineUerRep.AsQueryable().Filter("", true).Where(x => x.TenantId == user.TenantId).ToListAsync();
+            await _chatHubContext.Clients.Groups($"{ChatHubPrefix.GROUP_ONLINE}{user.TenantId}").OnlineUserChanged(new OnlineUserChangedDto
+            {
+                Name = user.Name,
+                Offline = true,
+                List = list
+            });
+
+            //var onlineUsers = await _cache.GetAsync<List<SysOnlineUser>>(CacheConst.KeyOnlineUser);
+            //if (onlineUsers == null) return;
 
-            onlineUsers.RemoveAll(u => u.ConnectionId == Context.ConnectionId);
-            await _cache.SetAsync(CacheConst.KeyOnlineUser, onlineUsers);
+            //onlineUsers.RemoveAll(u => u.ConnectionId == Context.ConnectionId);
+            //await _cache.RemoveAsync($"{CacheConst.KeyOnlineUser}{ Context.ConnectionId}");
         }
     }
 
+    /// <summary>
+    /// 强制下线
+    /// </summary>
+    /// <param name="request"></param>
+    /// <returns></returns>
+    public async Task ForceExistUser(ForceExistUserRequest request)
+    {
+        await _chatHubContext.Clients.Client(request.ConnectionId).ForceExist("强制下线");
+    }
+
+
     /// <summary>
     /// 前端调用发送方法(发送信息给某个人)
     /// </summary>
@@ -96,6 +143,7 @@ public class ChatHub : Hub<IChatClient>
     /// <returns></returns>
     public async Task ClientsSendMessagetoOther(MessageInput _message)
     {
+        // _message.userId为发送人ID
         await _sendMessageService.SendMessageToOtherUser(_message.Title, _message.Message, _message.MessageType, _message.UserId);
     }
 

+ 7 - 0
Admin.NET/Admin.NET.Core/Hub/Dto/ChatHubPrefix.cs

@@ -0,0 +1,7 @@
+
+namespace Admin.NET.Core;
+
+public class ChatHubPrefix
+{
+    public const string GROUP_ONLINE = "GROUP_ONLINE_";
+}

+ 7 - 0
Admin.NET/Admin.NET.Core/Hub/Dto/ChatHubRequest.cs

@@ -0,0 +1,7 @@
+
+namespace Admin.NET.Core;
+
+public class ChatHubRequest
+{
+    public string ConnectionId { get; set; }
+}

+ 7 - 0
Admin.NET/Admin.NET.Core/Hub/Dto/ForceExistUserRequest.cs

@@ -0,0 +1,7 @@
+
+namespace Admin.NET.Core;
+
+public class ForceExistUserRequest : ChatHubRequest
+{
+
+}

+ 9 - 0
Admin.NET/Admin.NET.Core/Hub/Dto/OnlineUserChanged.cs

@@ -0,0 +1,9 @@
+namespace Admin.NET.Core;
+
+public class OnlineUserChangedDto
+{
+    public string Name { get; set; }
+    public bool Offline { get; set; }
+    public List<SysOnlineUser> List { get; set; }
+}
+

+ 8 - 0
Admin.NET/Admin.NET.Core/Hub/IChatClient.cs

@@ -18,6 +18,14 @@ public interface IChatClient
     /// <returns></returns>
     Task ReceiveMessage(object context);
 
+
+    /// <summary>
+    /// 在线用户变动
+    /// </summary>
+    /// <param name="context"></param>
+    /// <returns></returns>
+    Task OnlineUserChanged(OnlineUserChangedDto context);
+
     /// <summary>
     /// 组合信息
     /// </summary>

+ 1 - 0
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -89,6 +89,7 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=252885263003860, Pid=252885263003780, Title="在线用户", Path="online", Name="OnlineManagement", Component="/sys/admin/online/index", Icon="ant-design:user-switch-outlined", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=107 },
             new SysMenu{ Id=252885263003861, Pid=252885263003860, Title="用户查询", Permission="online:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=252885263003862, Pid=252885263003860, Title="用户删除", Permission="online:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=252885263003863, Pid=252885263003860, Title="强制下线", Permission="online:ForceExistUser", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
             new SysMenu{ Id=252885263003870, Pid=252885263003780, Title="系统监控", Path="server", Name="ServerManagement", Component="/sys/admin/server/index", Icon="ant-design:alert-outlined", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=108 },
 

+ 2 - 0
Vben2/.env.development

@@ -19,3 +19,5 @@ VITE_GLOB_UPLOAD_URL=https://localhost:44326
 
 # Interface prefix
 VITE_GLOB_API_URL_PREFIX=
+
+VITE_GLOB_SIGNALR_URL=https://localhost:44326/hubs/chatHub

+ 2 - 1
Vben2/package.json

@@ -38,6 +38,7 @@
     "@iconify/iconify": "^2.2.1",
     "@logicflow/core": "^1.1.13",
     "@logicflow/extension": "^1.1.13",
+    "@microsoft/signalr": "^6.0.8",
     "@vue/runtime-core": "^3.2.33",
     "@vue/shared": "^3.2.33",
     "@vueuse/core": "^8.3.0",
@@ -191,4 +192,4 @@
       "path": "node_modules/cz-git"
     }
   }
-}
+}

+ 162 - 0
Vben2/src/extension/components/OnlineUser/OnlineUser.vue

@@ -0,0 +1,162 @@
+<template>
+  <div>
+    <div @click="toggleDrawer">{{ onlineUserList.length }}人在线</div>
+
+    <Drawer title="在线人员" width="600px" v-model:visible="drawerShow">
+      <List item-layout="horizontal" :data-source="onlineUserList">
+        <template #renderItem="{ item }">
+          <ListItem>
+            <ListItemMeta>
+              <template #title>
+                <div style="display: flex">
+                  <div style="flex: 1">{{ item.name }} ({{ item.account }}) </div>
+                  <Popconfirm
+                    title="确定要强制此用户下线吗?"
+                    ok-text="确定"
+                    cancel-text="取消"
+                    @confirm="onForceExist(item.connectionId)"
+                  >
+                    <a-button v-if="hasPermission('online:ForceExistUser')" type="link" danger
+                      >强制下线</a-button
+                    >
+                  </Popconfirm>
+                </div>
+              </template>
+              <template #description>
+                <!-- {{ item }} -->
+                <div class="extra-wrapper">
+                  <Space>
+                    <span>
+                      <ClockCircleOutlined />{{
+                        formatToDateTimes(item.lastTime, 'YYYY/MM/DD HH:mm:ss')
+                      }}
+                      <Divider type="vertical" />
+                    </span>
+                    <span>
+                      <LaptopOutlined />{{ item.lastLoginOs }}
+                      <Divider type="vertical" />
+                    </span>
+                    <span>
+                      <IeOutlined />{{ item.lastLoginBrowser }}
+                      <Divider type="vertical" />
+                    </span>
+                    <span> <ClusterOutlined />{{ item.lastLoginIp }} </span>
+                  </Space>
+                </div>
+              </template>
+            </ListItemMeta>
+          </ListItem>
+        </template>
+      </List>
+    </Drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import * as SignalR from '@microsoft/signalr';
+  import { getAppEnvConfig } from '/@/utils/env';
+  import { getToken } from '/@/utils/auth';
+  import { useUserStore } from '/@/store/modules/user';
+  import { usePermission } from '/@/hooks/web/usePermission';
+  //使用ant 原生组件 方便后期移植
+  import {
+    Drawer,
+    notification,
+    List,
+    ListItem,
+    ListItemMeta,
+    Space,
+    Divider,
+    Popconfirm,
+  } from 'ant-design-vue';
+  import {
+    IeOutlined,
+    LaptopOutlined,
+    ClusterOutlined,
+    ClockCircleOutlined,
+  } from '@ant-design/icons-vue';
+
+  import { formatToDateTimes } from '/@/utils/dateUtil';
+  const { VITE_GLOB_SIGNALR_URL } = getAppEnvConfig();
+  const userStore = useUserStore();
+  const { hasPermission } = usePermission();
+  const onlineUserList = ref<any>([]);
+  const drawerShow = ref(false);
+  console.log(userStore.getUserInfo);
+
+  function toggleDrawer() {
+    drawerShow.value = !drawerShow.value;
+  }
+  async function onForceExist(connectionId) {
+    console.log(connectionId);
+    await connection.send('ForceExistUser', { connectionId }).catch(function (err) {
+      console.log(err);
+    });
+  }
+
+  const messages = ref('');
+  const reciveMessage = (msg: any) => {
+    console.log('msg', msg);
+  };
+
+  //初始化signalr HubConnection对象
+  const connection = new SignalR.HubConnectionBuilder()
+    .configureLogging(SignalR.LogLevel.Information)
+    .withUrl(`${VITE_GLOB_SIGNALR_URL}?access_token=${getToken()}`)
+    .withAutomaticReconnect({
+      nextRetryDelayInMilliseconds: () => {
+        //每5秒重连一次
+        return 5000;
+      },
+    })
+    .build();
+
+  connection.keepAliveIntervalInMilliseconds = 15000; //定时PING服务器,避免掉线
+
+  //注册web端方法以供后端调用
+  connection.on('ReceiveMessage', reciveMessage);
+  connection.on('ForceExist', async (x: any) => {
+    console.log('强制下线', x);
+    await connection.stop();
+    userStore.logout(true);
+  });
+  connection.on('OnlineUserChanged', (data: any) => {
+    console.log('人员变动', data);
+    onlineUserList.value = data.list;
+    notification.open({
+      message: `${data.offline ? `${data.name}离开了` : `欢迎${data.name}上线`}`,
+      // description:
+      //   'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
+      placement: 'bottomRight',
+    });
+  });
+
+  connection.onclose(async () => {
+    //连接断开
+  });
+  connection.onreconnecting(() => {
+    //掉线重连中
+  });
+  connection.onreconnected(() => {
+    //重新连接成功
+  });
+
+  //启动连接并发送消息测试
+  connection.start().then(() => {
+    //第一次连接成功
+  });
+
+  // const sendMsg = async () => {
+  //   console.log(messages.value);
+  //   await connection.send('SendMessage', messages.value).catch(function (err) {
+  //     console.log(err);
+  //   });
+  // };
+</script>
+
+<style scoped>
+  .extra-wrapper ::v-deep(.anticon) {
+    margin-right: 8px;
+  }
+</style>

+ 4 - 2
Vben2/src/layouts/default/header/index.vue

@@ -49,7 +49,7 @@
       /> -->
 
       <UserDropDown :theme="getHeaderTheme" />
-
+      <OnlineUser />
       <SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
     </div>
   </Header>
@@ -80,6 +80,7 @@
 
   import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
   import { useLocale } from '/@/locales/useLocale';
+  import OnlineUser from '/@/extension/components/OnlineUser/OnlineUser.vue';
 
   export default defineComponent({
     name: 'LayoutHeader',
@@ -97,7 +98,8 @@
       ErrorAction,
       SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
         loading: true,
-      }),
+      }),      
+      OnlineUser,
     },
     props: {
       fixed: propTypes.bool,

+ 2 - 0
Vben2/src/utils/env.ts

@@ -28,6 +28,7 @@ export function getAppEnvConfig() {
     VITE_GLOB_APP_SHORT_NAME,
     VITE_GLOB_API_URL_PREFIX,
     VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_SIGNALR_URL,
   } = ENV;
 
   if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -42,6 +43,7 @@ export function getAppEnvConfig() {
     VITE_GLOB_APP_SHORT_NAME,
     VITE_GLOB_API_URL_PREFIX,
     VITE_GLOB_UPLOAD_URL,
+    VITE_GLOB_SIGNALR_URL,
   };
 }
 

+ 1 - 0
Vben2/types/config.d.ts

@@ -159,4 +159,5 @@ export interface GlobEnvConfig {
   VITE_GLOB_APP_SHORT_NAME: string;
   // Upload url
   VITE_GLOB_UPLOAD_URL?: string;
+  VITE_GLOB_SIGNALR_URL?: string;
 }