diff --git a/Bootstrap.Admin/Controllers/AdminController.cs b/Bootstrap.Admin/Controllers/AdminController.cs
index 270b1772f4eb45c3ca64461831821447ac377cfd..a4ea52662eaa445bc334f44c83a9f0fa1056b524 100644
--- a/Bootstrap.Admin/Controllers/AdminController.cs
+++ b/Bootstrap.Admin/Controllers/AdminController.cs
@@ -108,6 +108,12 @@ namespace Bootstrap.Admin.Controllers
///
public ActionResult Mobile() => View(new NavigatorBarModel(this));
+ ///
+ /// 在线用户
+ ///
+ ///
+ public ActionResult Online() => View(new NavigatorBarModel(this));
+
///
/// 用于测试ExceptionFilter
///
diff --git a/Bootstrap.Admin/Controllers/Api/OnlineUsersController.cs b/Bootstrap.Admin/Controllers/Api/OnlineUsersController.cs
new file mode 100644
index 0000000000000000000000000000000000000000..55409a444cb43c3d81e5e31014b3812d5eb10190
--- /dev/null
+++ b/Bootstrap.Admin/Controllers/Api/OnlineUsersController.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Bootstrap.Admin.Controllers.Api
+{
+ ///
+ /// 在线用户接口
+ ///
+ [Route("api/[controller]")]
+ [ApiController]
+ public class OnlineUsersController : ControllerBase
+ {
+ ///
+ /// 获取所有在线用户数据
+ ///
+ ///
+ [HttpPost()]
+ public IEnumerable Post([FromServices]IOnlineUsers onlineUSers)
+ {
+ return onlineUSers.OnlineUsers;
+ }
+
+ ///
+ /// 获取指定IP地址的在线用户请求地址明细数据
+ ///
+ ///
+ ///
+ ///
+ [HttpGet("{id}")]
+ public IEnumerable> Get(string id, [FromServices]IOnlineUsers onlineUSers)
+ {
+ var user = onlineUSers.OnlineUsers.FirstOrDefault(u => u.Ip == id);
+ return user?.RequestUrls ?? new KeyValuePair[0];
+ }
+ }
+}
diff --git a/Bootstrap.Admin/OnlineUsers/DefaultOnlineUsers.cs b/Bootstrap.Admin/OnlineUsers/DefaultOnlineUsers.cs
new file mode 100644
index 0000000000000000000000000000000000000000..53d4875a2992deee3aee8ea48ef906c83c04db2c
--- /dev/null
+++ b/Bootstrap.Admin/OnlineUsers/DefaultOnlineUsers.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace Bootstrap.Admin
+{
+ ///
+ ///
+ ///
+ internal class DefaultOnlineUsers : IOnlineUsers
+ {
+ private ConcurrentDictionary _onlineUsers = new ConcurrentDictionary();
+
+ ///
+ ///
+ ///
+ ///
+ public IEnumerable OnlineUsers
+ {
+ get { return _onlineUsers.Values; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public OnlineUser AddOrUpdate(string key, Func addValueFactory, Func updateValueFactory) => _onlineUsers.AddOrUpdate(key, addValueFactory, updateValueFactory);
+ }
+}
diff --git a/Bootstrap.Admin/OnlineUsers/IOnlineUsers.cs b/Bootstrap.Admin/OnlineUsers/IOnlineUsers.cs
new file mode 100644
index 0000000000000000000000000000000000000000..841237858774802d5bf41bb011da6481a7f40eb3
--- /dev/null
+++ b/Bootstrap.Admin/OnlineUsers/IOnlineUsers.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+
+namespace Bootstrap.Admin
+{
+ ///
+ ///
+ ///
+ public interface IOnlineUsers
+ {
+ ///
+ ///
+ ///
+ IEnumerable OnlineUsers { get; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ OnlineUser AddOrUpdate(string key, Func addValueFactory, Func updateValueFactory);
+ }
+}
diff --git a/Bootstrap.Admin/OnlineUsers/OnlineUser.cs b/Bootstrap.Admin/OnlineUsers/OnlineUser.cs
new file mode 100644
index 0000000000000000000000000000000000000000..5e78387762a493b95a524f369e1722fff2e0128e
--- /dev/null
+++ b/Bootstrap.Admin/OnlineUsers/OnlineUser.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+
+namespace Bootstrap.Admin
+{
+
+ ///
+ ///
+ ///
+ public class OnlineUser
+ {
+ private ConcurrentQueue> _requestUrls;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public OnlineUser(string ip, string userName)
+ {
+ Ip = ip;
+ UserName = userName;
+ FirstAccessTime = DateTime.Now;
+ LastAccessTime = DateTime.Now;
+ _requestUrls = new ConcurrentQueue>();
+ }
+
+ ///
+ ///
+ ///
+ public string UserName { get; }
+
+ ///
+ ///
+ ///
+ public DateTime FirstAccessTime { get; }
+
+ ///
+ ///
+ ///
+ public DateTime LastAccessTime { get; set; }
+
+ ///
+ ///
+ ///
+ public string Method { get; set; }
+
+ ///
+ ///
+ ///
+ public string Ip { get; set; }
+
+ ///
+ ///
+ ///
+ public string RequestUrl { get; set; }
+
+ ///
+ ///
+ ///
+ public IEnumerable> RequestUrls
+ {
+ get
+ {
+ return _requestUrls.ToArray();
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public void AddRequestUrl(string url)
+ {
+ _requestUrls.Enqueue(new KeyValuePair(DateTime.Now, url));
+ if (_requestUrls.Count > 5)
+ {
+ _requestUrls.TryDequeue(out _);
+ }
+ }
+ }
+}
diff --git a/Bootstrap.Admin/OnlineUsers/OnlineUsersMiddlewareExtensions.cs b/Bootstrap.Admin/OnlineUsers/OnlineUsersMiddlewareExtensions.cs
new file mode 100644
index 0000000000000000000000000000000000000000..c178329376c6981dac091a24f8e7a4d606befa3d
--- /dev/null
+++ b/Bootstrap.Admin/OnlineUsers/OnlineUsersMiddlewareExtensions.cs
@@ -0,0 +1,51 @@
+using Bootstrap.Admin;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ ///
+ ///
+ public static class OnlineUsersMiddlewareExtensions
+ {
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IApplicationBuilder UseOnlineUsers(this IApplicationBuilder builder) => builder.UseWhen(context => context.Filter(), app => app.Use(async (context, next) =>
+ {
+ await Task.Run(() =>
+ {
+ var onlineUsers = context.RequestServices.GetService();
+ var clientIp = context.Connection.RemoteIpAddress.ToString();
+ onlineUsers.AddOrUpdate(clientIp, key =>
+ {
+ var ou = new OnlineUser(key, context.User.Identity.Name);
+ ou.Method = context.Request.Method;
+ ou.RequestUrl = context.Request.Path;
+ ou.AddRequestUrl(context.Request.Path);
+ return ou;
+ }, (key, v) =>
+ {
+ v.LastAccessTime = DateTime.Now;
+ v.Method = context.Request.Method;
+ v.RequestUrl = context.Request.Path;
+ v.AddRequestUrl(context.Request.Path);
+ return v;
+ });
+ });
+ await next();
+ }));
+
+ private static bool Filter(this HttpContext context)
+ {
+ var url = context.Request.Path;
+ return !new string[] { "/api", "/NotiHub", "/swagger" }.Any(r => url.StartsWithSegments(r, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
diff --git a/Bootstrap.Admin/OnlineUsers/OnlineUsersServicesCollectionExtensions.cs b/Bootstrap.Admin/OnlineUsers/OnlineUsersServicesCollectionExtensions.cs
new file mode 100644
index 0000000000000000000000000000000000000000..6cd77909540b749e72a29567f31451ea1118340f
--- /dev/null
+++ b/Bootstrap.Admin/OnlineUsers/OnlineUsersServicesCollectionExtensions.cs
@@ -0,0 +1,22 @@
+using Bootstrap.Admin;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+ ///
+ ///
+ ///
+ public static class OnlineUsersServicesCollectionExtensions
+ {
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IServiceCollection AddOnlineUsers(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+ return services;
+ }
+ }
+}
diff --git a/Bootstrap.Admin/Startup.cs b/Bootstrap.Admin/Startup.cs
index bf51014dc76482d2a301a20975c6433124eb2aa5..ff603bc7b2de3896e04f7980bc0ce6884fa4d50d 100644
--- a/Bootstrap.Admin/Startup.cs
+++ b/Bootstrap.Admin/Startup.cs
@@ -60,6 +60,7 @@ namespace Bootstrap.Admin
services.AddConfigurationManager(Configuration);
services.AddCacheManager(Configuration);
services.AddDbAdapter();
+ services.AddOnlineUsers();
var dataProtectionBuilder = services.AddDataProtection(op => op.ApplicationDiscriminator = Configuration["ApplicationDiscriminator"])
.SetApplicationName(Configuration["ApplicationName"])
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["KeyPath"]));
@@ -125,6 +126,7 @@ namespace Bootstrap.Admin
app.UseStaticFiles();
app.UseAuthentication();
app.UseBootstrapAdminAuthorization(RoleHelper.RetrieveRolesByUserName, RoleHelper.RetrieveRolesByUrl, AppHelper.RetrievesByUserName);
+ app.UseOnlineUsers();
app.UseCacheManagerCorsHandler();
app.UseSignalR(routes => { routes.MapHub("/NotiHub"); });
app.UseMvc(routes =>
diff --git a/Bootstrap.Admin/Views/Admin/Online.cshtml b/Bootstrap.Admin/Views/Admin/Online.cshtml
new file mode 100644
index 0000000000000000000000000000000000000000..57cd7357062659aa82c6635f36b664eec5af44a9
--- /dev/null
+++ b/Bootstrap.Admin/Views/Admin/Online.cshtml
@@ -0,0 +1,29 @@
+@model NavigatorBarModel
+@{
+ ViewBag.Title = "在线用户";
+}
+@section css {
+
+
+
+
+
+
+}
+@section javascript {
+
+
+
+
+
+
+
+
+
+}
+
\ No newline at end of file
diff --git a/Bootstrap.Admin/wwwroot/js/online.js b/Bootstrap.Admin/wwwroot/js/online.js
new file mode 100644
index 0000000000000000000000000000000000000000..040a100ac22dbee02d78270bbd97b7aadf40028c
--- /dev/null
+++ b/Bootstrap.Admin/wwwroot/js/online.js
@@ -0,0 +1,52 @@
+$(function () {
+ var apiUrl = "api/OnlineUsers";
+ var $table = $('table').smartTable({
+ url: apiUrl,
+ method: "post",
+ sidePagination: "client",
+ showToggle: false,
+ showRefresh: false,
+ showColumns: false,
+ columns: [
+ {
+ title: "序号", formatter: function (value, row, index) {
+ var options = $table.bootstrapTable('getOptions');
+ return options.pageSize * (options.pageNumber - 1) + index + 1;
+ }
+ },
+ { title: "登陆名称", field: "UserName" },
+ { title: "显示名称", field: "DisplayName" },
+ { title: "登录时间", field: "FirstAccessTime" },
+ { title: "最近操作时间", field: "LastAccessTime" },
+ { title: "请求方式", field: "Method" },
+ { title: "IP地址", field: "Ip" },
+ { title: "访问地址", field: "RequestUrl" },
+ {
+ title: "历史地址", field: "Ip", formatter: function (value, row, index, field) {
+ return $.format('', value);
+ }
+ }
+ ]
+ }).on('click', 'button[data-id]', function () {
+ var $this = $(this);
+ if (!$this.data($.fn.popover.Constructor.DATA_KEY)) {
+ var id = $this.attr('data-id');
+ $.bc({
+ id: id, url: apiUrl,
+ callback: function (result) {
+ if (!result) return;
+ var content = result.map(function (item) {
+ return $.format("| {0} | {1} |
", item.Key, item.Value);
+ }).join('');
+ content = $.format('', content);
+ $this.lgbPopover({ content: content, placement: $(window).width() < 768 ? 'top' : 'left' });
+ $this.popover('show');
+ }
+ });
+ }
+ });
+
+ $('#refreshUsers').tooltip().on('click', function () {
+ $table.bootstrapTable('refresh');
+ });
+});
\ No newline at end of file
diff --git a/DatabaseScripts/InitData.sql b/DatabaseScripts/InitData.sql
index 853b8538a2fce74027945e7b57ce1207a0b1f95d..68719da3440412aa71a88d152328a9a17393b5c0 100644
--- a/DatabaseScripts/InitData.sql
+++ b/DatabaseScripts/InitData.sql
@@ -46,7 +46,8 @@ INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [C
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (11, 0, N'任务管理', 110, N'fa fa fa-tasks', N'~/Admin/Tasks', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (12, 0, N'通知管理', 120, N'fa fa-bell', N'~/Admin/Notifications', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (13, 0, N'系统日志', 130, N'fa fa-gears', N'~/Admin/Logs', N'0')
-INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (14, 0, N'程序异常', 140, N'fa fa-cubes', N'~/Admin/Exceptions', N'0')
+INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (14, 0, N'在线用户', 140, N'fa fa-users', N'~/Admin/Online', N'0')
+INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (15, 0, N'程序异常', 150, N'fa fa-cubes', N'~/Admin/Exceptions', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (16, 0, N'工具集合', 160, N'fa fa-gavel', N'#', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (17, 16, N'客户端测试', 10, N'fa fa-wrench', N'~/Admin/Mobile', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (18, 16, N'API文档', 10, N'fa fa-wrench', N'~/swagger', N'0')
diff --git a/DatabaseScripts/MongoDB/BootstrapAdmin.Navigations.json b/DatabaseScripts/MongoDB/BootstrapAdmin.Navigations.json
index fd27cc7489d9cf59c12087d658506af9fbbc7081..e00a5823f8c1cf4b36caf9292d6603df66e1026d 100644
--- a/DatabaseScripts/MongoDB/BootstrapAdmin.Navigations.json
+++ b/DatabaseScripts/MongoDB/BootstrapAdmin.Navigations.json
@@ -155,11 +155,23 @@
"IsResource": NumberInt(0),
"Application": "0"
},
+ {
+ "_id": ObjectId("5bd7b8445fa31256f77e4b89"),
+ "ParentId": "0",
+ "Name": "在线用户",
+ "Order": NumberInt(140),
+ "Icon": "fa fa-users",
+ "Url": "~/Admin/Online",
+ "Category": "0",
+ "Target": "_self",
+ "IsResource": NumberInt(0),
+ "Application": "0"
+ },
{
"_id": ObjectId("5bd7b8445fa31256f77e4b9d"),
"ParentId": "0",
"Name": "程序异常",
- "Order": NumberInt(140),
+ "Order": NumberInt(150),
"Icon": "fa fa-cubes",
"Url": "~/Admin/Exceptions",
"Category": "0",
diff --git a/DatabaseScripts/MySQL/initData.sql b/DatabaseScripts/MySQL/initData.sql
index 014cecd33ea2a0622aced9ab1dc77c9e4965f947..d02e41e760193aa2dec5724104cabaa73328ff1e 100644
--- a/DatabaseScripts/MySQL/initData.sql
+++ b/DatabaseScripts/MySQL/initData.sql
@@ -43,7 +43,8 @@ INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUE
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (11, 0, '任务管理', 110, 'fa fa fa-tasks', '~/Admin/Tasks', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (12, 0, '通知管理', 120, 'fa fa-bell', '~/Admin/Notifications', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (13, 0, '系统日志', 130, 'fa fa-gears', '~/Admin/Logs', '0');
-INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (14, 0, '程序异常', 140, 'fa fa-cubes', '~/Admin/Exceptions', '0');
+INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (14, 0, '在线用户', 140, 'fa fa-users', '~/Admin/Online', '0');
+INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (15, 0, '程序异常', 150, 'fa fa-cubes', '~/Admin/Exceptions', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (16, 0, '工具集合', 160, 'fa fa-gavel', '#', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (17, 16, '客户端测试', 10, 'fa fa-wrench', '~/Admin/Mobile', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (18, 16, 'API文档', 10, 'fa fa-wrench', '~/swagger', '0');
diff --git a/DatabaseScripts/Postgresql/initData.sql b/DatabaseScripts/Postgresql/initData.sql
index 2ff02fa14a5d8c2741dee83cd13e5f86f15e8578..abbcc8ab0daea99e4df7388b2f87a9a87d6c938d 100644
--- a/DatabaseScripts/Postgresql/initData.sql
+++ b/DatabaseScripts/Postgresql/initData.sql
@@ -43,7 +43,8 @@ INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '任务管理', 110, 'fa fa fa-tasks', '~/Admin/Tasks', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '通知管理', 120, 'fa fa-bell', '~/Admin/Notifications', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '系统日志', 130, 'fa fa-gears', '~/Admin/Logs', '0');
-INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '程序异常', 140, 'fa fa-cubes', '~/Admin/Exceptions', '0');
+INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '在线用户', 140, 'fa fa-users', '~/Admin/Online', '0');
+INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '程序异常', 150, 'fa fa-cubes', '~/Admin/Exceptions', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '工具集合', 160, 'fa fa-gavel', '#', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (currval('navigations_id_seq') - 1, '客户端测试', 10, 'fa fa-wrench', '~/Admin/Mobile', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (currval('navigations_id_seq') - 2, 'API文档', 10, 'fa fa-wrench', '~/swagger', '0');
diff --git a/DatabaseScripts/SQLite/InitData.sql b/DatabaseScripts/SQLite/InitData.sql
index 9d881338fad7bac512cc7cce67e75c2729d982d2..fadaf13509ea5b610a6de742908eacaa56b3177b 100644
--- a/DatabaseScripts/SQLite/InitData.sql
+++ b/DatabaseScripts/SQLite/InitData.sql
@@ -40,7 +40,8 @@ INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Ca
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (11, 0, '任务管理', 110, 'fa fa fa-tasks', '~/Admin/Tasks', '0');
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (12, 0, '通知管理', 120, 'fa fa-bell', '~/Admin/Notifications', '0');
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (13, 0, '系统日志', 130, 'fa fa-gears', '~/Admin/Logs', '0');
-INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (14, 0, '程序异常', 140, 'fa fa-cubes', '~/Admin/Exceptions', '0');
+INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (14, 0, '在线用户', 140, 'fa fa-users', '~/Admin/Online', '0');
+INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (15, 0, '程序异常', 150, 'fa fa-cubes', '~/Admin/Exceptions', '0');
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (16, 0, '工具集合', 160, 'fa fa-gavel', '#', '0');
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (17, 16, '客户端测试', 10, 'fa fa-wrench', '~/Admin/Mobile', '0');
INSERT INTO [Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (18, 16, 'API文档', 10, 'fa fa-wrench', '~/swagger', '0');