• 学习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方法

    1. GET:通常用来获取资源;GET请求会返回请求路径对应的资源,但它分两种情况:

      ①获取单个资源,通过使用URL的形式带上唯一标识,示例:api/Articles/{ArticleId};

      ②获取集合资源中符合条件的资源,会通过QueryString的形式在URL后面添加?查询条件作为筛选条件,示例:api/Articles?title=WebAPI

    2. POST:通常用来创建资源;POST的参数会放在请求body中,POST请求应该返回新创建的资源以及可以获取该资源的唯一标识URL,示例:api/Articles/{新增的ArticleId}

    3. DELETE:通常用来移除/删除对应路径的资源;通过使用URL的形式带上唯一标识,或者和GET一样使用QueryString,处理完成后通常不会返回资源,只返回状态码204,示例:api/Articles/{ArticleId};

    4. PUT:通常用来完全替换对应路径的资源信息;POST的参数会放在请求body中,且为一个完整对象,示例:api/Articles/{ArticleId};与此同时,它分两类情况:

      ①对应的资源不存在,则新增对应的资源,后续处理和POST一样;

      ②对应的资源存在,则替换对应的资源,处理完成不需要返回信息,只返回状态码204

    5. 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、内容协商

    1. 什么是内容协商?即当有多种表述格式(Json/Xml等)可用时,选取最佳的一个进行表述。简单来说就是请求什么格式,服务端就返回什么格式的数据;
    2. 如何设置内容协商?首先我们要从服务端的角度区分输出和输入,输出表示客户端发出请求服务端响应数据;输入表示客户端提交数据服务端对其进行处理;举例来说,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 前后端分离

    声明
  • 相关阅读:
    mysql分组查询
    (三)分布式数据库tidb-隔离级别详解
    (二)分布式数据库tidb-事务
    (一)ArrayList集合源码解析
    (一)分布式数据库tidb-简介
    (二)LinkedList集合解析及手写集合
    电商数仓中需要统计的指标
    实时推荐模型的算法设计
    数据库需要掌握到什么程度可以应付工作?
    Mysql的万能优化方法
  • 原文地址:https://www.cnblogs.com/Jscroop/p/12919641.html
Copyright © 2020-2023  润新知