提交订单
我们目前已经完成了和客户交互的基本功能,接下来要完成最后在购物车页面添加的结算功能。当用户点击结算按钮会提交购物车,生成订单,而后台服务端要接收订单,最后更新数据库。因此,数据库要能保存订单,还要更新每种商品(电子书)的库存数量。
创建数据模型
因为我们是代码优先再用迁移命令创建数据表的,我们需要先创建一个订单的数据模型类 Order ,再去更新数据库结构。
Listing 9-1 在 Models 文件夹里新建一个 Orders.cs 文件
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EBooksStore.Models;
public class Order
{
public int Id { get; set; }
[Required(ErrorMessage = "请输入姓名")]
public string? Name { get; set; }
[Required(ErrorMessage = "请输入地址")]
public string? Address { get; set; }
[Required(ErrorMessage = "请输入城市")]
public string? City { get; set; }
[Required(ErrorMessage = "请输入邮政编码")]
public string? Zip { get; set; }
[Required(ErrorMessage = "请输入国家")]
public string? Country { get; set; }
[InverseProperty(nameof(OrderItem.Order))]
public IEnumerable<OrderItem>? OrderItems { get; set; }
}
我们给这些字段都加上了 Required 属性限定。最后一行是购物车里的商品。
每个订单都要有商品(电子书),我们需要再创建一个订单物品的数据模型类 OrderItem 。
Listing 9-2 在 Models 文件夹里新建一个 OrderItems.cs 文件
using System.ComponentModel.DataAnnotations.Schema;
namespace EBooksStore.Models
{
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
[ForeignKey(nameof(OrderId))]
[InverseProperty(nameof(Order.OrderItems))]
public Order? Order { get; set; }
public int EBookId { get; set; }
[ForeignKey(nameof(EBookId))]
public EBook? EBook { get; set; }
public int Quantity { get; set; }
}
}
[ForeignKey]
的外键属性限定 OrderId 字段是映射到 Order 数据表的主键,而这个表的 Order 叫导航字段,由映射关系链接到数据库表 Order 中一个唯一的记录,我们就可以直接在这个数据模型即 OrderItem 中存取 Order 中对应的记录。[InverseProperty]
反向属性显示指定相关数据表(也叫数据实体)中的导航字段。在这个例子里: Order 里的 OrderItems 和 OrderItem 里的 Order 互为反向属性。它们直接的关系定义:
OrderItem ↔ Order: 一对多关系:一个 Order 对应多个 OrderItem
OrderItem ↔ EBook: 一对一关系:一个 OrderItem 对应 一本 EBook (有些情况是多对一关系)
这种外键加导航字段的方式能够让 Entity Framework Core 自动处理关系,提高数据存储效率。
我们还要在数据库上下文里增加 DbSet Orders 和 DbSet OrderItems。
Listing 9-3 在 Models 文件夹的 StoreDbContext.cs 里添加数据集合类型
using Microsoft.EntityFrameworkCore;
namespace EBooksStore.Models;
public class StoreDbContext(DbContextOptions<StoreDbContext> options) : DbContext(options)
{
public DbSet<EBook> EBooks => Set<EBook>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
}
数据模型已经就绪,开始添加数据库迁移命令。在项目文件夹下开启 PowerShell ,在终端里输入如下语句:
Listing 9-4 添加数据库迁移
dotnet ef migrations add AddOrderAndOrderItem
dotnet ef database update
如果还没有注释掉 Program.cs 里的 Seeds.InitPopulate(app) 语句,我们也可以不用输入 dotnet ef database update 命令,因为在这个方法里会检测迁移和应用数据库更新。
注意:如果数据库迁移出现错误,可以用 dotnet ef migrations remove
移除最近一次的迁移记录。
如果实在都搞砸了,也可以删除所有数据表和数据库,再从头开始做初始化迁移。删除数据表和数据库命令(必须在项目文件夹执行):
dotnet ef database drop --force
我们一般是数据迁移完成后,再开始程序逻辑。数据为王嘛。先修改购物车页面的结算按钮的控制器和方法。
Listing 9-4 修改 Views/Cart 文件夹里的 Index.cshtml
...
<div class="col-sm-6">
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">继续购物</a>
<a asp-action="Checkout" asp-controller="Order" class="btn btn-primary">结算</a>
<!-- a 链接到 Order 控制器的 Checkout 方法 -->
</div>
<form class="col-sm-6 text-end" asp-action="ClearCart" method="post">
<button type="submit" class="btn btn-danger">清空购物车</button>
</form>
</div>
...
创建控制器和视图
我们在 Controllers 文件夹里添加 Order 控制器类。
Listing 9-5 在 Controllers 文件夹新建一个文件 OrderController.cs
using Microsoft.AspNetCore.Mvc;
namespace EBooksStore.Controllers;
public class OrderController : Controller
{
public IActionResult Checkout()
{
return View();
}
}
在 Views 文件夹新建文件夹 Order,我们为 Order 控制器添加视图。
Listing 9-6 在 Views/Order 文件夹新建一个文件 Checkout.cshtml
@model Order
@{
ViewData["Title"] = "结算";
}
<h2>结算</h2>
<p>请输入送货信息,我们会尽快发货。</p>
<form asp-action="Checkout" method="post">
<h3>收货信息</h3>
<div class="form-group">
<label asp-for="Name">姓名</label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Address">地址</label>
<input asp-for="Address" class="form-control" />
<span asp-validation-for="Address" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="City">城市</label>
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Zip">邮编</label>
<input asp-for="Zip" class="form-control" />
<span asp-validation-for="Zip" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Country">国家</label>
<input asp-for="Country" class="form-control" />
<span asp-validation-for="Country" class="text-danger"></span>
</div>
<div class="text-center my-3">
<button type="submit" class="btn btn-primary">提交订单</button>
</div>
</form>
现在我们运行应用,在购物车页面点击结算按钮,将会打开结算页面,如图:
Figure 9-1
完成订单过程
对数据库数据操作比较好的方法,还是采用仓库模式。我们将定义一个仓库接口以及实现方法,再用依赖注入(DI)到应用中。
Listing 9-7 在 Models/Interfaces 文件夹里新建一个文件 IOrderRepository.cs
namespace EBooksStore.Models.Interfaces;
public interface IOrderRepository
{
IQueryable<Order> Orders { get; }
void SaveOrder(Order order);
void SaveOrder(Order order, List<OrderItem> orderItems)
}
再创建这个接口的实现类。这里我们创建 2 个同名的 SaveOrder 方法,每个方法最后都是要保存数据库更新,具体的数据库交易由数据库上下文类里封装好了,我们只管用C# 方法就好。
Listing 9-8 在 Models 文件夹里新建一个文件 OrderRepository.cs
using EBooksStore.Models.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace EBooksStore.Models;
public class OrderRepository(StoreDbContext context) : IOrderRepository
{
private readonly StoreDbContext _context = context;
public IQueryable<Order> Orders => _context.Orders;
public void SaveOrder(Order order)
{
if (order.Id == 0)
{
_context.Orders.Add(order);
}
_context.SaveChanges();
}
public void SaveOrder(Order order, List<OrderItem> orderItems)
{
if (order.Id == 0)
{
_context.Orders.Add(order);
}
foreach (var item in orderItems)
{
item.OrderId = order.Id;
_context.OrderItems.Add(item);
}
_context.SaveChanges();
}
}
在 Program.cs 里添加 <IOrderRepository, OrderRepository>
依赖注入。
Listing 9-9 在 Program.cs 里添加依赖注入
...
builder.Services.AddScoped<IStoreRepository, StoreRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
...
现在 OrderRepository 已经可以被注入了,那 OrderItem 怎么产生? OrderItem 可以从 CartLine 获得,而 CartLine 在我们的会话里,所以我们可以直接从那里取得,也可以写一个服务,让它从会话里取得 CarLine。我们这里暂时用最简单粗暴的直接取用法把。LOL。
Listing 9-10 修改 Controllers 文件夹里的 OrderController.cs
using EBooksStore.Extensions;
using EBooksStore.Models;
using EBooksStore.Models.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace EBooksStore.Controllers;
public class OrderController(IOrderRepository orderRepository) : Controller
{
private readonly IOrderRepository _orderRepository = orderRepository;
public IActionResult Checkout() => View(new Order());
[HttpPost]
public IActionResult Checkout(Order order)
{
// 直接从会话里取得行
var cart = HttpContext.Session.Get<List<CartLine>>("CartLines");
if (cart == null || cart.Count == 0)
{
ModelState.AddModelError("", "您的购物车是空的!");
}
else
{
List<OrderItem> orderItems = [];
foreach (var line in cart)
{
orderItems.Add(new OrderItem
{
EBookId = line.EBook.Id,
Quantity = line.Quantity
});
}
if (ModelState.IsValid)
{
order.OrderItems = orderItems;
// 用2个参数的签名的方法
_orderRepository.SaveOrder(order, orderItems);
// 清空会话数据
HttpContext.Session.Set("CartLines", new List<CartLine>());
HttpContext.Session.Set("DisabledCartItems", new List<int>());
return RedirectToAction();
}
}
return View(order);
}
}
还是按照前面的习惯,我们新建一个 HttpPost 方法,这个方法的参数是我们绑定的 Order 对象,我们用两个参数的签名的 SaveOrder 方法写入数据库并提交。ModelState 是ASP.NET Core
的固定用法,用它来保存绑定的数据模型的信息和有效性等。我们可以在代码中加入自定义错误信息。只有保证数据模型状态合法,我们才会保存数据库更新。最基本和有效的保证数据合法的逻辑,我们还是要在和客户交互的页面进行。
为了保证用户输入数据的初步合法性,这里是我们至少要让数据符合我们在 Models 文件里定义的 Order 类的属性限定。所有这些限定都会在 ModelState 里验证。我们也有必要告诉警告用户输入数据的问题,所以我们要在视图里加入合法性检测的 Tag Helper 页面元素。
Listing 9-11 在 Views/Order 文件里的 Checkout.cshtml 里添加检查
...
<h2>结算</h2>
<p>请输入送货信息,我们会尽快发货。</p>
<div asp-validation-summary="All" class="text-danger"></div>
<form asp-action="Checkout" method="post">
<h3>收货信息</h3>
...
这里我们选择把所有的验证信息都反馈给用户。
Figure 9-2
每次订单完成以后,我们还要给用户一个感谢信息,不能简单的回到主页。我们在 Order 控制器里创建一个感谢信息的 Action 方法,再在 Views/Order 文件夹里创建一个视图。
Listing 9-12 修改 OrderController.cs 文件,加入新的方法
using EBooksStore.Extensions;
using EBooksStore.Models;
using EBooksStore.Models.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace EBooksStore.Controllers;
public class OrderController(IOrderRepository orderRepository) : Controller
{
private readonly IOrderRepository _orderRepository = orderRepository;
public IActionResult Checkout() => View(new Order());
[HttpPost]
public IActionResult Checkout(Order order)
{
var cart = HttpContext.Session.Get<List<CartLine>>("CartLines");
if (cart == null || cart.Count == 0)
{
ModelState.AddModelError("", "您的购物车是空的!");
}
else
{
List<OrderItem> orderItems = [];
foreach (var line in cart)
{
orderItems.Add(new OrderItem
{
EBookId = line.EBook.Id,
Quantity = line.Quantity
});
}
if (ModelState.IsValid)
{
order.OrderItems = orderItems;
_orderRepository.SaveOrder(order, orderItems);
HttpContext.Session.Set("CartLines", new List<CartLine>());
HttpContext.Session.Set("DisabledCartItems", new List<int>());
// 转到 OrderSummary 方法
return RedirectToAction(nameof(OrderSummary), new { orderName = order.Name, orderId = order.Id });
}
}
return View(order);
}
// 感谢页面方法
public IActionResult OrderSummary(string orderName, int orderId)
{
string orderSummary = $"{orderName}! 感谢您的购买!n您的订单号是:{orderId}";
return View((object)orderSummary);
}
}
感谢页面的 Action 方法用订单姓名和订单号构造字符串对象作为数据模型。
Listing 9-13 在 Views/Order 文件夹里新建 OrderSummary.cshtml 文件
@model string
@{
ViewData["Title"] = "订单摘要";
Layout = "~/Views/Shared/_CartLayout.cshtml";
}
<h2>订单摘要</h2>
<p>@Model</p>
<p>您的订单已经提交,我们会尽快发货。</p>
<div class="text-center my-3">
<a asp-controller="Home" asp-action="Index" class="btn btn-primary">返回首页</a>
</div>
我们试着完成订单后,会转到类似下面的页面:
Figure 9-3
我们完成了网店的基本功能,特别是面对用户的基本功能。如果可以接着打磨它,完全可以作为生成版本使用。但是,生产版本基本还是要整合其他应用,特别是付款方面的;我们的这个简陋的东西权当作引子,相信各位可能做出功能更全面,更实用的应用。
特别提醒:在 OrderRepository 的 SaveOrder 方法里应用数据库更新时,基本要用 try catch 等类似的处理异常的过程,以防止不可预知的错误。
发表回复