学习ASP.NET Core(06)-Restful与WebAPI
上一篇我们使用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/
-
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 System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
namespace BlogSystem.IBLL
{
/// <summary>
/// 文章接口
/// </summary>
public interface IArticleService : IBaseService<Article>
{
/// <summary>
/// 新增文章
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<Guid> CreateArticleAsync(CreateArticleViewModel model, Guid userId);
/// <summary>
/// 编辑文章
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> EditArticleAsync(EditArticleViewModel model, Guid userId);
/// <summary>
/// 通过Id获取文章详情
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
Task<ArticleDetailsViewModel> GetArticleDetailsByArticleIdAsync(Guid articleId);
/// <summary>
/// 通过用户Id获取文章列表
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<List<ArticleListViewModel>> GetArticlesByUserIdAsync(Guid userId);
/// <summary>
/// 通过用户分类Id获取所有文章
/// </summary>
/// <param name="userId"></param>
/// <param name="categoryId"></param>
/// <returns></returns>
Task<List<ArticleListViewModel>> GetArticlesByCategoryIdAsync(Guid userId, Guid categoryId);
/// <summary>
/// 通过用户Id获取文章数量
/// </summary>
/// <param name="userid"></param>
/// <returns></returns>
Task<int> GetArticleCountByUserIdAsync(Guid userid);
/// <summary>
/// 点赞文章
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task AddGoodCount(Guid articleId, Guid userId);
/// <summary>
/// 点灭文章
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task AddBadCount(Guid articleId, Guid userId);
/// <summary>
/// 删除文章所属分类信息
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
Task<bool> RemoveArticleInCategory(Guid articleId, Guid userId);
/// <summary>
/// 新增文章所属分类信息
/// </summary>
/// <param name="articleId"></param>
/// <param name="categoryIds"></param>
/// <returns></returns>
Task CreateArticleInCategory(Guid articleId, List<Guid> categoryIds);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BlogSystem.IBLL;
using BlogSystem.IDAL;
using BlogSystem.Model;
using BlogSystem.Model.ViewModels;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.BLL
{
/// <summary>
/// 实现文章接口方法
/// </summary>
public class ArticleService : BaseService<Article>, IArticleService
{
private readonly IArticleRepository _articleRepository;
private readonly IArticleInCategoryRepository _articleInCategoryRepository;
private readonly ICategoryRepository _categoryRepository;
private readonly IUserRepository _userRepository;
//构造函数注入相关接口
public ArticleService(IArticleRepository articleRepository, IArticleInCategoryRepository articleInCategoryRepository,
ICategoryRepository categoryRepository, IUserRepository userRepository)
{
_articleRepository = articleRepository;
BaseRepository = articleRepository;
_articleInCategoryRepository = articleInCategoryRepository;
_categoryRepository = categoryRepository;
_userRepository = userRepository;
}
/// <summary>
/// 创建文章
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<Guid> CreateArticleAsync(CreateArticleViewModel model, Guid userId)
{
//新增文章
var articleId = Guid.NewGuid();
var article = new Article
{
Id = articleId,
UserId = userId,
Title = model.Title,
Content = model.Content
};
await _articleRepository.CreateAsync(article);
//新增文章所属分类
await CreateArticleInCategory(article.Id, model.CategoryIds);
return articleId;
}
/// <summary>
/// 编辑文章
/// </summary>
/// <param name="model"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> EditArticleAsync(EditArticleViewModel model, Guid userId)
{
//删除文章所属分类
if (!await RemoveArticleInCategory(model.Id, userId))
{
return false;
}
//新增文章所属分类
await CreateArticleInCategory(model.Id, model.CategoryIds);
//保存文章更新
var article = await _articleRepository.GetOneByIdAsync(model.Id);
article.Title = model.Title;
article.Content = model.Content;
await _articleRepository.EditAsync(article);
return true;
}
/// <summary>
/// 通过文章Id获取文章详情
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
public async Task<ArticleDetailsViewModel> GetArticleDetailsByArticleIdAsync(Guid articleId)
{
if (!await _articleRepository.GetAll().AnyAsync(m => m.Id == articleId))
{
return new ArticleDetailsViewModel();
}
var data = await _articleRepository.GetAll().Include(m => m.User).Where(m => m.Id == articleId)
.Select(m => new ArticleDetailsViewModel
{
Id = m.Id,
Title = m.Title,
Content = m.Content,
CreateTime = m.CreateTime,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto,
GoodCount = m.GoodCount,
BadCount = m.BadCount
}).FirstAsync();
//处理分类
var categories = await _articleInCategoryRepository.GetAll().Include(m => m.Category)
.Where(m => m.ArticleId == data.Id).ToListAsync();
data.CategoryIds = categories.Select(m => m.CategoryId).ToList();
data.CategoryNames = categories.Select(m => m.Category.CategoryName).ToList();
return data;
}
/// <summary>
/// 根据用户Id获取文章列表信息
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<List<ArticleListViewModel>> GetArticlesByUserIdAsync(Guid userId)
{
if (!await _articleRepository.GetAll().AnyAsync(m => m.UserId == userId))
{
return new List<ArticleListViewModel>();
}
return await _articleRepository.GetAllByOrder(false).Include(m => m.User)
.Where(m => m.UserId == userId).Select(m => new ArticleListViewModel
{
ArticleId = m.Id,
Title = m.Title,
Content = m.Content,
CreateTime = m.CreateTime,
Account = m.User.Account,
ProfilePhoto = m.User.ProfilePhoto
}).ToListAsync();
}
/// <summary>
/// 通过用户分类Id获取文章列表
/// </summary>
/// <param name="userId"></param>
/// <param name="categoryId"></param>
/// <returns></returns>
public async Task<List<ArticleListViewModel>> GetArticlesByCategoryIdAsync(Guid userId, Guid categoryId)
{
//判断有无用户及分类信息
if (!await _categoryRepository.GetAll().AnyAsync(m => m.UserId == userId && m.Id == categoryId))
{
return new List<ArticleListViewModel>();
}
var user = await _userRepository.GetOneByIdAsync(userId);
return await _articleInCategoryRepository.GetAll().Include(m => m.Article)
.Where(m => m.CategoryId == categoryId).Select(m => new ArticleListViewModel
{
ArticleId = m.Id,
Title = m.Article.Title,
Content = m.Article.Content,
CreateTime = m.CreateTime,
Account = user.Account,
ProfilePhoto = user.ProfilePhoto
}).ToListAsync();
}
/// <summary>
/// 获取用户文章数量
/// </summary>
/// <param name="userid"></param>
/// <returns></returns>
public async Task<int> GetArticleCountByUserIdAsync(Guid userid)
{
return await _articleRepository.GetAll().CountAsync(m => m.UserId == userid);
}
/// <summary>
/// 看好数量+1
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task AddGoodCount(Guid articleId, Guid userId)
{
var article = await _articleRepository.GetOneByIdAsync(articleId);
article.GoodCount++;
await _articleRepository.EditAsync(article);
}
/// <summary>
/// 不看好数量+1
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task AddBadCount(Guid articleId, Guid userId)
{
var article = await _articleRepository.GetOneByIdAsync(articleId);
article.BadCount++;
await _articleRepository.EditAsync(article);
}
/// <summary>
/// 删除文章所属分类信息
/// </summary>
/// <param name="articleId"></param>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<bool> RemoveArticleInCategory(Guid articleId, Guid userId)
{
//判断是否为该用户文章
if (!await _articleRepository.GetAll().AnyAsync(m => m.UserId == userId && m.Id == articleId))
{
return false;
}
//删除文章所属分类信息
var categoryIds = _articleInCategoryRepository.GetAll().Where(m => m.ArticleId == articleId);
foreach (var categoryId in categoryIds)
{
await _articleInCategoryRepository.RemoveAsync(categoryId, false);
}
await _articleInCategoryRepository.SavedAsync();
return true;
}
/// <summary>
/// 新增文章所属分类信息
/// </summary>
/// <param name="articleId"></param>
/// <param name="categoryIds"></param>
/// <returns></returns>
public async Task CreateArticleInCategory(Guid articleId, List<Guid> categoryIds)
{
foreach (var categoryId in categoryIds)
{
await _articleInCategoryRepository.CreateAsync(new ArticleInCategory()
{
ArticleId = articleId,
CategoryId = categoryId
}, false);
}
await _articleInCategoryRepository.SavedAsync();
}
}
}
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")]
public class ArticleController : ControllerBase
{
private readonly IArticleService _articleService;
private readonly Guid _userId;
public ArticleController(IArticleService articleService, IHttpContextAccessor httpContext)
{
_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="model"></param>
/// <returns></returns>
[Authorize]
[HttpPost]
public async Task<IActionResult> CreateArticle(CreateArticleViewModel model)
{
var articleId = await _articleService.CreateArticleAsync(model, _userId);
return CreatedAtRoute(nameof(GetArticleByArticleId), new { articleId }, model);
}
/// <summary>
/// 编辑文章
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[Authorize]
[HttpPatch]
public async Task<IActionResult> EditArticle(EditArticleViewModel model)
{
if (!await _articleService.EditArticleAsync(model, _userId))
{
return NotFound();
}
return NoContent();
}
/// <summary>
/// 删除文章
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
[Authorize]
[HttpDelete("{articleId}")]
public async Task<IActionResult> RemoveArticle(Guid articleId)
{
//删除文章所属分类
if (!await _articleService.RemoveArticleInCategory(articleId, _userId))
{
return NotFound();
}
//删除文章
await _articleService.RemoveAsync(articleId);
return NoContent();
}
/// <summary>
/// 通过文章Id获取文章详情
/// </summary>
/// <param name="articleId"></param>
/// <returns></returns>
[HttpGet("{articleId}", Name = nameof(GetArticleByArticleId))]
public async Task<IActionResult> GetArticleByArticleId(Guid articleId)
{
var article = await _articleService.GetArticleDetailsByArticleIdAsync(articleId);
if (article.Id == Guid.Empty)
{
return NotFound();
}
return Ok(article);
}
/// <summary>
/// 通过用户Id获取文章列表
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
[HttpGet("list/{userId}")]
public async Task<IActionResult> GetArticlesByUserId(Guid userId)
{
var list = await _articleService.GetArticlesByUserIdAsync(userId);
return Ok(list);
}
/// <summary>
/// 通过用户分类Id获取文章列表
/// </summary>
/// <param name="userId"></param>
/// <param name="categoryId"></param>
/// <returns></returns>
[HttpGet("list/{userId}/{categoryId}")]
public async Task<IActionResult> GetArticlesByCategoryId(Guid userId, Guid categoryId)
{
var list = await _articleService.GetArticlesByCategoryIdAsync(userId, categoryId);
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 前后端分离