咱們瞭解ABP框架內部自動記錄審計日誌和登陸日誌的,可是這些信息只是在相關的內部接口裏面進行記錄,並無一個管理界面供咱們瞭解,可是其系統數據庫記錄了這些數據信息,咱們能夠爲它們設計一個查看和導出這些審計日誌和登陸日誌的管理界面。本篇隨筆繼續ABP框架的系列介紹,一步步深刻了解ABP框架的應用開發,介紹審計日誌和登陸日誌的管理。數據庫
審計日誌,設置咱們在訪問或者調用某個應用服務層接口的時候,橫切面流下的一系列操做記錄,其中記錄咱們訪問的服務接口,參數,客戶端IP地址,訪問時間,以及異常等信息,這些操做都是在ABP系統自動記錄的,若是咱們須要屏蔽某些服務類或者接口,則這些就不會記錄在裏面,不然默認是記錄的。框架
登陸日誌,這個就是用戶嘗試登陸的時候,留下的記錄信息,其中包括用戶的登陸用戶名,ID,IP地址、登陸時間,以及登陸是否成功的狀態等信息。ide
咱們查看系統數據庫,能夠看到對應這兩個部分的日誌表,以下所示。函數
在ABP框架內部基礎項目Abp裏面,咱們能夠看到對應的領域對象實體和Store管理類,不過並無在應用層的對應服務和相關的DTO,咱們須要實現一個審計日誌和登錄日誌的管理功能界面,界面效果以下所示。工具
咱們搜索ABP項目,查找到審計日誌的相關類(包含領域對象實體和Store管理類),以下界面截圖。優化
一樣對於系統登陸日誌對象,咱們查找到對應的領域實體和對應的Manger業務邏輯類。this
這些也就表明它們都有底層的實現,可是沒有服務層應用和DTO對象,所以咱們須要擴展這些內容纔可以管理顯示這些記錄信息。spa
前面介紹過,默認的通常應用服務層和接口,都是會進行審計記錄寫入的,若是咱們須要屏蔽某些應用服務層或者接口,不進行審計信息的記錄,那麼須要使用特性標記[DisableAuditing]來管理。設計
如咱們針對審計日誌應用層接口的訪問,咱們不想讓它多餘的記錄,那麼就設置這個標記便可。3d
或者屏蔽某些接口
另外,若是咱們不想公佈某些特殊的接口訪問,那麼咱們能夠經過標記 [RemoteService(false)] 進行屏蔽,這樣在Web API層就不會公佈對應的接口了。
如對於審計日誌的記錄,增刪改咱們都不容許客戶端進行操做,那麼咱們把對應的應用服務層接口屏蔽便可。
前面介紹了,審計日誌和登錄日誌的處理,Abp系統只是作了一部分底層的內容,咱們若是進行這些信息的管理,咱們須要完善它,增長對應的DTO類和應用服務層接口和接口實現。
首先咱們根據底層的領域實體對象的屬性,複製過來做爲對應DTO對象的屬性,並增長對應的分頁條件DTO對象,因爲咱們不須要進行建立,所以不須要增長Create***Dto對象類。
如對於審計日誌的DTO對象,咱們定義以下所示(主要複製領域對象的屬性)。
而分頁處理的DTO對象以下所示,咱們主要增長一個用戶名和建立時間區間的條件。
對於登陸日誌的DTO對象,咱們依葫蘆畫瓢,也是如此操做便可。
登陸日誌的分頁對象Dto以下所示、
完善了這些DTO對象,下一步咱們須要建立對應的應用服務層類,這樣咱們才能在客戶端經過Web API獲取對應的數據。
首先咱們來定義審計日誌應用服務類,以下所示。
[DisableAuditing] //屏蔽這個AppService的審計功能 [AbpAuthorize] public class AuditLogAppService : AsyncCrudAppService<AuditLog, AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto> { private readonly IRepository<AuditLog, long> _repository; private readonly IAuditingStore _stroe; private readonly IRepository<User, long> _userRepository; public AuditLogAppService(IRepository<AuditLog, long> repository, IAuditingStore stroe, IRepository<User, long> userRepository) : base(repository) { _repository = repository; _stroe = stroe; _userRepository = userRepository; } ......
其中咱們須要IRepository<User, long>用來轉義用戶ID爲對應的用戶名,這樣對於咱們顯示有幫助。
默認來講,這個應用服務層已經具備常規的增刪改查、分頁等基礎接口了,可是咱們不須要對外公佈增刪改接口,咱們須要重寫實現把它屏蔽。
/// <summary> /// 屏蔽建立接口 /// </summary> [RemoteService(false)] public override Task<AuditLogDto> Create(AuditLogDto input) { return base.Create(input); } /// <summary> /// 屏蔽更新接口 /// </summary> [RemoteService(false)] public override Task<AuditLogDto> Update(AuditLogDto input) { return base.Update(input); } /// <summary> /// 屏蔽刪除接口 /// </summary> [RemoteService(false)] public override Task Delete(EntityDto<long> input) { return base.Delete(input); }
那麼咱們就剩下GetAll和Get兩個方法了,咱們若是不須要轉義特殊內容,咱們就能夠不重寫它,可是咱們這裏須要對用戶ID轉義爲用戶名稱,那麼須要進行一個處理,以下所示。
[DisableAuditing] public override Task<PagedResultDto<AuditLogDto>> GetAll(AuditLogPagedDto input) { var result = base.GetAll(input); foreach (var item in result.Result.Items) { ConvertDto(item);//對用戶名稱進行解析 } return result; } [DisableAuditing] public override Task<AuditLogDto> Get(EntityDto<long> input) { var result = base.Get(input); ConvertDto(result.Result); return result; } /// <summary> /// 對記錄進行轉義 /// </summary> /// <param name="item">dto數據對象</param> /// <returns></returns> protected virtual void ConvertDto(AuditLogDto item) { //用戶名稱轉義 if (item.UserId.HasValue) { item.UserName = _userRepository.Get(item.UserId.Value).UserName; } //IP地址轉義 if (!string.IsNullOrEmpty(item.ClientIpAddress)) { item.ClientIpAddress = item.ClientIpAddress.Replace("::1", "127.0.0.1"); } }
這裏主要就用戶ID和IP地址進行一個正常的轉義處理,這個也是咱們常規接口須要處理的一種常見的狀況之一。
排序咱們是以執行時間進行排序,倒序顯示便可,所以重寫排序函數。
/// <summary> /// 自定義排序處理 /// </summary> /// <param name="query"></param> /// <param name="input"></param> /// <returns></returns> protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog> query, AuditLogPagedDto input) { return base.ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime);//時間降序 }
通常狀況下,咱們就基本完成了這個模塊的處理了,這樣咱們在界面上在花點功夫就能夠調用這個API接口進行顯示信息了,以下界面是我編寫的審計日誌分頁列表顯示界面。
明細展現界面以下所示。
上面列表界面管理中,若是咱們還可以以用戶進行過濾,那就更好了,所以須要添加一個用戶名進行過濾(注意不是用戶ID),系統表裏面沒有用戶名稱。
若是咱們須要用戶名稱過濾,以下界面所示。
那麼咱們就須要在應用服務層的過濾函數裏面處理相應的規則了。
咱們先建立一個審計日誌和用戶信息的集合對象,以下所示。
/// <summary> /// 審計日誌和用戶的領域對象集合 /// </summary> public class AuditLogAndUser { public AuditLog AuditLog { get;set;} public User User { get; set; } }
而後在 CreateFilteredQuery 函數裏面進行處理,以下代碼所示。
/// <summary> /// 自定義條件處理 /// </summary> /// <param name="input">分頁查詢Dto對象</param> /// <returns></returns> protected override IQueryable<AuditLog> CreateFilteredQuery(AuditLogPagedDto input) { //構建關聯查詢Query var query = from auditLog in Repository.GetAll() join user in _userRepository.GetAll() on auditLog.UserId equals user.Id into userJoin from joinedUser in userJoin.DefaultIfEmpty() where auditLog.UserId.HasValue select new AuditLogAndUser { AuditLog = auditLog, User = joinedUser }; //過濾分頁條件 return query .WhereIf(!string.IsNullOrEmpty(input.UserName), t => t.User.UserName.Contains(input.UserName)) .WhereIf(input.ExecutionTimeStart.HasValue, s => s.AuditLog.ExecutionTime >= input.ExecutionTimeStart.Value) .WhereIf(input.ExecutionTimeEnd.HasValue, s => s.AuditLog.ExecutionTime <= input.ExecutionTimeEnd.Value) .Select(s => s.AuditLog); }
上面其實就是先經過EF的關聯表查詢,返回一個集合記錄,而後在判斷用戶名是否在集合裏面,最後返回所需的實體對象列表。
這個EF的關聯表查詢很是關鍵,這個也是咱們聯合查詢的精髓所在,經過LINQ的方式,能夠很方便實現關聯表的查詢處理並得到對應的結果。
而對於用戶登陸日誌,因爲系統記錄了用戶名,那麼過濾用戶名,這不須要這麼大費周章關聯表進行處理,只須要判斷數據庫字段對應狀況便可,這種方便不少。
/// <summary> /// 自定義條件處理 /// </summary> /// <param name="input"></param> /// <returns></returns> protected override IQueryable<UserLoginAttempt> CreateFilteredQuery(UserLoginAttemptPagedDto input) { return base.CreateFilteredQuery(input) .WhereIf(!string.IsNullOrEmpty(input.UserNameOrEmailAddress), t => t.UserNameOrEmailAddress.Contains(input.UserNameOrEmailAddress)) .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value) .WhereIf(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value); }
一樣系統用戶登陸日誌界面以下所示。
用戶登陸明細界面效果以下所示。
以上就是對於審計日誌和用戶登陸日誌的擴展實現,包括了對相關DTO的增長和實現應用服務層接口,以及對Web API Caller層的實現。
/// <summary> /// 審計日誌的Web API調用處理 /// </summary> public class AuditLogApiCaller : AsyncCrudApiCaller<AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto> { /// <summary> /// 提供單件對象使用 /// </summary> public static AuditLogApiCaller Instance { get { return Singleton<AuditLogApiCaller>.Instance; } } /// <summary> /// 默認構造函數 /// </summary> public AuditLogApiCaller() { this.DomainName = "AuditLog";//指定域對象名稱,用於組裝接口地址 } }
因爲只是部分實現功能,咱們仍是能夠基於前面介紹開發模式(利用代碼生成工具Database2Sharp快速生成)來實現ABP優化框架類文件的生成,以及界面代碼的生成,而後進行必定的調整就是本項目的代碼了。
代碼生成工具的ABP項目代碼模板,和基於ABPWinform界面代碼的模板,是我基於實際項目的反覆優化和驗證,並儘可能減小冗餘代碼而完成的一種快速開發方式,基於這樣開發方式能夠大大減小項目開發的難度,提升開發效率,並徹底匹配整個框架的須要,是一種很是愜意的快速開發方式。