上一篇我們使用Swagger新增了介面文件,使用Jwt完成了授權,本章我們簡答介紹一下RESTful風格的WebAPI開發過程中涉及到的一些知識點,並完善一下尚未完成的功能
.NET下的WebAPI是一種無限接近RESTful風格的框架,RESTful風格它有著自己的一套理論,它的大概意思就是說使用標準的Http方法,將Web系統的服務抽象為資源。稍微具體一點的介紹可以檢視阮一峰的這篇文章RESTful API最佳實踐。我們這裡就分幾部分介紹一下構建RESTful API需要了解的基礎知識
注:本章介紹部分的內容大多是基於solenovex的使用 ASP.NET Core 3.x 構建 RESTful Web API視訊內容的整理,若想進一步瞭解相關知識,請檢視原視訊
一、HTTP方法
1、什麼是HTTP方法
HTTP方法是對Web伺服器的說明,說明如何處理請求的資源。HTTP1.0 定義了三種請求方法: GET, POST 和 HEAD方法;HTTP1.1 新增了六種請求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
2、常用的HTTP方法
-
GET:通常用來獲取資源;GET請求會返回請求路徑對應的資源,但它分兩種情況:
①獲取單個資源,通過使用URL的形式帶上唯一標識,示例:api/Articles/{ArticleId};
②獲取集合資源中符合條件的資源,會通過QueryString的形式在URL後面新增?查詢條件作為篩選條件,示例:api/Articles?title=WebAPI
-
POST:通常用來建立資源;POST的引數會放在請求body中,POST請求應該返回新建立的資源以及可以獲取該資源的唯一標識URL,示例:api/Articles/{新增的ArticleId}
-
DELETE:通常用來移除/刪除對應路徑的資源;通過使用URL的形式帶上唯一標識,或者和GET一樣使用QueryString,處理完成後通常不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId};
-
PUT:通常用來完全替換對應路徑的資源資訊;POST的引數會放在請求body中,且為一個完整物件,示例:api/Articles/{ArticleId};與此同時,它分兩類情況:
①對應的資源不存在,則新增對應的資源,後續處理和POST一樣;
②對應的資源存在,則替換對應的資源,處理完成不需要返回資訊,只返回狀態碼204
-
PATCH:通常用來更新對應路徑資源的區域性資訊;PATCH的引數會放在請求頭中,處理完成後通常不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId};
綜上:給出一張圖例,來自solenovex,使用 ASP.NET Core 3.x 構建 RESTful Web API
3、安全性和冪等性
安全性是指方法執行後不會改變資源的表述;冪等性是指方法無論執行多少次都會得到相同的結果
二、狀態碼相關
1、狀態碼
HTTP狀態碼是表示Web伺服器響應狀態的3位數字程式碼。通常會以第一位數字為區分
1xx:屬於資訊性的狀態碼,WebAPI不使用
2xx:表示請求執行成功,常用的有200—請求成功,201—建立成功,204—請求成功無返回資訊,如刪除
3xx:用於跳轉,如告訴搜尋引擎,網址已改變。大多數WebAPI不需要使用這類狀態碼
4xx:表示客戶端錯誤
- 400:Bad Request,表示API使用者傳送到伺服器的請求存在錯誤;
- 401:Unauthorized,表示沒有提供授權資訊,或者授權資訊不正確;
- 403:Forbidden,表示身份認證成功,但是無許可權訪問請求的資源
- 404:Not Found,表示請求的資源不存在
- 405:Method not allowed,表示使用了不被支援的HTTP方法
- 406:Not Acceptable,表示API使用者請求的格式WebAPI不支援,且WebAPI不提供預設的表述格式
- 409:Conflict,表示衝突,一般用來表述併發問題,如修改資源期間,資源被已經被更新了
- 415:Unsupported Media Type,與406相反,表示伺服器接受的資源WebAPI不支援
- 422:Unprocessable Entity,表示伺服器已經解析了內容,但是無法處理,如實體驗證錯誤
5xx:表示伺服器錯誤
- 500:INternal Server Error:表示伺服器發生了錯誤,客戶端無法處理
2、錯誤與故障
基於HTTP請求狀態碼,我們需要了解一下錯誤和故障的區別
錯誤:API正常工作,但是API使用者請求傳遞的資料不合理,所以請求被拒絕。對應4xx錯誤;
故障:API工作異常,API使用者請求是合理的,但是API無法響應。對應5xx錯誤
3、故障處理
我們可以在非開發環境進行如下配置,以確保生產環境異常時能檢視到相關異常說明,通常這裡會寫入日誌記錄異常,我們會在後面的章節新增日誌功能,這裡先修改如下:
三、WebAPI相關
1、內容協商
- 什麼是內容協商?即當有多種表述格式(Json/Xml等)可用時,選取最佳的一個進行表述。簡單來說就是請求什麼格式,服務端就返回什麼格式的資料;
- 如何設定內容協商?首先我們要從服務端的角度區分輸出和輸入,輸出表示客戶端發出請求服務端響應資料;輸入表示客戶端提交資料服務端對其進行處理;舉例來說,Get就是輸出,Post就是輸入
- 先看輸出:在Http請求的Header中有一個Accept Header屬性,如該屬性設定的是application/json,那麼API返回的就應該是Json格式的;在ASP.NET Core中負責響應輸出格式的就是Output Formatters物件
- 再看輸入:HTTP請求的輸入格式對應的是Content-Type Header屬性,ASP.NET Core中負責響應輸入格式的就是Input Formatters物件
PS:如果沒有設定請求格式,就返回預設格式;而如果請求的格式不存在,則應當返回406狀態碼;
2、內容協商設定
ASP.NET Core目前的設定是僅返回Json格式資訊,不支援XML;如果請求的是XML或沒有設定,它同樣會返回Json;如果希望關閉此項設定,即不存在返回406狀態碼,可以在Controller服務註冊時新增如下設定;
而如果希望支援輸出和輸入都支援XML格式,可以配置如下:
3、物件繫結
客戶端資料可以通過多種方式傳遞給API,Binding Source Attribute則是負責處理繫結的物件,它會為告知Model的繫結引擎,從哪裡可以找到繫結源,Binding Source Attribute一共有六種繫結資料來源,如下:
- FromBody:從請求的Body中獲取繫結資料
- FromForm:從請求的Body中的form獲取繫結資料
- FromHeader:從請求的Header中獲取繫結資料
- FromQuery:從QueryString引數中獲取繫結資料
- FromRoute:從當前請求的路由中獲取繫結資料
- FromService:從作為Action引數而注入的服務中獲取繫結資料
4、ApiController特性
ASP.NET Core WebAPI中我們通常會使用[ApiController]特性來修飾我們的Controller物件,該特性為了更好的適應API方法,對上述分類規則進行了修改,修改如下:
- FormBody:通常用來推斷複雜型別的引數
- FromForm:通常用來推斷IFormFilr和IFormFileColllection型別的Action引數,即檔案上傳相對應的引數
- FromRoute:通常用來推斷Action的引數名和路由模板中的引數名一致的情況
- FromQuery:用來推斷其他的Action引數
一些特殊情況,需要手動指明物件的來源,如在HttpGet方法中,查詢引數是一個複雜的類型別,則ApiController物件會預設繫結源為請求body, 這時候就需要手動指明繫結源為FromQuery;
5、輸入驗證
通常我們會使用一些驗證規則對客戶端的輸入內容進行限制,像使用者名稱不能包含特殊字元,使用者名稱長度等
1、驗證規則
WebAPI中內建了一組名為Data Annotations的驗證規則,像之前我們新增的[Required],[StringLength...]都屬於這個型別。或者我們可以自定義一個類,實現IValidatableObject介面,對多個欄位進行限制;當然我們也可以針對類或者是屬性自定義一些驗證規則,需要繼承ValidationAttribute類重寫IsValid方法
2、驗證檢查
檢查時會使用ModelState物件,它是一個字典,包含model的狀態和model的繫結驗證資訊;同時它還包含針對每個提交的屬性值的錯誤資訊的集合,每當有請求進來的時候,定義好的驗證規則就會被檢查。如果驗證不通過,ModelState.IsValid()就會返回false;
3、報告驗證錯誤
如發生驗證錯誤,應當返回Unprocessable Entity 422錯誤,並在響應的body中包含驗證錯誤資訊;ASP.NET Core已經定義好了這部分內容,當Controller使用[ApiController]屬性進行註解時,如果遇到錯誤,那麼將會自返回400錯誤狀態碼
四、完成Controller基礎功能
controller功能的實現是大多基於對BLL層的引用,雖然我們在第3小結中已經實現了資料層和邏輯層的基礎功能,但在Controller實現時還是發現了很多不合理的地方,所以調整了很多內容,下面我們依次來看一下
1、UserController
1、首先對Model的層進行了調整,調整了出生日期和性別的預設值
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model
{
/// <summary>
/// 使用者
/// </summary>
public class User : BaseEntity
{
/// <summary>
/// 賬戶
/// </summary>
[Required, StringLength(40)]
public string Account { get; set; }
/// <summary>
/// 密碼
/// </summary>
[Required, StringLength(200)]
public string Password { get; set; }
/// <summary>
/// 頭像
/// </summary>
public string ProfilePhoto { get; set; }
/// <summary>
/// 出生日期
/// </summary>
public DateTime BirthOfDate { get; set; } = DateTime.Today;
/// <summary>
/// 性別
/// </summary>
public Gender Gender { get; set; } = Gender.保密;
/// <summary>
/// 使用者等級
/// </summary>
public Level Level { get; set; } = Level.普通使用者;
/// <summary>
/// 粉絲數
/// </summary>
public int FansNum { get; set; }
/// <summary>
/// 關注數
/// </summary>
public int FocusNum { get; set; }
}
}
對ViewModel進行了調整,如下:
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 使用者註冊
/// </summary>
public class RegisterViewModel
{
/// <summary>
/// 賬號
/// </summary>
[Required, StringLength(40, MinimumLength = 4)]
[RegularExpression(@"/^([\u4e00-\u9fa5]{2,4})|([A-Za-z0-9_]{4,16})|([a-zA-Z0-9_\u4e00-\u9fa5]{3,16})$/")]
public string Account { get; set; }
/// <summary>
/// 密碼
/// </summary>
[Required, StringLength(20, MinimumLength = 6)]
public string Password { get; set; }
/// <summary>
/// 確認密碼
/// </summary>
[Required, Compare(nameof(Password))]
public string RequirePassword { get; set; }
}
}
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 使用者登入
/// </summary>
public class LoginViewModel
{
/// <summary>
/// 使用者名稱稱
/// </summary>
[Required, StringLength(40, MinimumLength = 4)]
[RegularExpression(@"/^([\u4e00-\u9fa5]{2,4})|([A-Za-z0-9_]{4,16})|([a-zA-Z0-9_\u4e00-\u9fa5]{3,16})$/")]
public string Account { get; set; }
/// <summary>
/// 使用者密碼
/// </summary>
[Required, StringLength(20, MinimumLength = 6), DataType(DataType.Password)]
public string Password { get; set; }
}
}
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 修改使用者密碼
/// </summary>
public class ChangePwdViewModel
{
/// <summary>
/// 舊密碼
/// </summary>
[Required]
public string OldPassword { get; set; }
/// <summary>
/// 新密碼
/// </summary>
[Required]
public string NewPassword { get; set; }
/// <summary>
/// 確認新密碼
/// </summary>
[Required, Compare(nameof(NewPassword))]
public string RequirePassword { get; set; }
}
}
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 修改使用者資料
/// </summary>
public class ChangeUserInfoViewModel
{
/// <summary>
/// 賬號
/// </summary>
public string Account { get; set; }
/// <summary>
/// 出生日期
/// </summary>
[DataType(DataType.Date)]
public DateTime BirthOfDate { get; set; }
/// <summary>
/// 性別
/// </summary>
public Gender Gender { get; set; }
}
}
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 使用者詳細資訊
/// </summary>
public class UserDetailsViewModel
{
/// <summary>
/// 賬號
/// </summary>
public string Account { get; set; }
/// <summary>
/// 頭像
/// </summary>
public string ProfilePhoto { get; set; }
/// <summary>
/// 年齡
/// </summary>
public int Age { get; set; }
/// <summary>
/// 性別
/// </summary>
public string Gender { get; set; }
/// <summary>
/// 使用者等級
/// </summary>
public string Level { get; set; }
/// <summary>
/// 粉絲數
/// </summary>
public int FansNum { get; set; }
/// <summary>
/// 關注數
/// </summary>
public int FocusNum { get; set; }
}
}
2、IBLL和BLL層調整如下:
using System;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using System.Threading.Tasks;
namespace BlogSystem.IBLL
{
/// <summary>
/// 使用者服務介面
/// </summary>
public interface IUserService : IBaseService<User>
{
/// <summary>
/// 註冊
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
Task<bool> Register(RegisterViewModel model);
/// <summary>
/// 登入成功返回userId
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
Task<Guid> Login(LoginViewModel model);
/// <summary>
/// 修改使用者密碼
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> ChangePassword(ChangePwdViewModel model, Guid userId);
/// <summary>
/// 修改使用者頭像
/// </summary>
/// <param name="profilePhoto"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> ChangeUserPhoto(string profilePhoto, Guid userId);
/// <summary>
/// 修改使用者資訊
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> ChangeUserInfo(ChangeUserInfoViewModel model, Guid userId);
/// <summary>
/// 使用account獲取使用者資訊
/// </summary>
/// <param name="account"></param>
/// <returns></returns>
Task<UserDetailsViewModel> GetUserInfoByAccount(string account);
}
}
using BlogSystem.Common.Helpers;
using BlogSystem.IBLL;
using BlogSystem.IDAL;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace BlogSystem.BLL
{
public class UserService : BaseService<User>, IUserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
BaseRepository = userRepository;
}
/// <summary>
/// 使用者註冊
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public async Task<bool> Register(RegisterViewModel model)
{
//判斷賬戶是否存在
if (await _userRepository.GetAll().AnyAsync(m => m.Account == model.Account))
{
return false;
}
var pwd = Md5Helper.Md5Encrypt(model.Password);
await _userRepository.CreateAsync(new User
{
Account = model.Account,
Password = pwd
});
return true;
}
/// <summary>
/// 使用者登入
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public async Task<Guid> Login(LoginViewModel model)
{
var pwd = Md5Helper.Md5Encrypt(model.Password);
var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Account == model.Account && m.Password == pwd);
return user == null ? new Guid() : user.Id;
}
/// <summary>
/// 修改使用者密碼
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> ChangePassword(ChangePwdViewModel model, Guid userId)
{
var oldPwd = Md5Helper.Md5Encrypt(model.OldPassword);
var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Id == userId && m.Password == oldPwd);
if (user == null)
{
return false;
}
var newPwd = Md5Helper.Md5Encrypt(model.NewPassword);
user.Password = newPwd;
await _userRepository.EditAsync(user);
return true;
}
/// <summary>
/// 修改使用者照片
/// </summary>
/// <param name="profilePhoto"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> ChangeUserPhoto(string profilePhoto, Guid userId)
{
var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Id == userId);
if (user == null) return false;
user.ProfilePhoto = profilePhoto;
await _userRepository.EditAsync(user);
return true;
}
/// <summary>
/// 修改使用者資訊
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> ChangeUserInfo(ChangeUserInfoViewModel model, Guid userId)
{
//確保使用者名稱唯一
if (await _userRepository.GetAll().AnyAsync(m => m.Account == model.Account))
{
return false;
}
var user = await _userRepository.GetOneByIdAsync(userId);
user.Account = model.Account;
user.Gender = model.Gender;
user.BirthOfDate = model.BirthOfDate;
await _userRepository.EditAsync(user);
return true;
}
/// <summary>
/// 通過賬號名稱獲取使用者資訊
/// </summary>
/// <param name="account"></param>
/// <returns></returns>
public async Task<UserDetailsViewModel> GetUserInfoByAccount(string account)
{
if (await _userRepository.GetAll().AnyAsync(m => m.Account == account))
{
return await _userRepository.GetAll().Where(m => m.Account == account).Select(m =>
new UserDetailsViewModel()
{
Account = m.Account,
ProfilePhoto = m.ProfilePhoto,
Age = DateTime.Now.Year - m.BirthOfDate.Year,
Gender = m.Gender.ToString(),
Level = m.Level.ToString(),
FansNum = m.FansNum,
FocusNum = m.FocusNum
}).FirstAsync();
}
return new UserDetailsViewModel();
}
}
}
3、Controller層功能的實現大多數需要基於UserId,我們怎麼獲取UserId呢?還記得Jwt嗎?客戶端傳送請求時會在Header中帶上Jwt字串,我們可以解析該字串得到使用者名稱。在自定義的JwtHelper中我們實現了兩個方法,一個是加密Jwt,一個是解密Jwt,我們對解密方法進行調整,如下:
/// <summary>
/// Jwt解密
/// </summary>
/// <param name="jwtStr"></param>
/// <returns></returns>
public static TokenModelJwt JwtDecrypt(string jwtStr)
{
if (string.IsNullOrEmpty(jwtStr) || string.IsNullOrWhiteSpace(jwtStr))
{
return new TokenModelJwt();
}
jwtStr = jwtStr.Substring(7);//擷取前面的Bearer和空格
var jwtHandler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
jwtToken.Payload.TryGetValue(ClaimTypes.Role, out object level);
var model = new TokenModelJwt
{
UserId = Guid.Parse(jwtToken.Id),
Level = level == null ? "" : level.ToString()
};
return model;
}
在對應的Contoneller中我們可以使用HttpContext物件獲取Http請求的資訊,但是HttpContext的使用是需要註冊的,在StartUp的ConfigureServices中進行註冊,services.AddHttpContextAccessor();
之後在對應的控制器建構函式中進行注入IHttpContextAccessor物件即可,如下:
using BlogSystem.Core.Helpers;
using BlogSystem.IBLL;
using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace BlogSystem.Core.Controllers
{
[ApiController]
[Route("api/user")]
public class UserController : ControllerBase
{
private readonly IUserService _userService;
private readonly Guid _userId;
public UserController(IUserService userService, IHttpContextAccessor httpContext)
{
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
_userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId;
}
/// <summary>
/// 使用者註冊
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost(nameof(Register))]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!await _userService.Register(model))
{
return Ok("使用者已存在");
}
//建立成功返回到登入方法,並返回註冊成功的account
return CreatedAtRoute(nameof(Login), model.Account);
}
/// <summary>
/// 使用者登入
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[HttpPost("Login", Name = nameof(Login))]
public async Task<IActionResult> Login(LoginViewModel model)
{
//判斷賬號密碼是否正確
var userId = await _userService.Login(model);
if (userId == Guid.Empty) return Ok("賬號或密碼錯誤!");
//登入成功進行jwt加密
var user = await _userService.GetOneByIdAsync(userId);
TokenModelJwt tokenModel = new TokenModelJwt { UserId = user.Id, Level = user.Level.ToString() };
var jwtStr = JwtHelper.JwtEncrypt(tokenModel);
return Ok(jwtStr);
}
/// <summary>
/// 獲取使用者資訊
/// </summary>
/// <param name="account"></param>
/// <returns></returns>
[HttpGet("{account}")]
public async Task<IActionResult> UserInfo(string account)
{
var list = await _userService.GetUserInfoByAccount(account);
if (string.IsNullOrEmpty(list.Account))
{
return NotFound();
}
return Ok(list);
}
/// <summary>
/// 修改使用者密碼
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPatch("password")]
public async Task<IActionResult> ChangePassword(ChangePwdViewModel model)
{
if (!await _userService.ChangePassword(model, _userId))
{
return NotFound("使用者密碼錯誤!");
}
return NoContent();
}
/// <summary>
/// 修改使用者照片
/// </summary>
/// <param name="profilePhoto"></param>
/// <returns></returns>
[Authorize]
[HttpPatch("photo")]
public async Task<IActionResult> ChangeUserPhoto([FromBody]string profilePhoto)
{
if (!await _userService.ChangeUserPhoto(profilePhoto, _userId))
{
return NotFound();
}
return NoContent();
}
/// <summary>
/// 修改使用者資訊
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPatch("info")]
public async Task<IActionResult> ChangeUserInfo(ChangeUserInfoViewModel model)
{
if (!await _userService.ChangeUserInfo(model, _userId))
{
return Ok("使用者名稱已存在");
}
return NoContent();
}
}
}
2、分類Controller
1、調整ViewModel層如下:
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 編輯分類
/// </summary>
public class EditCategoryViewModel
{
/// <summary>
/// 分類Id
/// </summary>
public Guid CategoryId { get; set; }
/// <summary>
/// 分類名稱
/// </summary>
[Required, StringLength(30, MinimumLength = 2)]
public string CategoryName { get; set; }
}
}
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 分類列表
/// </summary>
public class CategoryListViewModel
{
/// <summary>
/// 分類Id
/// </summary>
public Guid CategoryId { get; set; }
/// <summary>
/// 分類名稱
/// </summary>
[Required, StringLength(30, MinimumLength = 2)]
public string CategoryName { get; set; }
}
}
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 建立文章分類
/// </summary>
public class CreateCategoryViewModel
{
/// <summary>
/// 分類Id
/// </summary>
public Guid CategoryId { get; set; }
/// <summary>
/// 分類名稱
/// </summary>
[Required, StringLength(30, MinimumLength = 2)]
public string CategoryName { get; set; }
}
}
2、調整IBLL和BLL層如下:
using BlogSystem.Model;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BlogSystem.Model.ViewModels;
namespace BlogSystem.IBLL
{
/// <summary>
/// 分類服務介面
/// </summary>
public interface ICategoryService : IBaseService<Category>
{
/// <summary>
/// 建立分類
/// </summary>
/// <param name="categoryName"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<Guid> CreateCategory(string categoryName, Guid userId);
/// <summary>
/// 編輯分類
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> EditCategory(EditCategoryViewModel model, Guid userId);
/// <summary>
/// 通過使用者Id獲取所有分類
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<CategoryListViewModel>> GetCategoryByUserIdAsync(Guid userId);
}
}
using BlogSystem.IBLL;
using BlogSystem.IDAL;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlogSystem.BLL
{
public class CategoryService : BaseService<Category>, ICategoryService
{
private readonly ICategoryRepository _categoryRepository;
public CategoryService(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
BaseRepository = categoryRepository;
}
/// <summary>
/// 建立分類
/// </summary>
/// <param name="categoryName"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<Guid> CreateCategory(string categoryName, Guid userId)
{
//當前使用者存在該分類名稱則返回
if (string.IsNullOrEmpty(categoryName) || await _categoryRepository.GetAll()
.AnyAsync(m => m.UserId == userId && m.CategoryName == categoryName))
{
return Guid.Empty;
}
//建立成功返回分類Id
var categoryId = Guid.NewGuid();
await _categoryRepository.CreateAsync(new Category
{
Id = categoryId,
UserId = userId,
CategoryName = categoryName
});
return categoryId;
}
/// <summary>
/// 編輯分類
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> EditCategory(EditCategoryViewModel model, Guid userId)
{
//使用者不存在該分類則返回
if (!await _categoryRepository.GetAll().AnyAsync(m => m.UserId == userId && m.Id == model.CategoryId))
{
return false;
}
await _categoryRepository.EditAsync(new Category
{
UserId = userId,
Id = model.CategoryId,
CategoryName = model.CategoryName
});
return true;
}
/// <summary>
/// 通過使用者Id獲取所有分類
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public Task<List<CategoryListViewModel>> GetCategoryByUserIdAsync(Guid userId)
{
return _categoryRepository.GetAll().Where(m => m.UserId == userId).Select(m => new CategoryListViewModel
{
CategoryId = m.Id,
CategoryName = m.CategoryName
}).ToListAsync();
}
}
}
3、調整Controller功能如下:
using BlogSystem.Core.Helpers;
using BlogSystem.IBLL;
using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace BlogSystem.Core.Controllers
{
[ApiController]
[Route("api/category")]
public class CategoryController : ControllerBase
{
private readonly ICategoryService _categoryService;
private readonly IArticleService _aeArticleService;
private readonly Guid _userId;
public CategoryController(ICategoryService categoryService, IArticleService articleService,
IHttpContextAccessor httpContext)
{
_categoryService = categoryService ?? throw new ArgumentNullException(nameof(categoryService));
_aeArticleService = articleService ?? throw new ArgumentNullException(nameof(articleService));
var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
_userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId;
}
/// <summary>
/// 查詢使用者的文章分類
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
[HttpGet("{userId}", Name = nameof(GetCategoryByUserId))]
public async Task<IActionResult> GetCategoryByUserId(Guid userId)
{
if (userId == Guid.Empty)
{
return NotFound();
}
var list = await _categoryService.GetCategoryByUserIdAsync(userId);
return Ok(list);
}
/// <summary>
/// 新增文章分類
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateCategory([FromBody]string categoryName)
{
var categoryId = await _categoryService.CreateCategory(categoryName, _userId);
if (categoryId == Guid.Empty)
{
return BadRequest("重複分類!");
}
//建立成功返回查詢頁面連結
var category = new CreateCategoryViewModel { CategoryId = categoryId, CategoryName = categoryName };
return CreatedAtRoute(nameof(GetCategoryByUserId), new { userId = _userId }, category);
}
/// <summary>
/// 刪除分類
/// </summary>
/// <param name="categoryId"></param>
/// <returns></returns>
[Authorize]
[HttpDelete("{categoryId}")]
public async Task<IActionResult> RemoveCategory(Guid categoryId)
{
//確認是否存在,操作人與歸屬人是否一致
var category = await _categoryService.GetOneByIdAsync(categoryId);
if (category == null || category.UserId != _userId)
{
return NotFound();
}
//有文章使用了該分類,無法刪除
var data = await _aeArticleService.GetArticlesByCategoryIdAsync(_userId, categoryId);
if (data.Count > 0)
{
return BadRequest("存在使用該分類的文章!");
}
await _categoryService.RemoveAsync(categoryId);
return NoContent();
}
/// <summary>
/// 編輯分類
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPatch]
public async Task<IActionResult> EditCategory(EditCategoryViewModel model)
{
if (!await _categoryService.EditCategory(model, _userId))
{
return NotFound();
}
return NoContent();
}
}
}
3、文章Controller
1、這裡我在操作時遇到了文章內容亂碼的問題,可能是因為資料庫的text格式和輸入格式有衝突,所以這裡我暫時將其改成了nvarchar(max)的型別
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlogSystem.Model
{
/// <summary>
/// 文章
/// </summary>
public class Article : BaseEntity
{
/// <summary>
/// 文章標題
/// </summary>
[Required]
public string Title { get; set; }
/// <summary>
/// 文章內容
/// </summary>
[Required]
public string Content { get; set; }
/// <summary>
/// 發表人的Id,使用者表的外來鍵
/// </summary>
[ForeignKey(nameof(User))]
public Guid UserId { get; set; }
public User User { get; set; }
/// <summary>
/// 看好人數
/// </summary>
public int GoodCount { get; set; }
/// <summary>
/// 不看好人數
/// </summary>
public int BadCount { get; set; }
/// <summary>
/// 文章檢視所需等級
/// </summary>
public Level Level { get; set; } = Level.普通使用者;
}
}
ViewModel調整如下:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 建立文章
/// </summary>
public class CreateArticleViewModel
{
/// <summary>
/// 文章標題
/// </summary>
[Required]
public string Title { get; set; }
/// <summary>
/// 文章內容
/// </summary>
[Required]
public string Content { get; set; }
/// <summary>
/// 文章分類
/// </summary>
[Required]
public List<Guid> CategoryIds { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 編輯文章
/// </summary>
public class EditArticleViewModel
{
/// <summary>
/// 文章Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 文章標題
/// </summary>
[Required]
public string Title { get; set; }
/// <summary>
/// 文章內容
/// </summary>
[Required]
public string Content { get; set; }
/// <summary>
/// 文章分類
/// </summary>
public List<Guid> CategoryIds { get; set; }
}
}
using System;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 文章列表
/// </summary>
public class ArticleListViewModel
{
/// <summary>
/// 文章Id
/// </summary>
public Guid ArticleId { get; set; }
/// <summary>
/// 文章標題
/// </summary>
public string Title { get; set; }
/// <summary>
/// 文章內容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 建立時間
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 賬號
/// </summary>
public string Account { get; set; }
/// <summary>
/// 頭像
/// </summary>
public string ProfilePhoto { get; set; }
}
}
using System;
using System.Collections.Generic;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 文章詳情
/// </summary>
public class ArticleDetailsViewModel
{
/// <summary>
/// 文章Id
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// 文章標題
/// </summary>
public string Title { get; set; }
/// <summary>
/// 文章內容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 建立時間
/// </summary>
public DateTime CreateTime { get; set; }
/// <summary>
/// 作者
/// </summary>
public string Account { get; set; }
/// <summary>
/// 頭像
/// </summary>
public string ProfilePhoto { get; set; }
/// <summary>
/// 分類Id
/// </summary>
public List<Guid> CategoryIds { get; set; }
/// <summary>
/// 分類名稱
/// </summary>
public List<string> CategoryNames { get; set; }
/// <summary>
/// 看好人數
/// </summary>
public int GoodCount { get; set; }
/// <summary>
/// 不看好人數
/// </summary>
public int BadCount { get; set; }
}
}
2、調整IBLL和BLL內容,如下
using BlogSystem.Model;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BlogSystem.Model.ViewModels;
namespace BlogSystem.IBLL
{
/// <summary>
/// 評論服務介面
/// </summary>
public interface ICommentService : IBaseService<ArticleComment>
{
/// <summary>
/// 新增評論
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId);
/// <summary>
/// 新增普通評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId);
/// <summary>
/// 新增回複評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId);
/// <summary>
/// 通過文章Id獲取所有評論
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId);
/// <summary>
/// 確認回覆型評論是否存在
/// </summary>
/// <param name="commentId"></param>
/// <returns></returns>
Task<bool> ReplyExistAsync(Guid commentId);
}
}
using BlogSystem.IBLL;
using BlogSystem.IDAL;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlogSystem.BLL
{
public class CommentService : BaseService<ArticleComment>, ICommentService
{
private readonly IArticleCommentRepository _commentRepository;
private readonly ICommentReplyRepository _commentReplyRepository;
public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository)
{
_commentRepository = commentRepository;
BaseRepository = commentRepository;
_commentReplyRepository = commentReplyRepository;
}
/// <summary>
/// 新增評論
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId)
{
await _commentRepository.CreateAsync(new ArticleComment()
{
ArticleId = articleId,
Content = model.Content,
UserId = userId
});
}
/// <summary>
/// 新增普通評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId)
{
var comment = await _commentRepository.GetOneByIdAsync(commentId);
var toUserId = comment.UserId;
await _commentReplyRepository.CreateAsync(new CommentReply()
{
CommentId = commentId,
ToUserId = toUserId,
ArticleId = articleId,
UserId = userId,
Content = model.Content
});
}
/// <summary>
/// 新增回復型評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId)
{
var comment = await _commentReplyRepository.GetOneByIdAsync(commentId);
var toUserId = comment.UserId;
await _commentReplyRepository.CreateAsync(new CommentReply()
{
CommentId = commentId,
ToUserId = toUserId,
ArticleId = articleId,
UserId = userId,
Content = model.Content
});
}
/// <summary>
/// 根據文章Id獲取評論資訊
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
public async Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId)
{
//正常評論
var comment = await _commentRepository.GetAll().Where(m => m.ArticleId == articleId)
.Include(m => m.User).Select(m => new CommentListViewModel
{
ArticleId = m.ArticleId,
UserId = m.UserId,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto,
CommentId = m.Id,
CommentContent = m.Content,
CreateTime = m.CreateTime
}).ToListAsync();
//回覆型的評論
var replyComment = await _commentReplyRepository.GetAll().Where(m => m.ArticleId == articleId)
.Include(m => m.User).Select(m => new CommentListViewModel
{
ArticleId = m.ArticleId,
UserId = m.UserId,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto,
CommentId = m.Id,
CommentContent = $"@{m.ToUser.Account}" + Environment.NewLine + m.Content,
CreateTime = m.CreateTime
}).ToListAsync();
var list = comment.Union(replyComment).OrderByDescending(m => m.CreateTime).ToList();
return list;
}
/// <summary>
/// 確認回覆型評論是否存在
/// </summary>
/// <param name="commentId"></param>
/// <returns></returns>
public async Task<bool> ReplyExistAsync(Guid commentId)
{
return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId);
}
}
}
3、調整Controller如下:
using BlogSystem.Core.Helpers;
using BlogSystem.IBLL;
using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace BlogSystem.Core.Controllers
{
[ApiController]
[Route("api/Article/{articleId}/Comment")]
public class CommentController : ControllerBase
{
private readonly ICommentService _commentService;
private readonly IArticleService _articleService;
private readonly Guid _userId;
public CommentController(ICommentService commentService, IArticleService articleService, IHttpContextAccessor httpContext)
{
_commentService = commentService ?? throw new ArgumentNullException(nameof(commentService));
_articleService = articleService ?? throw new ArgumentNullException(nameof(articleService));
var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
_userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId;
}
/// <summary>
/// 新增評論
/// </summary>
/// <param name="articleId"></param>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateComment(Guid articleId, CreateCommentViewModel model)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
await _commentService.CreateComment(model, articleId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
/// <summary>
/// 新增回復型評論
/// </summary>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPost("reply")]
public async Task<IActionResult> CreateReplyComment(Guid articleId, Guid commentId, CreateApplyCommentViewModel model)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
//回覆的是正常評論
if (await _commentService.ExistsAsync(commentId))
{
await _commentService.CreateReplyComment(model, articleId, commentId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
//需要考慮回覆的是正常評論還是回覆型評論
if (await _commentService.ReplyExistAsync(commentId))
{
await _commentService.CreateToReplyComment(model, articleId, commentId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
return NotFound();
}
/// <summary>
/// 獲取評論
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
[HttpGet(Name = nameof(GetComments))]
public async Task<IActionResult> GetComments(Guid articleId)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
var list = await _commentService.GetCommentsByArticleIdAsync(articleId);
return Ok(list);
}
}
}
4、評論Controller
1、這裡發現評論回覆表CommentReply設計存在問題,因為回覆也有可能是針對回覆型評論的,所以調整之後需要使用EF的遷移命令更行資料庫,如下:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlogSystem.Model
{
/// <summary>
/// 評論回覆表
/// </summary>
public class CommentReply : BaseEntity
{
/// <summary>
/// 回覆指向的評論Id
/// </summary>
public Guid CommentId { get; set; }
/// <summary>
/// 回覆指向的使用者Id
/// </summary>
[ForeignKey(nameof(ToUser))]
public Guid ToUserId { get; set; }
public User ToUser { get; set; }
/// <summary>
/// 文章ID
/// </summary>
[ForeignKey(nameof(Article))]
public Guid ArticleId { get; set; }
public Article Article { get; set; }
/// <summary>
/// 使用者Id
/// </summary>
[ForeignKey(nameof(User))]
public Guid UserId { get; set; }
public User User { get; set; }
/// <summary>
/// 回覆的內容
/// </summary>
[Required, StringLength(800)]
public string Content { get; set; }
}
}
調整ViewModel如下,有人發現評論和回覆的ViewModel相同,為什麼不使用一個?是為了應對後續兩張表欄位不同時,需要調整的情況
using System;
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 文章評論
/// </summary>
public class CreateCommentViewModel
{
/// <summary>
/// 評論內容
/// </summary>
[Required, StringLength(800)]
public string Content { get; set; }
}
}
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 新增回復型評論
/// </summary>
public class CreateApplyCommentViewModel
{
/// <summary>
/// 回覆的內容
/// </summary>
[Required, StringLength(800)]
public string Content { get; set; }
}
}
using System;
namespace BlogSystem.Model.ViewModels
{
/// <summary>
/// 文章評論列表
/// </summary>
public class CommentListViewModel
{
/// <summary>
/// 文章Id
/// </summary>
public Guid ArticleId { get; set; }
/// <summary>
/// 使用者Id
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 賬號
/// </summary>
public string Account { get; set; }
/// <summary>
/// 頭像
/// </summary>
public string ProfilePhoto { get; set; }
/// <summary>
/// 評論Id
/// </summary>
public Guid CommentId { get; set; }
/// <summary>
/// 評論內容
/// </summary>
public string CommentContent { get; set; }
/// <summary>
/// 建立時間
/// </summary>
public DateTime CreateTime { get; set; }
}
}
2、調整IBLL和BLL如下:
using BlogSystem.Model;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BlogSystem.Model.ViewModels;
namespace BlogSystem.IBLL
{
/// <summary>
/// 評論服務介面
/// </summary>
public interface ICommentService : IBaseService<ArticleComment>
{
/// <summary>
/// 新增評論
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId);
/// <summary>
/// 新增普通評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId);
/// <summary>
/// 新增回複評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId);
/// <summary>
/// 通過文章Id獲取所有評論
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId);
/// <summary>
/// 確認回覆型評論是否存在
/// </summary>
/// <param name="commentId"></param>
/// <returns></returns>
Task<bool> ReplyExistAsync(Guid commentId);
}
}
using BlogSystem.IBLL;
using BlogSystem.IDAL;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BlogSystem.BLL
{
public class CommentService : BaseService<ArticleComment>, ICommentService
{
private readonly IArticleCommentRepository _commentRepository;
private readonly ICommentReplyRepository _commentReplyRepository;
public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository)
{
_commentRepository = commentRepository;
BaseRepository = commentRepository;
_commentReplyRepository = commentReplyRepository;
}
/// <summary>
/// 新增評論
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId)
{
await _commentRepository.CreateAsync(new ArticleComment()
{
ArticleId = articleId,
Content = model.Content,
UserId = userId
});
}
/// <summary>
/// 新增普通評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId)
{
var comment = await _commentRepository.GetOneByIdAsync(commentId);
var toUserId = comment.UserId;
await _commentReplyRepository.CreateAsync(new CommentReply()
{
CommentId = commentId,
ToUserId = toUserId,
ArticleId = articleId,
UserId = userId,
Content = model.Content
});
}
/// <summary>
/// 新增回復型評論的回覆
/// </summary>
/// <param name="model"></param>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId)
{
var comment = await _commentReplyRepository.GetOneByIdAsync(commentId);
var toUserId = comment.UserId;
await _commentReplyRepository.CreateAsync(new CommentReply()
{
CommentId = commentId,
ToUserId = toUserId,
ArticleId = articleId,
UserId = userId,
Content = model.Content
});
}
/// <summary>
/// 根據文章Id獲取評論資訊
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
public async Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId)
{
//正常評論
var comment = await _commentRepository.GetAll().Where(m => m.ArticleId == articleId)
.Include(m => m.User).Select(m => new CommentListViewModel
{
ArticleId = m.ArticleId,
UserId = m.UserId,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto,
CommentId = m.Id,
CommentContent = m.Content,
CreateTime = m.CreateTime
}).ToListAsync();
//回覆型的評論
var replyComment = await _commentReplyRepository.GetAll().Where(m => m.ArticleId == articleId)
.Include(m => m.User).Select(m => new CommentListViewModel
{
ArticleId = m.ArticleId,
UserId = m.UserId,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto,
CommentId = m.Id,
CommentContent = $"@{m.ToUser.Account}" + Environment.NewLine + m.Content,
CreateTime = m.CreateTime
}).ToListAsync();
var list = comment.Union(replyComment).OrderByDescending(m => m.CreateTime).ToList();
return list;
}
/// <summary>
/// 確認回覆型評論是否存在
/// </summary>
/// <param name="commentId"></param>
/// <returns></returns>
public async Task<bool> ReplyExistAsync(Guid commentId)
{
return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId);
}
}
}
3、調整Controller如下:
using BlogSystem.Core.Helpers;
using BlogSystem.IBLL;
using BlogSystem.Model.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace BlogSystem.Core.Controllers
{
[ApiController]
[Route("api/Article/{articleId}/Comment")]
public class CommentController : ControllerBase
{
private readonly ICommentService _commentService;
private readonly IArticleService _articleService;
private readonly Guid _userId;
public CommentController(ICommentService commentService, IArticleService articleService, IHttpContextAccessor httpContext)
{
_commentService = commentService ?? throw new ArgumentNullException(nameof(commentService));
_articleService = articleService ?? throw new ArgumentNullException(nameof(articleService));
var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
_userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId;
}
/// <summary>
/// 新增評論
/// </summary>
/// <param name="articleId"></param>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateComment(Guid articleId, CreateCommentViewModel model)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
await _commentService.CreateComment(model, articleId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
/// <summary>
/// 新增回復型評論
/// </summary>
/// <param name="articleId"></param>
/// <param name="commentId"></param>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPost("reply")]
public async Task<IActionResult> CreateReplyComment(Guid articleId, Guid commentId, CreateApplyCommentViewModel model)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
//回覆的是正常評論
if (await _commentService.ExistsAsync(commentId))
{
await _commentService.CreateReplyComment(model, articleId, commentId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
//需要考慮回覆的是正常評論還是回覆型評論
if (await _commentService.ReplyExistAsync(commentId))
{
await _commentService.CreateToReplyComment(model, articleId, commentId, _userId);
return CreatedAtRoute(nameof(GetComments), new { articleId }, model);
}
return NotFound();
}
/// <summary>
/// 獲取評論
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
[HttpGet(Name = nameof(GetComments))]
public async Task<IActionResult> GetComments(Guid articleId)
{
if (!await _articleService.ExistsAsync(articleId))
{
return NotFound();
}
var list = await _commentService.GetCommentsByArticleIdAsync(articleId);
return Ok(list);
}
}
}
該專案原始碼已上傳至GitHub,有需要的朋友可以下載使用:https://github.com/Jscroop/BlogSystem
本章完~
本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。
本文部分內容參考了網路上的視訊內容和文章,僅為學習和交流,視訊地址如下:
solenovex,ASP.NET Core 3.x 入門視訊
solenovex,使用 ASP.NET Core 3.x 構建 RESTful Web API
老張的哲學,系列教程一目錄:.netcore+vue 前後端分離