寸金游( 后端开发 )(二)


C# - 基于.NetCore开发的“寸金游”(后端开发)(第二部分)

十一、 【单点登录】JWT与用户身份验证

JWT原理剖析

JWT是干什么用的?

  • JSON Web Token
  • JWT的作用是用户授权(Athorization),而不是用户的身份认证(Authentication)。

授权 ?认证

传统的Session登录

  • 用户登录后,服务器会保存登陆的session信息。
  • Session ID会通过cookie传递给前端。
  • http请求会附带cookie。
  • 有状态登录。

JWT彻底改变了用户授权与认证的过程

  • 替换cookie
  • JWT信息只需要保存在客户端
  • 无状态登录

有状态登录 ?无状态登录

有状态登录(session)

无状态登录(JWT)

有状态登录 ?无状态登录

  • Session需要保存在服务器上,而Session则保存在cookie中。
  • JWT信息只需要保存在客户端。
  • 无状态登陆优势∶分布式部署。

JWT与单点登录实例解释

JWT的原理

  • JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
  • { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" }
  • 以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。
  • 为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
  • 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

区别

  • session 存储在服务端占用服务器资源,而 JWT 存储在客户端。
  • session 存储在 Cookie 中,存在伪造跨站请求伪造攻击的风险。
  • session 只存在一台服务器上,那么下次请求就必须请求这台服务器,不利于分布式应用。
  • 存储在客户端的 JWT 比存储在服务端的 session 更具有扩展性。

常用的单点登录(SSO)系统

JWT的优点

  • 无状态,简单、方便,完美支持分布式部署。
  • 非对称加密,Token安全性高。

JWT的缺点

  • 无状态,token一经发布则无法取消。(无解)
  • 明文传递,Token安全性低。(使用Https即可解决)

启用无状态登陆系统(模拟用户登陆)

流程

  • 使用用户名和密码登陆,获得JWT。
  • 用户凭借JWT访问数据资源。
  • 模拟用户登陆。

安装JWT框架Microsoft.AspNetCore.Authentication.JwtBearer

新建身份认证控制器AuthenticateController.cs

using _02NET___CJ_ASP_Travel.Dtos;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace _02NET___CJ_ASP_Travel.Controllers
{
    [ApiController]
    [Route("auth")]
    public class AuthenticateController : ControllerBase
    {
        private readonly IConfiguration _configuration;

        public AuthenticateController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [AllowAnonymous]
        [HttpPost("login")]
        public IActionResult login([FromBody] LoginDto loginDto)
        {
            // 1 验证用户名密码

            // 2 创建jwt
            // header
            var signingAlgorithm = SecurityAlgorithms.HmacSha256;
            // payload
            var claims = new[]
            {
                // sub
                new Claim(JwtRegisteredClaimNames.Sub, "fake_user_id")
            };
            // signiture
            var secretByte = Encoding.UTF8.GetBytes(_configuration["Authentication:SecretKey"]);
            var signingKey = new SymmetricSecurityKey(secretByte);
            var signingCredentials = new SigningCredentials(signingKey, signingAlgorithm);

            var token = new JwtSecurityToken(
                issuer: _configuration["Authentication:Issuer"],
                audience: _configuration["Authentication:Audience"],
                claims,
                notBefore: DateTime.UtcNow,
                expires: DateTime.UtcNow.AddDays(1),
                signingCredentials
            );

            var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);

            // 3 return 200 ok + jwt
            return Ok(tokenStr);
        }
    }
}

创建LoginDto.cs处理json数据反序列化

namespace _02NET___CJ_ASP_Travel.Dtos
{
    public class LoginDto
    {
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
}

私钥可以保存在配置文件中appsettings.json

"Authentication": {
        "SecretKey": "Key",
        "Issuer": "baidu.com",
        "Audience": "baidu.com"
    }

启动API授权

注册身份认证服务(Startup.cs)ConfigureServices

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    var secretByte = Encoding.UTF8.GetBytes(Configuration["Authentication:SecretKey"]);
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidIssuer = Configuration["Authentication:Issuer"],

                        ValidateAudience = true,
                        ValidAudience = Configuration["Authentication:Audience"],

                        ValidateLifetime = true,

                        IssuerSigningKey = new SymmetricSecurityKey(secretByte)
                    };
                });

服务启动配置Configure

            // 你在哪?
            app.UseRouting();
            // 你是谁?
            app.UseAuthentication();
            // 你可以干什么?有什么权限?
            app.UseAuthorization();

TouristRoutesController.cs - CreateTouristRoute方法

  • 加上[Authorize]
        [HttpPost]
        [Authorize]
//创建旅游路线
        public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)

添加用户角色

Claim

  • 资源的所有权。
  • 表述用户的身份、说明用户的角色、表示用户所具有的权限。
  • 最小不可分割单位,使用的灵活度相当高,可以自由组合。

在开发网站的时候

  • 验证与授权完全分开。
  • 使用的是基于Claims的身份认证体系。
  • JWT只是Claim的其中一种应用方式而已。

AuthenticateController.cs

  • 新增管理员身份
  • new Claim(ClaimTypes.Role, "Admin")
            // payload
            var claims = new[]
            {
                // sub
                new Claim(JwtRegisteredClaimNames.Sub, "fake_user_id")
                new Claim(ClaimTypes.Role, "Admin")
            };

TouristRoutesController.cs - CreateTouristRoute方法

  • [Authorize]内加上(Roles = "Admin")
  • 这样就只有网站管理员才有权限访问该Api。
        [HttpPost]
        [Authorize(Roles = "Admin")]
        //创建旅游路线
        public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)

用户模型设计与数据库更新

安装microsoft.aspnetcore.identity.entityframeworkcore

AppDbContext.cs

  • IdentityDbContext 代替DbContext继承
    public class AppDbContext :IdentityDbContext<IdentityUser> /*: DbContext*/

添加服务依赖(Startup.cs)ConfigureServices

            services.AddIdentity<IdentityUser, IdentityRole>()
                 .AddEntityFrameworkStores<AppDbContext>();

进行数据迁移工作

Package Manage Console (PMC)

  • 如果报错,请降低包版本。
add-migration initialMigration
    
update-database

以Asp开头的表即为刚刚数据迁移工作所建立的数据表

用户注册

api

localhost/auth/register

AuthenticateController.cs

        private readonly UserManager<IdentityUser> _userManager;

//用户注册
        [AllowAnonymous]
        [HttpPost("register")]
        public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
        {
            // 1 使用用户名创建用户对象
            var user = new IdentityUser()
            {
                UserName = registerDto.Email,
                Email = registerDto.Email
            };

            // 2 hash密码,保存用户
            var result = await _userManager.CreateAsync(user, registerDto.Password);
            if (!result.Succeeded)
            {
                return BadRequest();
            }

            // 3 return
            return Ok();
        }

注入服务依赖


        //注入服务依赖
        public AuthenticateController(
            IConfiguration configuration,
            UserManager<IdentityUser> userManager
        )
        {
            _configuration = configuration;
            _userManager = userManager;
        }

创建RegisterDto

using System.ComponentModel.DataAnnotations;

namespace _02NET___CJ_ASP_Travel.Dtos
{
    public class RegisterDto
    {
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
        [Required]
        [Compare(nameof(Password), ErrorMessage = "密码输入不一致")]
        public string ConfirmPassword { get; set; }
    }
}

用户登陆

添加服务依赖

        private readonly SignInManager<IdentityUser> _signInManager;

        //注入服务依赖
        public AuthenticateController(
            IConfiguration configuration,
            UserManager<IdentityUser> userManager,
            SignInManager<IdentityUser> signInManager
        )
        {
            _configuration = configuration;
            _userManager = userManager;
            _signInManager = signInManager;
        }

验证用户名密码(AuthenticateController.cs)

        [AllowAnonymous]
        [HttpPost("login")]
        public async Task<IActionResult> login([FromBody] LoginDto loginDto)
        {
            // 1 验证用户名密码
            var loginResult = await _signInManager.PasswordSignInAsync(
                loginDto.Email,
                loginDto.Password,
                false,
                false
            );
            if (!loginResult.Succeeded)
            {
                return BadRequest();
            }

            var user = await _userManager.FindByNameAsync(loginDto.Email);
            
            //----------------省略

"fake_user_id"修改为user.Id

            // payload
            var claims = new List<Claim>
            {
                // sub
                new Claim(JwtRegisteredClaimNames.Sub, user.Id),
                new Claim(ClaimTypes.Role, "Admin")
            };
            var roleNames = await _userManager.GetRolesAsync(user);
            foreach (var roleName in roleNames)
            {
                var roleClaim = new Claim(ClaimTypes.Role, roleName);
                claims.Add(roleClaim);
            }

给所有api添加上 [Authorize(AuthenticationSchemes = "Bearer")]

namespace FakeXiecheng.API.Controllers
{
    [Route("api/[controller]")] // api/touristroute
    [ApiController]
    public class TouristRoutesController : ControllerBase
    {
        private ITouristRouteRepository _touristRouteRepository;
        private readonly IMapper _mapper;

        public TouristRoutesController(
            ITouristRouteRepository touristRouteRepository,
            IMapper mapper
        )
        {
            _touristRouteRepository = touristRouteRepository;
            _mapper = mapper;
        }

        // api/touristRoutes?keyword=传入的参数
        [HttpGet]
        [HttpHead]
        public async Task<IActionResult> GerTouristRoutes(
            [FromQuery] TouristRouteResourceParamaters paramaters
        //[FromQuery] string keyword,
        //string rating // 小于lessThan, 大于largerThan, 等于equalTo lessThan3, largerThan2, equalTo5 
        )// FromQuery vs FromBody
        {
            var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutesAsync(paramaters.Keyword, paramaters.RatingOperator, paramaters.RatingValue);
            if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
            {
                return NotFound("没有旅游路线");
            }
            var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
            return Ok(touristRoutesDto);
        }

        // api/touristroutes/{touristRouteId}
        [HttpGet("{touristRouteId}", Name = "GetTouristRouteById")]
        public async Task<IActionResult> GetTouristRouteById(Guid touristRouteId)
        {
            var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
            if (touristRouteFromRepo == null)
            {
                return NotFound($"旅游路线{touristRouteId}找不到");
            }

            var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
            return Ok(touristRouteDto);
        }

        [HttpPost]
        [Authorize(AuthenticationSchemes = "Bearer")]
        [Authorize]
        public async Task<IActionResult> CreateTouristRoute([FromBody] TouristRouteForCreationDto touristRouteForCreationDto)
        {
            var touristRouteModel = _mapper.Map<TouristRoute>(touristRouteForCreationDto);
            _touristRouteRepository.AddTouristRoute(touristRouteModel);
            await _touristRouteRepository.SaveAsync();
            var touristRouteToReture = _mapper.Map<TouristRouteDto>(touristRouteModel);
            return CreatedAtRoute(
                "GetTouristRouteById",
                new { touristRouteId = touristRouteToReture.Id },
                touristRouteToReture
            );
        }

        [HttpPut("{touristRouteId}")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        [Authorize(Roles = "Admin")]
        public async Task<IActionResult> UpdateTouristRoute(
            [FromRoute] Guid touristRouteId,
            [FromBody] TouristRouteForUpdateDto touristRouteForUpdateDto
        )
        {
            if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
            {
                return NotFound("旅游路线找不到");
            }

            var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
            // 1. 映射dto
            // 2. 更新dto
            // 3. 映射model
            _mapper.Map(touristRouteForUpdateDto, touristRouteFromRepo);

            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }

        [HttpPatch("{touristRouteId}")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        [Authorize(Roles = "Admin")]
        public async Task<IActionResult> PartiallyUpdateTouristRoute(
            [FromRoute] Guid touristRouteId,
            [FromBody] JsonPatchDocument<TouristRouteForUpdateDto> patchDocument
        )
        {
            if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
            {
                return NotFound("旅游路线找不到");
            }

            var touristRouteFromRepo = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
            var touristRouteToPatch = _mapper.Map<TouristRouteForUpdateDto>(touristRouteFromRepo);
            patchDocument.ApplyTo(touristRouteToPatch, ModelState);
            if (!TryValidateModel(touristRouteToPatch))
            {
                return ValidationProblem(ModelState);
            }
            _mapper.Map(touristRouteToPatch, touristRouteFromRepo);
            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }

        [HttpDelete("{touristRouteId}")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        [Authorize(Roles = "Admin")]
        public async Task<IActionResult> DeleteTouristRoute([FromRoute] Guid touristRouteId)
        {
            if (!(await _touristRouteRepository.TouristRouteExistsAsync(touristRouteId)))
            {
                return NotFound("旅游路线找不到");
            }

            var touristRoute = await _touristRouteRepository.GetTouristRouteAsync(touristRouteId);
            _touristRouteRepository.DeleteTouristRoute(touristRoute);
            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }

        [HttpDelete("({touristIDs})")]
        [Authorize(Roles = "Admin")]
        public async Task<IActionResult> DeleteByIDs(
            [ModelBinder(BinderType = typeof(ArrayModelBinder))][FromRoute] IEnumerable<Guid> touristIDs)
        {
            if (touristIDs == null)
            {
                return BadRequest();
            }

            var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutesByIDListAsync(touristIDs);
            _touristRouteRepository.DeleteTouristRoutes(touristRoutesFromRepo);
            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }
    }
}

定制用户模型并添加初始化用户数据

创建自定义用户模型ApplicationUser.cs

namespace _03NET___CJ_ASP_Travel3.Models
{
    public class ApplicationUser : IdentityUser
    {
        public string Address { get; set; }
        // ShoppingCart
        // Order

        public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
    }
}

初始化用户与角色的种子数据(AppDbContext.cs)

            // 初始化用户与角色的种子数据
            // 1. 更新用户与角色的外键关系
            modelBuilder.Entity<ApplicationUser>(b => {
                b.HasMany(x => x.UserRoles)
                .WithOne()
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
            });

            // 2. 添加角色
            var adminRoleId = "308660dc-ae51-480f-824d-7dca6714c3e2"; // guid 
            modelBuilder.Entity<IdentityRole>().HasData(
                new IdentityRole
                {
                    Id = adminRoleId, 
                    Name = "Admin",
                    NormalizedName = "Admin".ToUpper()
                }
            );

            // 3. 添加用户
            var adminUserId = "90184155-dee0-40c9-bb1e-b5ed07afc04e";
            ApplicationUser adminUser = new ApplicationUser
            {
                Id = adminUserId,
                UserName = "admin@fakexiecheng.com",
                NormalizedUserName = "admin@fakexiecheng.com".ToUpper(),
                Email = "admin@fakexiecheng.com",
                NormalizedEmail = "admin@fakexiecheng.com".ToUpper(),
                TwoFactorEnabled = false,
                EmailConfirmed = true,
                PhoneNumber = "123456789",
                PhoneNumberConfirmed = false
            };
            PasswordHasher<ApplicationUser> ph = new PasswordHasher<ApplicationUser>();
            adminUser.PasswordHash = ph.HashPassword(adminUser, "Fake123$");
            modelBuilder.Entity<ApplicationUser>().HasData(adminUser);

            // 4. 给用户加入管理员权限
            // 通过使用 linking table:IdentityUserRole
            modelBuilder.Entity<IdentityUserRole<string>>()
                .HasData(new IdentityUserRole<string>() 
                {
                    RoleId = adminRoleId,
                    UserId = adminUserId
                });

            base.OnModelCreating(modelBuilder);

数据更新

Package Manage Console (PMC)

  • 如果报错,请降低包版本。
add-migration initialMigration
    
update-database

十二、【购物系统从0到1】功能完整的购物车开发

开发概要与接口设计

如何设计我们的购物车

与其他系统产生的关联关系

购物车与其他系统的关系

  • 购物车与会员系统
  • 购物车与商品系统
  • 购物车与价格系统
  • 购物车与订单系统

购物车基本功能

Restful风格的购物车api接口设计

购物车模型设计与数据库更新

购物系统的流程

  • Lineltem使其联动起来。

模型优先

(数据模型)Lineltem.cs

namespace _03NET___CJ_ASP_Travel3.Models
{
    public class LineItem
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        [ForeignKey("TouristRouteId")]
        public Guid TouristRouteId { get; set; }
        public TouristRoute TouristRoute { get; set; }
        public Guid? ShoppingCartId { get; set; }
        //public Guid? OrderId { get; set; }
        [Column(TypeName = "decimal(18, 2)")]
        public decimal OriginalPrice { get; set; }
        [Range(0.0, 1.0)]
        public double? DiscountPresent { get; set; }

    }
}

ShoppingCart.cs

namespace _03NET___CJ_ASP_Travel3.Models
{
    public class ShoppingCart
    {
        [Key]
        public Guid Id { get; set; }
        public string UserId { get; set; }
        public ApplicationUser User { get; set; }
        public ICollection<LineItem> ShoppingCartItems { get; set; }
    }
}

添加购物车的外键关系(ApplicationUser.cs)

        // ShoppingCart
        public ShoppingCart ShoppingCart { get; set; }

添加引用(AppDbContext.cs)

        public DbSet<ShoppingCart> ShoppingCarts { get; set; }
        public DbSet<LineItem> LineItems { get; set; }

数据更新

Package Manage Console (PMC)

  • 如果报错,请降低包版本。
add-migration initialMigration
    
update-database

获得当前用户的购物车

新建ShoppingCartController.cs

using _04NET___CJ_ASP_Travel4.Services;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace _04NET___CJ_ASP_Travel4.Controllers
{
    [ApiController]
    [Route("api/shoppingCart")]
    public class ShoppingCartController : ControllerBase
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ITouristRouteRepository _touristRouteRepository;
        private readonly IMapper _mapper;

        public ShoppingCartController(
            IHttpContextAccessor httpContextAccessor,
            ITouristRouteRepository touristRouteRepository,
            IMapper mapper
        )
        {
            _httpContextAccessor = httpContextAccessor;
            _touristRouteRepository = touristRouteRepository;
            _mapper = mapper;
        }

        [HttpGet]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> GetShoppingCart()
        {
            // 1 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            // 2 使用userid获得购物车
            var shoppingCart = await _touristRouteRepository.GetShoppingCartByUserId(userId);

            return Ok(_mapper.Map<ShoppingCartDto>(shoppingCart));
        }
    }
}

新增接口函数(ITouristRouteRepository.cs)

        Task<ShoppingCart> GetShoppingCartByUserId(string userId);
        Task CreateShoppingCart(ShoppingCart shoppingCart);

实现接口 TouristRouteRepository.cs

        public async Task<ShoppingCart> GetShoppingCartByUserId(string userId)
        {
            return await _context.ShoppingCarts
                .Include(s => s.User)
                .Include(s => s.ShoppingCartItems).ThenInclude(li => li.TouristRoute)
                .Where(s => s.UserId == userId)
                .FirstOrDefaultAsync();
        }

        public async Task CreateShoppingCart(ShoppingCart shoppingCart)
        {
            await _context.ShoppingCarts.AddAsync(shoppingCart);
        }

初始化用户购物车(AuthenticateController.cs)

        private readonly ITouristRouteRepository _touristRouteRepository;

        public AuthenticateController(
            IConfiguration configuration,
            UserManager<ApplicationUser> userManager,
            SignInManager<ApplicationUser> signInManager,
            ITouristRouteRepository touristRouteRepository
        )
        {
            _configuration = configuration;
            _userManager = userManager;
            _signInManager = signInManager;
            _touristRouteRepository = touristRouteRepository;
        }

//-----------------------省略

            // 3 初始化购物车
            var shoppingCart = new ShoppingCart()
            {
                Id = Guid.NewGuid(),
                UserId = user.Id
            };
            await _touristRouteRepository.CreateShoppingCart(shoppingCart);
            await _touristRouteRepository.SaveAsync();

            // 4 return
            return Ok();
        }
    }
}

ShoppingCartDto

namespace _04NET___CJ_ASP_Travel4.Dtos
{
    public class ShoppingCartDto
    {
        public Guid Id { get; set; }
        public string UserId { get; set; }
        public ICollection<LineItemDto> ShoppingCartItems { get; set; }
    }
}

新建LineItemDto

namespace _04NET___CJ_ASP_Travel4.Dtos
{
    public class LineItemDto
    {
        public int Id { get; set; }
        public Guid TouristRouteId { get; set; }
        public TouristRouteDto TouristRoute { get; set; }
        public Guid? ShoppingCartId { get; set; }
        //public Guid? OrderId { get; set; }
        public decimal OriginalPrice { get; set; }
        public double? DiscountPresent { get; set; }
    }
}

新建ShoppingCartProfile.cs

namespace _04NET___CJ_ASP_Travel4.Profiles
{
    public class ShoppingCartProfile : Profile
    {
        public ShoppingCartProfile()
        {
            CreateMap<ShoppingCart, ShoppingCartDto>();
            CreateMap<LineItem, LineItemDto>();
        }
    }
}

测试

注册用户

https://localhost:44381/auth/register

{
    "email": "alex1234@163.com",
    "password":"Fake123&",
    "ConfirmPassword":"Fake123&"
}

用户登录

https://localhost:44381/auth/login

{
    "email": "alex1234@163.com",
    "password":"Fake123&"

}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI2YzQwY2U4NS04NTQ0LTRkYzItOGE5NS1iNTU1MzVhNjgwNmMiLCJuYmYiOjE2NzU1Nzk1MTYsImV4cCI6MTY3NTY2NTkxNiwiaXNzIjoiYmFpZHUuY29tIiwiYXVkIjoiYmFpZHUuY29tLmNvbSJ9.Kw4NiXbnJEp2AjivnDKdgW8bj6btjsS_M2wT91j2YsQ

请求https://localhost:44381/api/shoppingCart

向购物车加入商品

添加商品函数ShoppingCartController.cs

[HttpPost("items")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> AddShoppingCartItem(
            [FromBody] AddShoppingCartItemDto addShoppingCartItemDto
        )
        {
            // 1 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            // 2 使用userid获得购物车
            var shoppingCart = await _touristRouteRepository
                .GetShoppingCartByUserId(userId);

            // 3 创建lineItem
            var touristRoute = await _touristRouteRepository
                .GetTouristRouteAsync(addShoppingCartItemDto.TouristRouteId);
            if(touristRoute == null)
            {
                return NotFound("旅游路线不存在");
            }

            var lineItem = new LineItem()
            {
                TouristRouteId = addShoppingCartItemDto.TouristRouteId,
                ShoppingCartId = shoppingCart.Id,
                OriginalPrice = touristRoute.OriginalPrice,
                DiscountPresent = touristRoute.DiscountPresent
            };

            // 4 添加lineitem,并保存数据库
            await _touristRouteRepository.AddShoppingCartItem(lineItem);
            await _touristRouteRepository.SaveAsync();

            return Ok(_mapper.Map<ShoppingCartDto>(shoppingCart));
        }

新建AddShoppingCartItemDto.cs

namespace _04NET___CJ_ASP_Travel4.Dtos
{
    public class AddShoppingCartItemDto
    {
        public Guid TouristRouteId { get; set; }
    }
}

添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs

        Task AddShoppingCartItem(LineItem lineItem);
        public async Task AddShoppingCartItem(LineItem lineItem)
        {
            await _context.LineItems.AddAsync(lineItem);
        }

请求https://localhost:44381/api/shoppingCart/items

从购物车删除商品

删除商品函数ShoppingCartController.cs

        //删除商品
        [HttpDelete("items/{itemId}")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> DeleteShoppingCartItem([FromRoute] int itemId)
        {
            // 1 获取lineitem数据
            var lineItem = await _touristRouteRepository
                .GetShoppingCartItemByItemId(itemId);
            if (lineItem == null)
            {
                return NotFound("购物车商品找不到");
            }

            _touristRouteRepository.DeleteShoppingCartItem(lineItem);
            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }

添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs

        Task<LineItem> GetShoppingCartItemByItemId(int lineItemId);
        void DeleteShoppingCartItem(LineItem lineItem);
        public async Task<LineItem> GetShoppingCartItemByItemId(int lineItemId)
        {
            return await _context.LineItems
                .Where(li => li.Id == lineItemId)
                .FirstOrDefaultAsync();
        }

        public async void DeleteShoppingCartItem(LineItem lineItem)
        {
            _context.LineItems.Remove(lineItem);
        }

测试 - 请求https://localhost:44381/api/shoppingCart/items/1

  • 返回204

从购物车批量删除商品

批量删除商品函数ShoppingCartController.cs

        [HttpDelete("items/({itemIDs})")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> RemoveShoppingCartItems(
            [ModelBinder(BinderType = typeof(ArrayModelBinder))]
            [FromRoute] IEnumerable<int> itemIDs
        )
        {
            var lineitems = await _touristRouteRepository
                .GeshoppingCartsByIdListAsync(itemIDs);

            _touristRouteRepository.DeleteShoppingCartItems(lineitems);
            await _touristRouteRepository.SaveAsync();

            return NoContent();
        }

添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs

        //批量删除
        Task<IEnumerable<LineItem>> GeshoppingCartsByIdListAsync(IEnumerable<int> ids);
        void DeleteShoppingCartItems(IEnumerable<LineItem> lineItems);
        //批量删除
        public async Task<IEnumerable<LineItem>> GeshoppingCartsByIdListAsync(
    IEnumerable<int> ids)
        {
            return await _context.LineItems
                .Where(li => ids.Contains(li.Id))
                .ToListAsync();
        }

        public void DeleteShoppingCartItems(IEnumerable<LineItem> lineItems)
        {
            _context.LineItems.RemoveRange(lineItems);
        }

十三、【购物系统从0到1】极简主义的订单系统

开发概要与接口设计

如何设计电商网站的订单系统

  • 订单系统的角色
  • 订单系统核心功能,业务流程
  • 订单的状态处理
  • 订单API接口设计

订单系统的角色

订单系统核心功能

  • 交易凭证,记录交易过程(订单信息)
  • 完成交易闭环(订单状态流转)

订单状态

RestFul风格的订单API接口设计

订单模型开发与数据库更新

新建Model - Order.cs

namespace _04NET___CJ_ASP_Travel4.Models
{
    public enum OrderStateEnum
    {
        Pending, // 订单已生成
        Processing, // 支付处理中
        Completed, // 交易成功
        Declined, // 交易失败
        Cancelled, // 订单取消
        Refund, // 已退款
    }

    public class Order
    {
        [Key]
        public Guid Id { get; set; }
        public string UserId { get; set; }
        public ApplicationUser User { get; set; }
        public ICollection<LineItem> OrderItems { get; set; }
        public OrderStateEnum State { get; set; }
        public DateTime CreateDateUTC { get; set; }
        public string TransactionMetadata { get; set; }
    }
}

ApplicationUser.cs

        public ICollection<Order> Orders { get; set; }

AppDbContext.cs

        public DbSet<Order> Orders { get; set; }

更新数据库

add-migration OrderMigration
    
update-database

订单的有限状态

有限状态机

状态机4要素

使用Stateless实现订单状态机

Model - Order.cs

    public enum OrderStateTriggerEnum
    {
        PlaceOrder, // 支付
        Approve, // 收款成功
        Reject, // 收款失败
        Cancel, // 取消
        Return // 退货
    }

使用Stateless实现订单状态机

order.cs

using Stateless;

public class Order
{
    //状态机
            public Order()
        {
            StateMachineInit();
        }
        StateMachine<OrderStateEnum, OrderStateTriggerEnum> _machine;

        private void StateMachineInit()
        {
            _machine = new StateMachine<OrderStateEnum, OrderStateTriggerEnum>
            (OrderStateEnum.Pending);

            _machine.Configure(OrderStateEnum.Pending)
                .Permit(OrderStateTriggerEnum.PlaceOrder, OrderStateEnum.Processing)
                .Permit(OrderStateTriggerEnum.Cancel, OrderStateEnum.Cancelled);

            _machine.Configure(OrderStateEnum.Processing)
                .Permit(OrderStateTriggerEnum.Approve, OrderStateEnum.Completed)
                .Permit(OrderStateTriggerEnum.Reject, OrderStateEnum.Declined);

            _machine.Configure(OrderStateEnum.Declined)
                .Permit(OrderStateTriggerEnum.PlaceOrder, OrderStateEnum.Processing);

            _machine.Configure(OrderStateEnum.Completed)
                .Permit(OrderStateTriggerEnum.Return, OrderStateEnum.Refund);
        }
}

购物车下单、结算

购物车结算函数(ShoppingCartController.cs)

        [HttpPost("checkout")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> Checkout()
        {
            // 1 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            // 2 使用userid获得购物车
            var shoppingCart = await _touristRouteRepository.GetShoppingCartByUserId(userId);

            // 3 创建订单
            var order = new Order()
            {
                Id = Guid.NewGuid(),
                UserId = userId,
                State = OrderStateEnum.Pending,
                OrderItems = shoppingCart.ShoppingCartItems,
                CreateDateUTC = DateTime.UtcNow,
            };

            shoppingCart.ShoppingCartItems = null;

            // 4 保存数据
            await _touristRouteRepository.AddOrderAsync(order);
            await _touristRouteRepository.SaveAsync();

            // 5 return
            return Ok(_mapper.Map<OrderDto>(order));
        }

添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs

        Task AddOrderAsync(Order order);
        public async Task AddOrderAsync(Order order)
        {
            await _context.Orders.AddAsync(order);
        }

返回响应OrderDto.cs

namespace _04NET___CJ_ASP_Travel4.API.Dtos
{
    public class OrderDto
    {
        public Guid Id { get; set; }
        public string UserId { get; set; }
        public ICollection<LineItemDto> OrderItems { get; set; }
        public string State { get; set; }
        public DateTime CreateDateUTC { get; set; }
        public string TransactionMetadata { get; set; }
    }
}

OrderProfile.cs

namespace _04NET___CJ_ASP_Travel4.Profiles
{
    public class OrderProfile : Profile
    {
        public OrderProfile()
        {
            CreateMap<Order, OrderDto>()
                .ForMember(
                    dest => dest.State,
                    opt =>
                    {
                        opt.MapFrom(src => src.State.ToString());
                    }
                );
        }
    }
}

获得用户订单

新建OrdersController.cs

namespace _04NET___CJ_ASP_Travel4.Controllers
{
    [ApiController]
    [Route("api/orders")]
    public class OrdersController : ControllerBase
    {
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ITouristRouteRepository _touristRouteRepository;
        private readonly IMapper _mapper;

        public OrdersController(
            IHttpContextAccessor httpContextAccessor,
            ITouristRouteRepository touristRouteRepository,
            IMapper mapper
        )
        {
            _httpContextAccessor = httpContextAccessor;
            _touristRouteRepository = touristRouteRepository;
            _mapper = mapper;
        }

        [HttpGet]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> GetOrders()
        {
            // 1. 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            // 2. 使用用户id来获取订单历史记录
            var orders = await _touristRouteRepository.GetOrdersByUserId(userId);

            return Ok(_mapper.Map<IEnumerable<OrderDto>>(orders));
        }

        [HttpGet("{orderId}")]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> GerOrderById([FromRoute] Guid orderId)
        {
            // 1. 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            var order = await _touristRouteRepository.GetOrderById(orderId);

            return Ok(_mapper.Map<OrderDto>(order));
        }

    }
}

添加和实现新的接口函数ITouristRouteRepository.cs / TouristRouteRepository.cs

    Task<IEnumerable<Order>> GetOrdersByUserId(string userId);
        Task<Order> GetOrderById(Guid orderId);
        public async Task<IEnumerable<Order>> GetOrdersByUserId(string userId)
        {
            return await _context.Orders.Where(o => o.UserId == userId).ToListAsync();
        }

        public async Task<Order> GetOrderById(Guid orderId)
        {
            return await _context.Orders
                .Include(o => o.OrderItems).ThenInclude(oi => oi.TouristRoute)
                .Where(o => o.Id == orderId)
                .FirstOrDefaultAsync();
        }

十四、【RESTful技能进阶】数据分页显示

  • 如何接受分页参数
  • 如何返回分页信息
  • 进行模块化分页处理

分页与项目架构浅析

数据分页Pagination

分页思路

项目架构

完成简单分页

TouristRouteResourceParamaters.cs

        private int _pageNumber = 1;
        public int PageNumber {
            get
            {
                return _pageNumber;
            }
            set
            {
                if(value >= 1)
                {
                    _pageNumber = value;
                }
            }
        }
        private int _pageSize = 10;
        const int maxPageSize = 50;
        public int PageSize {
            get 
            {
                return _pageSize;
            }
            set
            {
                if (value >= 1)
                {
                    _pageSize = (value > maxPageSize) ? maxPageSize : value;
                }
            }
        }

TouristRoutesController.cs 新增两参数

            var touristRoutesFromRepo = await _touristRouteRepository
                .GetTouristRoutesAsync(
                    paramaters.Keyword,
                    paramaters.RatingOperator,
                    paramaters.RatingValue,
                    //分页
                    paramaters.PageSize,
                    paramaters.PageNumber
                );

ITouristRouteRepository.cs 新增两参数

      Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
    string keyword, string ratingOperator, int? ratingValue
            //分页
    , int pageSize, int pageNumber);

TouristRouteRepository.cs

        public async Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
            string keyword, 
            string ratingOperator, 
            int? ratingValue,
             //分页
            int pageSize,
            int pageNumber
        )
            
//省略---------------------------

            // pagination
            // skip
            var skip = (pageNumber - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize为标准显示一定量的数据
            result = result.Take(pageSize);

测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=5&pagesize=1

  • 即使不加后缀条件,也默认显示第一页的数据。

分页进阶:模组化

分页组件类:PaginationList <T>

Helper - PaginationList.cs

namespace _04NET___CJ_ASP_Travel4.Helper
{
    public class PaginationList<T> : List<T>
    {
        public int CurrentPage { get; set; }
        public int PageSize { get; set; }

        public PaginationList(int currentPage, int pageSize, List<T> items)
        {
            CurrentPage = currentPage;
            PageSize = pageSize;
            AddRange(items);
        }

        public static async Task<PaginationList<T>> CreateAsync(
            int currentPage, int pageSize, IQueryable<T> result)
        {
            // pagination
            // skip
            var skip = (currentPage - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize为标准显示一定量的数据
            result = result.Take(pageSize);

            // include vs join
            var items = await result.ToListAsync();

            return new PaginationList<T>(currentPage, pageSize, items);
        }
    }
}

修改ITouristRouteRepository.cs和TouristRouteRepository.cs

  • IEnumerablePaginationList
        /*        Task<IEnumerable<TouristRoute>> GetTouristRoutesAsync(
            string keyword, string ratingOperator, int? ratingValue
                    //分页
            , int pageSize, int pageNumber);*/

        Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
            string keyword, string ratingOperator, int? ratingValue
            //分页
            , int pageSize, int pageNumber);
        //`IEnumerable`改`PaginationList`
        public async Task<PaginationList<TouristRoute>> GetTouristRoutesAsync(
            string keyword, 
            string ratingOperator, 
            int? ratingValue,
             //分页
            int pageSize,
            int pageNumber
        )

删除对分页的单独处理

/*            // pagination
            // skip
            var skip = (pageNumber - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize为标准显示一定量的数据
            result = result.Take(pageSize);*

工厂函数创建实例

            // include vs join
            return await PaginationList<TouristRoute>.CreateAsync(pageNumber, pageSize, result);

测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=1&pagesize=5

  • 获得第一页的数据,5条旅游路线。

复用模组化分页

新建PaginationResourceParamaters.cs

  • 剪切TouristRouteResourceParamaters.cs中的所有分页方法。
namespace _04NET___CJ_ASP_Travel4.ResourceParameters
{
    public class PaginationResourceParamaters
    {
        private int _pageNumber = 1;
        public int PageNumber
        {
            get
            {
                return _pageNumber;
            }
            set
            {
                if (value >= 1)
                {
                    _pageNumber = value;
                }
            }
        }
        private int _pageSize = 10;
        const int maxPageSize = 50;
        public int PageSize
        {
            get
            {
                return _pageSize;
            }
            set
            {
                if (value >= 1)
                {
                    _pageSize = (value > maxPageSize) ? maxPageSize : value;
                }
            }
        }
    }
}

修改TouristRoutesController.cs的GerTouristRoutes方法

[FromQuery] PaginationResourceParamaters paramaters2

        public async Task<IActionResult> GerTouristRoutes(
            [FromQuery] TouristRouteResourceParamaters paramaters,
            //新增代码-------------------
            [FromQuery] PaginationResourceParamaters paramaters2
        
        )
            
                  {
            var touristRoutesFromRepo = await _touristRouteRepository
                .GetTouristRoutesAsync(
                    paramaters.Keyword,
                    paramaters.RatingOperator,
                    paramaters.RatingValue,
                    //分页
                    paramaters2.PageSize,
                    paramaters2.PageNumber
                );
            //-------------省略
        }

OrdersController.cs - GetOrders

  • 加入分页参数
  • [FromQuery] PaginationResourceParamaters paramaters
        [HttpGet]
        [Authorize(AuthenticationSchemes = "Bearer")]
        public async Task<IActionResult> GetOrders([FromQuery] PaginationResourceParamaters paramaters)
        {
            // 1. 获得当前用户
            var userId = _httpContextAccessor
                .HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;

            // 2. 使用用户id来获取订单历史记录
            var orders = await _touristRouteRepository.GetOrdersByUserId(
                userId, paramaters.PageSize, paramaters.PageNumber);

            return Ok(_mapper.Map<IEnumerable<OrderDto>>(orders));
        }

修改ITouristRouteRepository.cs和TouristRouteRepository.cs

        /*        Task<IEnumerable<Order>> GetOrdersByUserId(string userId);*/
        Task<PaginationList<Order>> GetOrdersByUserId(string userId, int pageSize, int pageNumber);
        public async Task<PaginationList<Order>> GetOrdersByUserId(
            string userId, int pageSize, int pageNumber)
        {
            //return await _context.Orders.Where(o => o.UserId == userId).ToListAsync();
            IQueryable<Order> result = _context.Orders.Where(o => o.UserId == userId);
            return await PaginationList<Order>.CreateAsync(pageNumber, pageSize, result);
        }

分页导航

响应中包含哪些分页数据呢

  • 数据列表将会出现在响应主体中。
  • 而分页的信息与数据列表彻底分开。
  • metadata元数据的形式在header中输出。

分页的元数据pagination metadata

在RESTful中为什么要将分页信息与数据列表分开?

  • 请求使用application/json,目的是获取资源。
  • 分页信息并不是资源,所以称为元数据metadata而不是资源数据resource。
  • 分页导航属于api成孰度level3级别。
  • HATEOAS: API的自我发现机制。

拓展PaginationList<T>:给header添加导航

改造PaginationList

PaginationList.cs

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace _04NET___CJ_ASP_Travel4.Helper
{
    public class PaginationList<T> : List<T>
    {
        public int TotalPages { get; private set; }
        public int TotalCount { get; private set; }
        public bool HasPrevious => CurrentPage > 1;
        public bool HasNext => CurrentPage < TotalPages;
        public int CurrentPage { get; set; }
        public int PageSize { get; set; }

        public PaginationList(int totalCount, int currentPage, int pageSize, List<T> items)
        {
            CurrentPage = currentPage;
            PageSize = pageSize;
            AddRange(items);
            TotalCount = totalCount;
            TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
        }

        public static async Task<PaginationList<T>> CreateAsync(
            int currentPage, int pageSize, IQueryable<T> result)
        {
            var totalCount = await result.CountAsync();
            // pagination
            // skip
            var skip = (currentPage - 1) * pageSize;
            result = result.Skip(skip);
            // 以pagesize为标准显示一定量的数据
            result = result.Take(pageSize);

            // include vs join
            var items = await result.ToListAsync();

            return new PaginationList<T>(totalCount, currentPage, pageSize, items);
        }
    }
}

新建Helper - ResourceUriType.cs

namespace _04NET___CJ_ASP_Travel4.Helper
{
    public enum ResourceUriType
    {
        PreviousPage,
        NextPage
    }
}

_urlHelper需注册服务(Start.cs)ConfigureServices

  • 注册IAtionContextAccessor
            services.AddHttpClient();
          services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

创建分页URL导航

添加响应头部 x-pagination

TourisRoutesController.cs - 改造GerTouristRoutes方法

private readonly IUrlHelper _urlHelper;

private string GenerateTouristRouteResourceURL(
            TouristRouteResourceParamaters paramaters,
            PaginationResourceParamaters paramaters2,
            ResourceUriType type
        )
        {
            return type switch
            {
                ResourceUriType.PreviousPage => _urlHelper.Link("GetTouristRoutes",
                    new
                    {
                        keyword = paramaters.Keyword,
                        rating = paramaters.Rating,
                        pageNumber = paramaters2.PageNumber - 1,
                        pageSize = paramaters2.PageSize
                    }),
                ResourceUriType.NextPage => _urlHelper.Link("GetTouristRoutes",
                    new
                    {
                        keyword = paramaters.Keyword,
                        rating = paramaters.Rating,
                        pageNumber = paramaters2.PageNumber + 1,
                        pageSize = paramaters2.PageSize
                    }),
                _ => _urlHelper.Link("GetTouristRoutes",
                    new
                    {
                        keyword = paramaters.Keyword,
                        rating = paramaters.Rating,
                        pageNumber = paramaters2.PageNumber,
                        pageSize = paramaters2.PageSize
                    })
            };
        }

        // api/touristRoutes?keyword=传入的参数
        [HttpGet(Name = "GetTouristRoutes")]
        [HttpHead]
        public async Task<IActionResult> GerTouristRoutes(
            [FromQuery] TouristRouteResourceParamaters paramaters,
            [FromQuery] PaginationResourceParamaters paramaters2
        //[FromQuery] string keyword,
        //string rating // 小于lessThan, 大于largerThan, 等于equalTo lessThan3, largerThan2, equalTo5 
        )// FromQuery vs FromBody
        {
            var touristRoutesFromRepo = await _touristRouteRepository
                .GetTouristRoutesAsync(
                    paramaters.Keyword,
                    paramaters.RatingOperator,
                    paramaters.RatingValue,
                    //分页
                    paramaters2.PageSize,
                    paramaters2.PageNumber
                );
            if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
            {
                return NotFound("没有旅游路线");
            }
            var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
            
            var previousPageLink = touristRoutesFromRepo.HasPrevious
               ? GenerateTouristRouteResourceURL(
                   paramaters, paramaters2, ResourceUriType.PreviousPage)
               : null;

            var nextPageLink = touristRoutesFromRepo.HasNext
                ? GenerateTouristRouteResourceURL(
                    paramaters, paramaters2, ResourceUriType.NextPage)
                : null;

            // x-pagination
            var paginationMetadata = new
            {
                previousPageLink,
                nextPageLink,
                totalCount = touristRoutesFromRepo.TotalCount,
                pageSize = touristRoutesFromRepo.PageSize,
                currentPage = touristRoutesFromRepo.CurrentPage,
                totalPages = touristRoutesFromRepo.TotalPages
            };

            Response.Headers.Add("x-pagination",
                Newtonsoft.Json.JsonConvert.SerializeObject(paginationMetadata));
            
            return Ok(touristRoutesDto);
        }

测试 - 请求https://localhost:44381/api/touristRoutes?pagenumber=1&pagesize=5

x-pagination

  • 得到x-pagination信息
{
    "previousPageLink":null,
    "nextPageLink":"https://localhost:44381/api/TouristRoutes?pageNumber=2&pageSize=5",
    "totalCount":16,
    "pageSize":5,
    "currentPage":1,
    "totalPages":4
}

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 寸金游( 后端开发 )(二)


三二一的一的二