增加功能:系统日志增加访问日志 closed #ITO8H

#Issue
https://gitee.com/LongbowEnterprise/BootstrapAdmin/issues/ITO8H
This commit is contained in:
Argo Zhang 2019-03-16 20:24:33 +08:00
parent 7fe2763f8e
commit 45adc853e2
31 changed files with 631 additions and 81 deletions

Binary file not shown.

View File

@ -1,6 +1,6 @@
using Bootstrap.Admin.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using System;
@ -53,6 +53,12 @@ namespace Bootstrap.Admin.Controllers
/// <returns></returns>
public ActionResult Logs() => View(new NavigatorBarModel(this));
/// <summary>
///
/// </summary>
/// <returns></returns>
public ActionResult Traces() => View(new NavigatorBarModel(this));
/// <summary>
///
/// </summary>
@ -125,7 +131,7 @@ namespace Bootstrap.Admin.Controllers
/// <summary>
/// 用于测试ExceptionFilter
/// </summary>
/// <returns></returns>
/// <returns></returns>
public ActionResult Error() => throw new Exception("Customer Excetion UnitTest");
}
}

View File

@ -0,0 +1,26 @@
using Bootstrap.Admin.Query;
using Bootstrap.DataAccess;
using Longbow.Web.Mvc;
using Microsoft.AspNetCore.Mvc;
namespace Bootstrap.Admin.Controllers.Api
{
/// <summary>
///
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class TracesController : ControllerBase
{
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpGet]
public QueryData<Trace> Get([FromQuery]QueryTraceOptions value)
{
return value.RetrieveData();
}
}
}

View File

@ -20,40 +20,41 @@ namespace Microsoft.AspNetCore.Builder
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseOnlineUsers(this IApplicationBuilder builder) => builder.UseWhen(context => context.Filter(), app => app.Use(async (context, next) =>
{
await System.Threading.Tasks.Task.Run(() =>
{
var user = UserHelper.RetrieveUserByUserName(context.User.Identity.Name);
if (user == null) return;
{
await System.Threading.Tasks.Task.Run(() =>
{
var user = UserHelper.RetrieveUserByUserName(context.User.Identity.Name);
if (user == null) return;
var onlineUserSvr = context.RequestServices.GetRequiredService<IOnlineUsers>();
var proxy = new Func<OnlineUserCache, Action, OnlineUserCache>((c, action) =>
{
var v = c.User;
v.UserName = user.UserName;
v.DisplayName = user.DisplayName;
v.LastAccessTime = DateTime.Now;
v.Method = context.Request.Method;
v.RequestUrl = context.Request.Path;
v.AddRequestUrl(context.Request.Path);
action?.Invoke();
return c;
});
onlineUserSvr.AddOrUpdate(context.Connection.Id ?? "", key =>
{
var agent = new UserAgent(context.Request.Headers["User-Agent"]);
var v = new OnlineUser();
v.ConnectionId = key;
v.Ip = (context.Connection.RemoteIpAddress ?? IPAddress.IPv6Loopback).ToString();
v.Location = onlineUserSvr.RetrieveLocaleByIp(v.Ip);
v.Browser = $"{agent.Browser.Name} {agent.Browser.Version}";
v.OS = $"{agent.OS.Name} {agent.OS.Version}";
v.FirstAccessTime = DateTime.Now;
return proxy(new OnlineUserCache(v, () => onlineUserSvr.TryRemove(key, out _)), null);
}, (key, v) => proxy(v, () => v.Reset()));
});
await next();
}));
var onlineUserSvr = context.RequestServices.GetRequiredService<IOnlineUsers>();
var proxy = new Func<OnlineUserCache, Action, OnlineUserCache>((c, action) =>
{
var v = c.User;
v.UserName = user.UserName;
v.DisplayName = user.DisplayName;
v.LastAccessTime = DateTime.Now;
v.Method = context.Request.Method;
v.RequestUrl = context.Request.Path;
v.AddRequestUrl(context.Request.Path);
action?.Invoke();
TraceHelper.Save(new Trace { Ip = v.Ip, RequestUrl = v.RequestUrl, LogTime = v.LastAccessTime, City = v.Location, Browser = v.Browser, OS = v.OS, UserName = v.UserName });
return c;
});
onlineUserSvr.AddOrUpdate(context.Connection.Id ?? "", key =>
{
var agent = new UserAgent(context.Request.Headers["User-Agent"]);
var v = new OnlineUser();
v.ConnectionId = key;
v.Ip = (context.Connection.RemoteIpAddress ?? IPAddress.IPv6Loopback).ToString();
v.Location = onlineUserSvr.RetrieveLocaleByIp(v.Ip);
v.Browser = $"{agent.Browser.Name} {agent.Browser.Version}";
v.OS = $"{agent.OS.Name} {agent.OS.Version}";
v.FirstAccessTime = DateTime.Now;
return proxy(new OnlineUserCache(v, () => onlineUserSvr.TryRemove(key, out _)), null);
}, (key, v) => proxy(v, () => v.Reset()));
});
await next();
}));
private static bool Filter(this HttpContext context)
{

View File

@ -0,0 +1,36 @@
using Bootstrap.DataAccess;
using Longbow.Web.Mvc;
using System;
namespace Bootstrap.Admin.Query
{
/// <summary>
/// Query trace options.
/// </summary>
public class QueryTraceOptions : PaginationOption
{
/// <summary>
///
/// </summary>
public DateTime? OperateTimeStart { get; set; }
/// <summary>
///
/// </summary>
public DateTime? OperateTimeEnd { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
public QueryData<Trace> RetrieveData()
{
var data = TraceHelper.Retrieves(this, OperateTimeStart, OperateTimeEnd);
var ret = new QueryData<Trace>();
ret.total = data.TotalItems;
ret.rows = data.Items;
return ret;
}
}
}

View File

@ -0,0 +1,103 @@
@model NavigatorBarModel
@{
ViewBag.Title = "访问日志";
}
@section css {
<environment include="Development">
<link href="~/lib/datetimepicker/css//bootstrap-datetimepicker.css" rel="stylesheet" />
<link href="~/lib/bootstrap-table/bootstrap-table.css" rel="stylesheet" />
</environment>
<environment exclude="Development">
<link href="~/lib/datetimepicker/css//bootstrap-datetimepicker.min.css" rel="stylesheet" />
<link href="~/lib/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" />
</environment>
}
@section javascript {
<environment include="Development">
<script src="~/lib/bootstrap-table/bootstrap-table.js"></script>
<script src="~/lib/bootstrap-table/extensions/export/bootstrap-table-export.js"></script>
<script src="~/lib/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script>
<script src="~/lib/tablexport/tableExport.js"></script>
<script src="~/lib/datetimepicker/js/bootstrap-datetimepicker.js"></script>
</environment>
<environment exclude="Development">
<script src="~/lib/bootstrap-table/bootstrap-table.min.js"></script>
<script src="~/lib/bootstrap-table/extensions/export/bootstrap-table-export.min.js"></script>
<script src="~/lib/bootstrap-table/locale/bootstrap-table-zh-CN.min.js"></script>
<script src="~/lib/tablexport/tableExport.min.js"></script>
<script src="~/lib/datetimepicker/js/bootstrap-datetimepicker.min.js"></script>
</environment>
<script src="~/lib/datetimepicker/js/locales/bootstrap-datetimepicker.zh-CN.js"></script>
<script src="~/js/traces.js" asp-append-version="true"></script>
}
<div class="card">
<div class="card-header">查询条件</div>
<div class="card-body">
<form class="form-inline">
<div class="row">
<div class="form-group col-sm-auto">
<label class="control-label" for="txt_operate_start">起始时间</label>
<div class="input-group date">
<input id="txt_operate_start" class="form-control" size="16" type="text" value="" readonly>
<div class="input-group-append input-group-addon">
<div class="input-group-text"><span class="fa fa-times"></span></div>
</div>
<div class="input-group-append input-group-addon">
<div class="input-group-text"><span class="fa fa-calendar"></span></div>
</div>
</div>
</div>
<div class="form-group col-sm-auto">
<label class="control-label" for="txt_operate_end">终止时间</label>
<div class="input-group date">
<input id="txt_operate_end" class="form-control" size="16" type="text" value="" readonly>
<div class="input-group-append input-group-addon">
<div class="input-group-text"><span class="fa fa-times"></span></div>
</div>
<div class="input-group-append input-group-addon">
<div class="input-group-text"><span class="fa fa-calendar"></span></div>
</div>
</div>
</div>
<div class="form-group col-sm-auto flex-sm-fill justify-content-sm-end">
<button type="button" id="btn_query" class="btn btn-primary btn-fill"><i class="fa fa-search" aria-hidden="true"></i><span>查询</span></button>
</div>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
查询结果
</div>
<div class="card-body">
<table></table>
</div>
</div>
@section modal {
<div class="modal fade" id="dialogRequestData" tabindex="-1" role="dialog" data-backdrop="static" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="myModalLabel">请求数据明细窗口</h5>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
</div>
<div class="modal-body">
<form class="form-inline">
<div class="form-row">
<div class="form-group col-12">
<pre id="requestData"></pre>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fa fa-times"></i>
<span>关闭</span>
</button>
</div>
</div>
</div>
</div>
}

View File

@ -0,0 +1,25 @@
$(function () {
var url = 'api/Traces';
var $data = $('#requestData');
var $dialog = $('#dialogRequestData');
$('.card-body table').smartTable({
url: url,
sortName: 'LogTime',
sortOrder: 'desc',
queryParams: function (params) { return $.extend(params, { OperateTimeStart: $("#txt_operate_start").val(), OperateTimeEnd: $("#txt_operate_end").val() }); },
columns: [
{ title: "用户名称", field: "UserName", sortable: true },
{ title: "操作时间", field: "LogTime", sortable: true },
{ title: "登录主机", field: "Ip", sortable: true },
{ title: "操作地点", field: "City", sortable: true },
{ title: "浏览器", field: "Browser", sortable: true },
{ title: "操作系统", field: "OS", sortable: true },
{ title: "操作页面", field: "RequestUrl", sortable: true }
],
exportOptions: {
fileName: "访问日志数据",
ignoreColumn: [8]
}
});
});

View File

@ -138,6 +138,17 @@ namespace Bootstrap.DataAccess.MongoDB
return DBAccess.GetCollection<DataAccess.ResetUser>("ResetUsers");
}
}
/// <summary>
///
/// </summary>
public static IMongoCollection<DataAccess.Trace> Traces
{
get
{
return DBAccess.GetCollection<DataAccess.Trace>("Traces");
}
}
#endregion
private static void InitDb()
@ -232,15 +243,23 @@ namespace Bootstrap.DataAccess.MongoDB
md.IdMemberMap.SetIgnoreIfDefault(true);
md.UnmapMember(ex => ex.Period);
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(DataAccess.Log)))
}
if (!BsonClassMap.IsClassMapRegistered(typeof(DataAccess.Trace)))
{
BsonClassMap.RegisterClassMap<DataAccess.Log>(md =>
BsonClassMap.RegisterClassMap<DataAccess.Trace>(md =>
{
md.AutoMap();
md.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId));
md.IdMemberMap.SetIgnoreIfDefault(true);
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(DataAccess.Log)))
{
BsonClassMap.RegisterClassMap<DataAccess.Log>(md =>
{
md.AutoMap();
md.AddKnownType(typeof(DataAccess.Trace));
});
}
if (!BsonClassMap.IsClassMapRegistered(typeof(DataAccess.LoginUser)))
{
@ -259,7 +278,7 @@ namespace Bootstrap.DataAccess.MongoDB
md.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId));
md.IdMemberMap.SetIgnoreIfDefault(true);
});
}
}
}
}
}

View File

@ -0,0 +1,79 @@
using Longbow.Web.Mvc;
using MongoDB.Driver;
using PetaPoco;
using System;
using System.Linq;
namespace Bootstrap.DataAccess.MongoDB
{
/// <summary>
///
/// </summary>
public class Trace : DataAccess.Trace
{
/// <summary>
///
/// </summary>
/// <returns></returns>
public override Page<DataAccess.Trace> Retrieves(PaginationOption po, DateTime? startTime, DateTime? endTime)
{
// filter
var filterBuilder = Builders<DataAccess.Trace>.Filter;
var filter = filterBuilder.Empty;
if (startTime.HasValue) filter = filterBuilder.Gt("LogTime", startTime.Value);
if (endTime.HasValue) filter = filterBuilder.Lt("LogTime", endTime.Value.AddDays(1).AddSeconds(-1));
if (startTime == null && endTime == null) filter = filterBuilder.Gt("LogTime", DateTime.Today.AddDays(-7));
// sort
var sortBuilder = Builders<DataAccess.Trace>.Sort;
SortDefinition<DataAccess.Trace> sort = null;
switch (po.Sort)
{
case "LogTime":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.LogTime) : sortBuilder.Descending(t => t.LogTime);
break;
case "IP":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.Ip) : sortBuilder.Descending(t => t.Ip);
break;
case "UserName":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.UserName) : sortBuilder.Descending(t => t.UserName);
break;
case "City":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.City) : sortBuilder.Descending(t => t.City);
break;
case "Browser":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.Browser) : sortBuilder.Descending(t => t.Browser);
break;
case "OS":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.OS) : sortBuilder.Descending(t => t.OS);
break;
case "RequestUrl":
sort = po.Order == "asc" ? sortBuilder.Ascending(t => t.RequestUrl) : sortBuilder.Descending(t => t.RequestUrl);
break;
}
var traces = DbManager.Traces.Find(filter).Sort(sort).ToList();
return new Page<DataAccess.Trace>()
{
Context = traces,
CurrentPage = po.PageIndex,
ItemsPerPage = po.Limit,
TotalItems = traces.Count,
TotalPages = (long)Math.Ceiling(traces.Count * 1.0 / po.Limit),
Items = traces.Skip(po.Offset).Take(po.Limit).ToList()
};
}
/// <summary>
///
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public override bool Save(DataAccess.Trace p)
{
p.Id = null;
DbManager.Traces.InsertOne(p);
return true;
}
}
}

View File

@ -28,6 +28,7 @@ namespace Bootstrap.DataAccess
database.AddMap<Menu>("Navigations", new string[] { "ParentName", "CategoryName", "Active", "Menus" });
database.AddMap<Role>("Roles", new string[] { "Checked" });
database.AddMap<Task>("Tasks");
database.AddMap<Trace>("Traces");
return database;
}
}

View File

@ -17,7 +17,6 @@ namespace Bootstrap.DataAccess
/// <summary>
/// 查询所有日志信息
/// </summary>
/// <param name="tId"></param>
/// <returns></returns>
public static IEnumerable<Log> Retrieves() => CacheManager.GetOrAdd(RetrieveLogsDataKey, key => DbContextManager.Create<Log>().Retrieves());

View File

@ -0,0 +1,26 @@
using Longbow.Data;
using Longbow.Web.Mvc;
using PetaPoco;
using System;
namespace Bootstrap.DataAccess
{
public static class TraceHelper
{
/// <summary>
/// 保存访问历史记录
/// </summary>
/// <param name="p"></param>
public static void Save(Trace p) => DbContextManager.Create<Trace>().Save(p);
/// <summary>
/// 获得指定IP历史访问记录
/// </summary>
/// <param name="po"></param>
/// <param name="startTime"></param>
/// <param name="endTime"></param>
/// <returns></returns>
public static Page<Trace> Retrieves(PaginationOption po, DateTime? startTime, DateTime? endTime) => DbContextManager.Create<Trace>().Retrieves(po, startTime, endTime);
}
}

View File

@ -6,53 +6,13 @@ namespace Bootstrap.DataAccess
/// <summary>
///
/// </summary>
public class Log
public class Log : Trace
{
/// <summary>
/// 获得/设置 操作日志主键ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 获得/设置 操作类型
/// </summary>
public string CRUD { get; set; }
/// <summary>
/// 获得/设置 用户名称
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 获得/设置 操作时间
/// </summary>
public DateTime LogTime { get; set; }
/// <summary>
/// 获得/设置 客户端IP
/// </summary>
public string Ip { get; set; }
/// <summary>
///
/// </summary>
public string City { get; set; }
/// <summary>
///
/// </summary>
public string Browser { get; set; }
/// <summary>
///
/// </summary>
public string OS { get; set; }
/// <summary>
/// 获取/设置 请求网址
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 获得/设置 请求数据
/// </summary>

View File

@ -0,0 +1,79 @@
using Longbow.Web.Mvc;
using PetaPoco;
using System;
namespace Bootstrap.DataAccess
{
/// <summary>
///
/// </summary>
public class Trace
{
/// <summary>
/// 获得/设置 操作日志主键ID
/// </summary>
public string Id { get; set; }
/// <summary>
/// 获得/设置 用户名称
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 获得/设置 操作时间
/// </summary>
public DateTime LogTime { get; set; }
/// <summary>
/// 获得/设置 客户端IP
/// </summary>
public string Ip { get; set; }
/// <summary>
///
/// </summary>
public string City { get; set; }
/// <summary>
///
/// </summary>
public string Browser { get; set; }
/// <summary>
///
/// </summary>
public string OS { get; set; }
/// <summary>
/// 获取/设置 请求网址
/// </summary>
public string RequestUrl { get; set; }
/// <summary>
/// 保存访问历史记录
/// </summary>
/// <param name="p"></param>
public virtual bool Save(Trace p)
{
if (p == null) throw new ArgumentNullException(nameof(p));
DbManager.Create().Save(p);
return true;
}
/// <summary>
///
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public virtual Page<Trace> Retrieves(PaginationOption po, DateTime? startTime, DateTime? endTime)
{
var sql = new Sql("select * from Traces");
if (startTime.HasValue) sql.Append("where LogTime > @0", startTime.Value);
if (endTime.HasValue) sql.Append("where LogTime < @0", endTime.Value.AddDays(1).AddSeconds(-1));
if (startTime == null && endTime == null) sql.Append("where LogTime > @0", DateTime.Today.AddDays(-7));
sql.Append($"order by {po.Sort} {po.Order}");
return DbManager.Create().Page<Trace>(po.PageIndex, po.Limit, sql);
}
}
}

View File

@ -58,6 +58,7 @@ INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [C
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (13, 0, N'日志管理', 130, N'fa fa-gears', N'#', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (20, 13, N'操作日志', 10, N'fa fa-edit', N'~/Admin/Logs', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (21, 13, N'登录日志', 20, N'fa fa-user-circle-o', N'~/Admin/Logins', N'0')
INSERT [dbo].[Navigations] ([ID], [ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (22, 13, N'访问日志', 30, N'fa fa-bars', N'~/Admin/Traces', 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')

View File

@ -535,3 +535,33 @@ GO
SET ANSI_PADDING OFF
GO
/****** Object: Table [dbo].[Traces] Script Date: 03/16/2019 18:37:46 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[Traces](
[ID] [int] IDENTITY(1,1) NOT NULL,
[UserName] [varchar](50) NOT NULL,
[LogTime] [datetime] NOT NULL,
[Ip] [varchar](15) NOT NULL,
[Browser] [varchar](50) NULL,
[OS] [varchar](50) NULL,
[City] [nvarchar](50) NULL,
[RequestUrl] [nvarchar](500) NOT NULL,
CONSTRAINT [PK_Traces] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO

View File

@ -179,6 +179,18 @@
"IsResource": NumberInt(0),
"Application": "0"
},
{
"_id": ObjectId("5bd9b3d868aa001661776f59"),
"ParentId": "5bd7b8445fa31256f77e4b9c",
"Name": "访问日志",
"Order": NumberInt(30),
"Icon": "fa fa-bars",
"Url": "~/Admin/Traces",
"Category": "0",
"Target": "_self",
"IsResource": NumberInt(0),
"Application": "0"
},
{
"_id": ObjectId("5bd7b8445fa31256f77e4b89"),
"ParentId": "0",

View File

@ -55,6 +55,7 @@ INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUE
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (13, 0, '系统日志', 130, 'fa fa-gears', '#', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (20, 13, '操作日志', 10, 'fa fa-edit', '~/Admin/Logs', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (21, 13, '登录日志', 20, 'fa fa-user-circle-o', '~/Admin/Logins', '0');
INSERT INTO Navigations (ID, ParentId, Name, `Order`, Icon, Url, Category) VALUES (21, 13, '访问日志', 30, 'fa fa-bars', '~/Admin/Traces', '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');

View File

@ -166,4 +166,15 @@ CREATE TABLE ResetUsers(
DisplayName VARCHAR (50) NOT NULL,
Reason VARCHAR (500) NOT NULL,
ResetTime DATETIME NOT NULL
);
CREATE TABLE Traces(
ID INTEGER PRIMARY KEY Auto_increment,
UserName VARCHAR (50) NOT NULL,
LogTime DATETIME NOT NULL,
IP VARCHAR (15) NOT NULL,
Browser VARCHAR (50),
OS VARCHAR (50),
City VARCHAR (50),
RequestUrl VARCHAR (500) NOT NULL
);

View File

@ -55,6 +55,7 @@ INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (0, '系统日志', 130, 'fa fa-gears', '#', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (currval('navigations_id_seq') - 1, 0, '操作日志', 10, 'fa fa-edit', '~/Admin/Logs', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (currval('navigations_id_seq') - 2, 0, '登录日志', 20, 'fa fa-user-circle-o', '~/Admin/Logins', '0');
INSERT INTO Navigations (ParentId, Name, "order", Icon, Url, Category) VALUES (currval('navigations_id_seq') - 3, 0, '访问日志', 30, 'fa fa-bars', '~/Admin/Traces', '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');

View File

@ -167,3 +167,14 @@ CREATE TABLE ResetUsers(
Reason VARCHAR (500) NOT NULL,
ResetTime DATE NOT NULL
);
CREATE TABLE Traces(
ID SERIAL PRIMARY KEY,
UserName VARCHAR (50) NOT NULL,
LogTime DATE NOT NULL,
IP VARCHAR (15) NOT NULL,
Browser VARCHAR (50),
OS VARCHAR (50),
City VARCHAR (50),
RequestUrl VARCHAR (500) NOT NULL
);

View File

@ -52,6 +52,7 @@ INSERT INTO [Navigations] ([ParentId], [Name], [Order], [Icon], [Url], [Category
INSERT INTO [Navigations] ([ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (0, '系统日志', 130, 'fa fa-gears', '#', '0');
INSERT INTO [Navigations] ([ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (last_insert_rowid(), '操作日志', 10, 'fa fa-edit', '~/Admin/Logs', '0');
INSERT INTO [Navigations] ([ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (last_insert_rowid() - 1, '登录日志', 20, 'fa fa-user-circle-o', '~/Admin/Logins', '0');
INSERT INTO [Navigations] ([ParentId], [Name], [Order], [Icon], [Url], [Category]) VALUES (last_insert_rowid() - 2, '访问日志', 30, 'fa fa-bars', '~/Admin/Traces', '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');

View File

@ -166,4 +166,15 @@ CREATE TABLE ResetUsers(
[DisplayName] VARCHAR (50) NOT NULL,
[Reason] VARCHAR (500) NOT NULL,
[ResetTime] DATETIME NOT NULL
);
CREATE TABLE Traces(
ID INTEGER PRIMARY KEY,
UserName VARCHAR (50) NOT NULL COLLATE NOCASE,
LogTime DATETIME NOT NULL,
IP VARCHAR (15) NOT NULL,
Browser VARCHAR (50),
OS VARCHAR (50),
City VARCHAR (50),
RequestUrl VARCHAR (500) NOT NULL
);

View File

@ -0,0 +1,10 @@
using Xunit;
namespace Bootstrap.Admin.Api.MySql
{
[Collection("MySqlContext")]
public class TracesTest : Api.TracesTest
{
public TracesTest(MySqlBAWebHost factory) : base(factory) { }
}
}

View File

@ -0,0 +1,10 @@
using Xunit;
namespace Bootstrap.Admin.Api.SQLite
{
[Collection("SQLiteContext")]
public class TracesTest : Api.TracesTest
{
public TracesTest(SQLiteBAWebHost factory) : base(factory) { }
}
}

View File

@ -0,0 +1,27 @@
using Bootstrap.DataAccess;
using Longbow.Web.Mvc;
using System;
using Xunit;
namespace Bootstrap.Admin.Api
{
public class TracesTest : ControllerTest
{
public TracesTest(BAWebHost factory) : base(factory, "api/Traces") { }
[Fact]
public async void Get_Ok()
{
var trac = new Trace() { Browser = "UnitTest", OS = "UnitTest", City = "本地连接", Ip = "::1", RequestUrl = "~/UnitTest", UserName = "UnitTest", LogTime = DateTime.Now };
trac.Save(trac);
// 菜单 系统菜单 系统使用条件
var query = "?sort=LogTime&order=desc&offset=0&limit=20&operateType=&OperateTimeStart=&OperateTimeEnd=&_=1547617573596";
var qd = await Client.GetAsJsonAsync<QueryData<Trace>>(query);
Assert.NotEmpty(qd.rows);
// clean
DbManager.Create().Execute("Delete from Traces where LogTime = @0", trac.LogTime);
}
}
}

View File

@ -15,6 +15,7 @@ namespace Bootstrap.Admin.Controllers
[InlineData("Roles", "角色管理")]
[InlineData("Menus", "菜单管理")]
[InlineData("Logs", "操作日志")]
[InlineData("Traces", "访问日志")]
[InlineData("Logins", "登录日志")]
[InlineData("FAIcon", "图标集")]
[InlineData("IconView", "图标分类")]

View File

@ -0,0 +1,10 @@
using Xunit;
namespace Bootstrap.DataAccess.MySql
{
[Collection("MySqlContext")]
public class TracesTest : DataAccess.TracesTest
{
}
}

View File

@ -0,0 +1,10 @@
using Xunit;
namespace Bootstrap.DataAccess.SQLite
{
[Collection("SQLiteContext")]
public class TracesTest : DataAccess.TracesTest
{
}
}

View File

@ -0,0 +1,43 @@
using Longbow.Web.Mvc;
using System;
using Xunit;
namespace Bootstrap.DataAccess
{
[Collection("SQLServerContext")]
public class TracesTest
{
[Fact]
public void Save_Ok()
{
var log = new Trace()
{
UserName = "UnitTest",
Browser = "UnitTest",
City = "本地连接",
OS = "UnitTest",
Ip = "::1",
LogTime = DateTime.Now,
RequestUrl = "~/Home/Index"
};
Assert.True(log.Save(log));
}
[Fact]
public void Retrieves_Ok()
{
var log = new Trace()
{
UserName = "UnitTest",
Browser = "UnitTest",
City = "本地连接",
OS = "UnitTest",
Ip = "::1",
LogTime = DateTime.Now,
RequestUrl = "~/Home/Index"
};
log.Save(log);
Assert.NotEmpty(log.Retrieves(new PaginationOption() { Limit = 20, Offset = 0, Order = "desc", Sort = "LogTime" }, null, null).Items);
}
}
}

Binary file not shown.