1. 程式人生 > >基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(三)

基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(三)

## 系列文章 1. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 使用 abp cli 搭建專案](https://www.cnblogs.com/meowv/p/12896177.html)** 2. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 給專案瘦身,讓它跑起來](https://www.cnblogs.com/meowv/p/12896898.html)** 3. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 完善與美化,Swagger登場](https://www.cnblogs.com/meowv/p/12909558.html)** 4. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 資料訪問和程式碼優先](https://www.cnblogs.com/meowv/p/12913676.html)** 5. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 自定義倉儲之增刪改查](https://www.cnblogs.com/meowv/p/12916613.html)** 6. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 統一規範API,包裝返回模型](https://www.cnblogs.com/meowv/p/12924409.html)** 7. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 再說Swagger,分組、描述、小綠鎖](https://www.cnblogs.com/meowv/p/12924859.html)** 8. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 接入GitHub,用JWT保護你的API](https://www.cnblogs.com/meowv/p/12935693.html)** 9. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 異常處理和日誌記錄](https://www.cnblogs.com/meowv/p/12943699.html)** 10. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 使用Redis快取資料](https://www.cnblogs.com/meowv/p/12956696.html)** 11. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 整合Hangfire實現定時任務處理](https://www.cnblogs.com/meowv/p/12961014.html)** 12. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 用AutoMapper搞定物件對映](https://www.cnblogs.com/meowv/p/12966092.html)** 13. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(一)](https://www.cnblogs.com/meowv/p/12971041.html)** 14. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(二)](https://www.cnblogs.com/meowv/p/12974439.html)** 15. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 定時任務最佳實戰(三)](https://www.cnblogs.com/meowv/p/12980301.html)** 16. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(一)](https://www.cnblogs.com/meowv/p/12987623.html)** 17. **[基於 abp vNext 和 .NET Core 開發部落格專案 - 部落格介面實戰篇(二)](https://www.cnblogs.com/meowv/p/12994914.html)** --- [上篇文章](https://www.cnblogs.com/meowv/p/12994914.html)完成了分類和標籤頁面相關的共6個介面,本篇繼續來寫部落格增刪改查API的業務。 供前端查詢用的介面還剩下一個,這裡先補上。 ## 友鏈列表 ![0](https://img2020.cnblogs.com/blog/891843/202006/891843-20200601170002317-1723090056.png) 分析:返回標題和對應的連結即可,傳輸物件`FriendLinkDto.cs`。 ```CSharp //FriendLinkDto.cs namespace Meowv.Blog.Application.Contracts.Blog { public class FriendLinkDto { /// /// 標題 /// public string Title { get; set; } /// /// 連結 /// public string LinkUrl { get; set; } } } ``` 新增查詢友鏈列表介面和快取介面。 ```CSharp //IBlogService.FriendLink.cs using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using System.Collections.Generic; using System.Threading.Tasks; namespace Meowv.Blog.Application.Blog { public partial interface IBlogService { /// /// 查詢友鏈列表 ///
/// Task>> QueryFriendLinksAsync(); } } ``` ```CSharp //IBlogCacheService.FriendLink.cs using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Meowv.Blog.Application.Caching.Blog { public partial interface IBlogCacheService { /// /// 查詢友鏈列表 ///
/// /// Task>> QueryFriendLinksAsync(Func>>> factory); } } ``` 接下來,實現他們。 ```CSharp //BlogCacheService.FriendLink.cs using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using System; using System.Collections.Generic; using System.Threading.Tasks; using static Meowv.Blog.Domain.Shared.MeowvBlogConsts; namespace Meowv.Blog.Application.Caching.Blog.Impl { public partial class BlogCacheService { private const string KEY_QueryFriendLinks = "Blog:FriendLink:QueryFriendLinks"; /// /// 查詢友鏈列表 ///
/// /// public async Task>> QueryFriendLinksAsync(Func>>> factory) { return await Cache.GetOrAddAsync(KEY_QueryFriendLinks, factory, CacheStrategy.ONE_DAY); } } } ``` ```CSharp //BlogService.FriendLink.cs using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.Domain.Blog; using Meowv.Blog.ToolKits.Base; using System.Collections.Generic; using System.Threading.Tasks; namespace Meowv.Blog.Application.Blog.Impl { public partial class BlogService { /// /// 查詢友鏈列表 /// /// public async Task>> QueryFriendLinksAsync() { return await _blogCacheService.QueryFriendLinksAsync(async () => { var result = new ServiceResult>(); var friendLinks = await _friendLinksRepository.GetListAsync(); var list = ObjectMapper.Map, IEnumerable>(friendLinks); result.IsSuccess(list); return result; }); } } } ``` 直接查詢所有的友鏈資料,這裡使用前面講到的AutoMapper處理物件對映,將`IEnumerable`轉換為`IEnumerable`。 在`MeowvBlogAutoMapperProfile.cs`中新增一條配置:`CreateMap();`,在`BlogController`中新增API。 ```CSharp /// /// 查詢友鏈列表 /// /// [HttpGet] [Route("friendlinks")] public async Task>> QueryFriendLinksAsync() { return await _blogService.QueryFriendLinksAsync(); } ``` 編譯執行,開啟查詢友鏈的API,此時沒資料,手動新增幾條資料進去再試試吧。 ![2](https://img2020.cnblogs.com/blog/891843/202006/891843-20200601172550001-1793416839.png) ## 文章管理 ![3](https://img2020.cnblogs.com/blog/891843/202006/891843-20200601213047469-759323607.png) 後臺文章管理包含:文章列表、新增、更新、刪除文章,接下來依次完成這些介面。 ### 文章列表 這裡的文章列表和前臺的文章列表差不多,就是多了一個Id,以供編輯和刪除使用,所以可以新建一個模型類`QueryPostForAdminDto`繼承`QueryPostDto`,新增`PostBriefForAdminDto`繼承`PostBriefDto`同時新增一個欄位主鍵Id。 在`QueryPostForAdminDto`中隱藏基類成員`Posts`,使用新的接收型別:`IEnumerable`。 ```CSharp //PostBriefForAdminDto.cs namespace Meowv.Blog.Application.Contracts.Blog { public class PostBriefForAdminDto : PostBriefDto { /// /// 主鍵 /// public int Id { get; set; } } } ``` ```CSharp //QueryPostForAdminDto.cs using System.Collections.Generic; namespace Meowv.Blog.Application.Contracts.Blog { public class QueryPostForAdminDto : QueryPostDto { /// /// Posts /// public new IEnumerable Posts { get; set; } } } ``` 新增分頁查詢文章列表的介面:`QueryPostsForAdminAsync()`,關於後臺的一些介面就不新增快取了。 ```CSharp //IBlogService.Admin.cs using Meowv.Blog.Application.Contracts; using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using System.Threading.Tasks; namespace Meowv.Blog.Application.Blog { public partial interface IBlogService { /// /// 分頁查詢文章列表 /// /// /// Task>> QueryPostsForAdminAsync(PagingInput input); } } ``` 然後實現這個介面。 ```CSharp //BlogService.Admin.cs using Meowv.Blog.Application.Contracts; using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using Meowv.Blog.ToolKits.Extensions; using System.Linq; using System.Threading.Tasks; namespace Meowv.Blog.Application.Blog.Impl { public partial class BlogService { /// /// 分頁查詢文章列表 /// /// /// public async Task>> QueryPostsForAdminAsync(PagingInput input) { var result = new ServiceResult>(); var count = await _postRepository.GetCountAsync(); var list = _postRepository.OrderByDescending(x => x.CreationTime) .PageByIndex(input.Page, input.Limit) .Select(x => new PostBriefForAdminDto { Id = x.Id, Title = x.Title, Url = x.Url, Year = x.CreationTime.Year, CreationTime = x.CreationTime.TryToDateTime() }) .GroupBy(x => x.Year) .Select(x => new QueryPostForAdminDto { Year = x.Key, Posts = x.ToList() }).ToList(); result.IsSuccess(new PagedList(count.TryToInt(), list)); return result; } } } ``` 實現邏輯也非常簡單和之前一樣,就是在`Select`的時候多了一個`Id`,新增一個新的Controller:`BlogController.Admin.cs`,新增這個介面。 ```CSharp //BlogController.Admin.cs using Meowv.Blog.Application.Contracts; using Meowv.Blog.Application.Contracts.Blog; using Meowv.Blog.ToolKits.Base; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using static Meowv.Blog.Domain.Shared.MeowvBlogConsts; namespace Meowv.Blog.HttpApi.Controllers { public partial class BlogController { /// /// 分頁查詢文章列表 /// /// /// [HttpGet] [Authorize] [Route("admin/posts")] [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)] public async Task>> QueryPostsForAdminAsync([FromQuery] PagingInput input) { return await _blogService.QueryPostsForAdminAsync(input); } } } ``` 因為是後臺的介面,所以加上`AuthorizeAttribute`,指定介面組為`GroupName_v2`,引數方式為`[FromQuery]`。 當沒有進行授權的時候,是無法訪問介面的。 ![4](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602101442280-748284116.png) ### 新增文章 ![5](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602102546980-1951417062.png) 在做新增文章的時候要注意幾點,不是單純的新增文章資料就結束了,要指定文章分類,新增文章的標籤。新增標籤我這裡是從標籤庫中去取得資料,只存標籤Id,所以新增標籤的時候就可能存在添加了標籤庫中已有的標籤。 新建一個新增和更新文章的通用輸出引數模型類,起名:`EditPostInput`,繼承`PostDto`,然後新增標籤Tags欄位,返回型別`IEnumerable`。 ```CSharp //EditPostInput.cs using System.Collections.Generic; namespace Meowv.Blog.Application.Contracts.Blog.Params { public class EditPostInput : PostDto { /// /// 標籤列表 /// public IEnumerable Tags { get; set; } } } ``` 新增新增文章的介面:`InsertPostAsync`。 ```CSharp /// /// 新增文章 /// /// /// Task InsertPostAsync(EditPostInput input); ``` 然後去實現這個介面,實現之前,配置AutoMapper實體對映。 ```CSharp CreateMap().ForMember(x => x.Id, opt => opt.Ignore()); ``` 將`EditPostInput`轉換為`Post`,並且忽略`Id`欄位。 ```CSharp /// /// 新增文章 /// /// /// public async Task InsertPostAsync(EditPostInput input) { var result = new ServiceResult(); var post = ObjectMapper.Map(input); post.Url = $"{post.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{post.Url}/"; await _postRepository.InsertAsync(post); var tags = await _tagRepository.GetListAsync(); var newTags = input.Tags .Where(item => !tags.Any(x => x.TagName.Equals(item))) .Select(item => new Tag { TagName = item, DisplayName = item }); await _tagRepository.BulkInsertAsync(newTags); var postTags = input.Tags.Select(item => new PostTag { PostId = post.Id, TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id }); await _postTagRepository.BulkInsertAsync(postTags); result.IsSuccess(ResponseText.INSERT_SUCCESS); return result; } ``` URL欄位,根據建立時間按照`yyyy/MM/dd/name/`格式拼接。 然後找出是否有新標籤,有的話批量新增至標籤表。 再根據`input.Tags`構建`PostTag`列表,也進行批量儲存,這樣才算是新增好一篇文章,最後輸出`ResponseText.INSERT_SUCCESS`常量,提示成功。 在`BlogController.Admin.cs`新增API。 ```CSharp /// /// 新增文章 /// /// /// [HttpPost] [Authorize] [Route("post")] [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)] public async Task InsertPostAsync([FromBody] EditPostInput input) { return await _blogService.InsertPostAsync(input); } ``` ![6](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602112430082-1494392218.png) ### 更新文章 更新操作和新增操作輸入引數一樣,只新增一個Id用來標識更新那篇文章,新增`UpdatePostAsync`更新文章介面。 ```CSharp /// /// 更新文章 /// /// /// /// Task UpdatePostAsync(int id, EditPostInput input); ``` 同樣的實現這個介面。 ```CSharp /// /// 更新文章 /// /// /// /// public async Task UpdatePostAsync(int id, EditPostInput input) { var result = new ServiceResult(); var post = await _postRepository.GetAsync(id); post.Title = input.Title; post.Author = input.Author; post.Url = $"{input.CreationTime.ToString(" yyyy MM dd ").Replace(" ", "/")}{input.Url}/"; post.Html = input.Html; post.Markdown = input.Markdown; post.CreationTime = input.CreationTime; post.CategoryId = input.CategoryId; await _postRepository.UpdateAsync(post); var tags = await _tagRepository.GetListAsync(); var oldPostTags = from post_tags in await _postTagRepository.GetListAsync() join tag in await _tagRepository.GetListAsync() on post_tags.TagId equals tag.Id where post_tags.PostId.Equals(post.Id) select new { post_tags.Id, tag.TagName }; var removedIds = oldPostTags.Where(item => !input.Tags.Any(x => x == item.TagName) && tags.Any(t => t.TagName == item.TagName)) .Select(item => item.Id); await _postTagRepository.DeleteAsync(x => removedIds.Contains(x.Id)); var newTags = input.Tags .Where(item => !tags.Any(x => x.TagName == item)) .Select(item => new Tag { TagName = item, DisplayName = item }); await _tagRepository.BulkInsertAsync(newTags); var postTags = input.Tags .Where(item => !oldPostTags.Any(x => x.TagName == item)) .Select(item => new PostTag { PostId = id, TagId = _tagRepository.FirstOrDefault(x => x.TagName == item).Id }); await _postTagRepository.BulkInsertAsync(postTags); result.IsSuccess(ResponseText.UPDATE_SUCCESS); return result; } ``` `ResponseText.UPDATE_SUCCESS`是常量更新成功。 先根據Id查詢到資料庫中的這篇文章資料,然後根據input引數,修改需要修改的資料,最後儲存。 注意的是,如果修改的時候修改了標籤,有可能新增也有可能刪除,也許會又有新增又有刪除。 這時候就需要注意,這裡做了一個比較通用的方法,找到資料庫中當前文章Id的所有Tags,然後根據引數`input.Tags`可以找出被刪掉的標籤的PostTags的Id,呼叫刪除方法刪掉即可,同時也可以獲取到新增的標籤,批量進行儲存。 完成上面操作後,才儲存新加標籤與文章對應的資料,最後提示更新成功,在`BlogController.Admin`新增API。 ```CSharp /// /// 更新文章 /// /// /// /// [HttpPut] [Authorize] [Route("post")] [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)] public async Task UpdatePostAsync([Required] int id, [FromBody] EditPostInput input) { return await _blogService.UpdatePostAsync(id, input); } ``` `[HttpPut]`指定請求方式為`put`請求,一般需要修改用put,新增用post。 `[Required]`指定引數id必填且是FromQuery的方式,input為`[FromBody]`。 更新一下上面新增的資料試試。 ![7](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602221038444-846665879.png) ![8](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602221351347-498118468.png) ### 刪除文章 刪除相對來說就非常簡單了,一般刪除都會做邏輯刪除,就是避免某些手殘刪除了,有找回的餘地,我們這裡就直接Delete了,也沒什麼重要資料。 新增介面:`DeletePostAsync`。 ```CSharp /// /// 刪除文章 /// /// /// Task DeletePostAsync(int id); ``` 實現介面。 ```CSharp /// /// 刪除文章 /// /// /// public async Task DeletePostAsync(int id) { var result = new ServiceResult(); var post = await _postRepository.GetAsync(id); if (null == post) { result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("Id", id)); return result; } await _postRepository.DeleteAsync(id); await _postTagRepository.DeleteAsync(x => x.PostId == id); result.IsSuccess(ResponseText.DELETE_SUCCESS); return result; } ``` 刪除的時候同樣去查詢一下資料,來判斷是否存在。 `ResponseText.DELETE_SUCCESS`是新增的常量刪除成功,刪除成功同時也要將post_tags表的標籤對應關係也幹掉才算完整,在BlogController.Admin新增API。 ```CSharp /// /// 刪除文章 /// /// /// [HttpDelete] [Authorize] [Route("post")] [ApiExplorerSettings(GroupName = Grouping.GroupName_v2)] public async Task DeletePostAsync([Required] int id) { return await _blogService.DeletePostAsync(id); } ``` `[HttpDelete]`指定請求方式是刪除資源,`[Required]`指定引數Id必填。 刪掉上面新增的文章看看效果。 ![9](https://img2020.cnblogs.com/blog/891843/202006/891843-20200602223007548-1633020549.png) 至此,完成了部落格文章的增刪改介面,未完待續... 開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial --- **搭配下方課程學習更佳 ↓ ↓ ↓** [http://gk.link/a/10iQ7](http://gk.link/a/10iQ7) ![7](https://img2020.cnblogs.com/blog/891843/202006/891843-20200603101718800-1586400